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, CodegenStatus, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore,
12 CycleMessageRole, DebugEditSteps, DeployHistory, DeployPromptLibrary, EditSuggestionGroup,
13 InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector,
14 PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
15 ResolvedWorkflowStepEditSuggestions, SavedContextMetadata, Split, ToggleFocus,
16 ToggleModelSelector, 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, FontWeight, InteractiveElement, IntoElement, Model, ParentElement,
38 Pixels, ReadGlobal, Render, SharedString, StatefulInteractiveElement, Styled, Subscription,
39 Task, Transformation, 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 if let Some(terminal_view) = workspace
817 .active_item(cx)
818 .and_then(|item| item.act_as::<TerminalView>(cx))
819 {
820 Some(InlineAssistTarget::Terminal(terminal_view))
821 } else {
822 None
823 }
824 }
825
826 fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
827 if self.project.read(cx).is_remote() {
828 let task = self
829 .context_store
830 .update(cx, |store, cx| store.create_remote_context(cx));
831
832 cx.spawn(|this, mut cx| async move {
833 let context = task.await?;
834
835 this.update(&mut cx, |this, cx| {
836 let workspace = this.workspace.clone();
837 let project = this.project.clone();
838 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
839
840 let fs = this.fs.clone();
841 let project = this.project.clone();
842 let weak_assistant_panel = cx.view().downgrade();
843
844 let editor = cx.new_view(|cx| {
845 ContextEditor::for_context(
846 context,
847 fs,
848 workspace,
849 project,
850 lsp_adapter_delegate,
851 weak_assistant_panel,
852 cx,
853 )
854 });
855
856 this.show_context(editor, cx);
857
858 anyhow::Ok(())
859 })??;
860
861 anyhow::Ok(())
862 })
863 .detach_and_log_err(cx);
864
865 None
866 } else {
867 let context = self.context_store.update(cx, |store, cx| store.create(cx));
868 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
869
870 let assistant_panel = cx.view().downgrade();
871 let editor = cx.new_view(|cx| {
872 let mut editor = ContextEditor::for_context(
873 context,
874 self.fs.clone(),
875 self.workspace.clone(),
876 self.project.clone(),
877 lsp_adapter_delegate,
878 assistant_panel,
879 cx,
880 );
881 editor.insert_default_prompt(cx);
882 editor
883 });
884
885 self.show_context(editor.clone(), cx);
886 Some(editor)
887 }
888 }
889
890 fn show_context(&mut self, context_editor: View<ContextEditor>, cx: &mut ViewContext<Self>) {
891 let focus = self.focus_handle(cx).contains_focused(cx);
892 let prev_len = self.pane.read(cx).items_len();
893 self.pane.update(cx, |pane, cx| {
894 pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx)
895 });
896
897 if prev_len != self.pane.read(cx).items_len() {
898 self.subscriptions
899 .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
900 }
901
902 self.show_updated_summary(&context_editor, cx);
903
904 cx.emit(AssistantPanelEvent::ContextEdited);
905 cx.notify();
906 }
907
908 fn show_updated_summary(
909 &self,
910 context_editor: &View<ContextEditor>,
911 cx: &mut ViewContext<Self>,
912 ) {
913 context_editor.update(cx, |context_editor, cx| {
914 let new_summary = context_editor
915 .context
916 .read(cx)
917 .summary()
918 .map(|s| s.text.clone())
919 .unwrap_or_else(|| context_editor.title(cx).to_string());
920 self.model_summary_editor.update(cx, |summary_editor, cx| {
921 if summary_editor.text(cx) != new_summary {
922 summary_editor.set_text(new_summary, cx);
923 }
924 });
925 });
926 }
927
928 fn handle_context_editor_event(
929 &mut self,
930 context_editor: View<ContextEditor>,
931 event: &EditorEvent,
932 cx: &mut ViewContext<Self>,
933 ) {
934 match event {
935 EditorEvent::TitleChanged => {
936 self.show_updated_summary(&context_editor, cx);
937 cx.notify()
938 }
939 EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
940 _ => {}
941 }
942 }
943
944 fn show_configuration(
945 workspace: &mut Workspace,
946 _: &ShowConfiguration,
947 cx: &mut ViewContext<Workspace>,
948 ) {
949 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
950 return;
951 };
952
953 if !panel.focus_handle(cx).contains_focused(cx) {
954 workspace.toggle_panel_focus::<AssistantPanel>(cx);
955 }
956
957 panel.update(cx, |this, cx| {
958 this.show_configuration_tab(cx);
959 })
960 }
961
962 fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
963 let configuration_item_ix = self
964 .pane
965 .read(cx)
966 .items()
967 .position(|item| item.downcast::<ConfigurationView>().is_some());
968
969 if let Some(configuration_item_ix) = configuration_item_ix {
970 self.pane.update(cx, |pane, cx| {
971 pane.activate_item(configuration_item_ix, true, true, cx);
972 });
973 } else {
974 let configuration = cx.new_view(|cx| ConfigurationView::new(cx));
975 self.configuration_subscription = Some(cx.subscribe(
976 &configuration,
977 |this, _, event: &ConfigurationViewEvent, cx| match event {
978 ConfigurationViewEvent::NewProviderContextEditor(provider) => {
979 if LanguageModelRegistry::read_global(cx)
980 .active_provider()
981 .map_or(true, |p| p.id() != provider.id())
982 {
983 if let Some(model) = provider.provided_models(cx).first().cloned() {
984 update_settings_file::<AssistantSettings>(
985 this.fs.clone(),
986 cx,
987 move |settings, _| settings.set_model(model),
988 );
989 }
990 }
991
992 this.new_context(cx);
993 }
994 },
995 ));
996 self.pane.update(cx, |pane, cx| {
997 pane.add_item(Box::new(configuration), true, true, None, cx);
998 });
999 }
1000 }
1001
1002 fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
1003 let history_item_ix = self
1004 .pane
1005 .read(cx)
1006 .items()
1007 .position(|item| item.downcast::<ContextHistory>().is_some());
1008
1009 if let Some(history_item_ix) = history_item_ix {
1010 self.pane.update(cx, |pane, cx| {
1011 pane.activate_item(history_item_ix, true, true, cx);
1012 });
1013 } else {
1014 let assistant_panel = cx.view().downgrade();
1015 let history = cx.new_view(|cx| {
1016 ContextHistory::new(
1017 self.project.clone(),
1018 self.context_store.clone(),
1019 assistant_panel,
1020 cx,
1021 )
1022 });
1023 self.pane.update(cx, |pane, cx| {
1024 pane.add_item(Box::new(history), true, true, None, cx);
1025 });
1026 }
1027 }
1028
1029 fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
1030 open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx);
1031 }
1032
1033 fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
1034 self.model_selector_menu_handle.toggle(cx);
1035 }
1036
1037 fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
1038 self.pane
1039 .read(cx)
1040 .active_item()?
1041 .downcast::<ContextEditor>()
1042 }
1043
1044 pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
1045 Some(self.active_context_editor(cx)?.read(cx).context.clone())
1046 }
1047
1048 fn open_saved_context(
1049 &mut self,
1050 path: PathBuf,
1051 cx: &mut ViewContext<Self>,
1052 ) -> Task<Result<()>> {
1053 let existing_context = self.pane.read(cx).items().find_map(|item| {
1054 item.downcast::<ContextEditor>()
1055 .filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path))
1056 });
1057 if let Some(existing_context) = existing_context {
1058 return cx.spawn(|this, mut cx| async move {
1059 this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
1060 });
1061 }
1062
1063 let context = self
1064 .context_store
1065 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1066 let fs = self.fs.clone();
1067 let project = self.project.clone();
1068 let workspace = self.workspace.clone();
1069
1070 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
1071
1072 cx.spawn(|this, mut cx| async move {
1073 let context = context.await?;
1074 let assistant_panel = this.clone();
1075 this.update(&mut cx, |this, cx| {
1076 let editor = cx.new_view(|cx| {
1077 ContextEditor::for_context(
1078 context,
1079 fs,
1080 workspace,
1081 project,
1082 lsp_adapter_delegate,
1083 assistant_panel,
1084 cx,
1085 )
1086 });
1087 this.show_context(editor, cx);
1088 anyhow::Ok(())
1089 })??;
1090 Ok(())
1091 })
1092 }
1093
1094 fn open_remote_context(
1095 &mut self,
1096 id: ContextId,
1097 cx: &mut ViewContext<Self>,
1098 ) -> Task<Result<View<ContextEditor>>> {
1099 let existing_context = self.pane.read(cx).items().find_map(|item| {
1100 item.downcast::<ContextEditor>()
1101 .filter(|editor| *editor.read(cx).context.read(cx).id() == id)
1102 });
1103 if let Some(existing_context) = existing_context {
1104 return cx.spawn(|this, mut cx| async move {
1105 this.update(&mut cx, |this, cx| {
1106 this.show_context(existing_context.clone(), cx)
1107 })?;
1108 Ok(existing_context)
1109 });
1110 }
1111
1112 let context = self
1113 .context_store
1114 .update(cx, |store, cx| store.open_remote_context(id, cx));
1115 let fs = self.fs.clone();
1116 let workspace = self.workspace.clone();
1117 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
1118
1119 cx.spawn(|this, mut cx| async move {
1120 let context = context.await?;
1121 let assistant_panel = this.clone();
1122 this.update(&mut cx, |this, cx| {
1123 let editor = cx.new_view(|cx| {
1124 ContextEditor::for_context(
1125 context,
1126 fs,
1127 workspace,
1128 this.project.clone(),
1129 lsp_adapter_delegate,
1130 assistant_panel,
1131 cx,
1132 )
1133 });
1134 this.show_context(editor.clone(), cx);
1135 anyhow::Ok(editor)
1136 })?
1137 })
1138 }
1139
1140 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1141 LanguageModelRegistry::read_global(cx)
1142 .active_provider()
1143 .map_or(false, |provider| provider.is_authenticated(cx))
1144 }
1145
1146 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1147 LanguageModelRegistry::read_global(cx)
1148 .active_provider()
1149 .map_or(
1150 Task::ready(Err(anyhow!("no active language model provider"))),
1151 |provider| provider.authenticate(cx),
1152 )
1153 }
1154}
1155
1156impl Render for AssistantPanel {
1157 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1158 let mut registrar = DivRegistrar::new(
1159 |panel, cx| {
1160 panel
1161 .pane
1162 .read(cx)
1163 .toolbar()
1164 .read(cx)
1165 .item_of_type::<BufferSearchBar>()
1166 },
1167 cx,
1168 );
1169 BufferSearchBar::register(&mut registrar);
1170 let registrar = registrar.into_div();
1171
1172 v_flex()
1173 .key_context("AssistantPanel")
1174 .size_full()
1175 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
1176 this.new_context(cx);
1177 }))
1178 .on_action(
1179 cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
1180 )
1181 .on_action(cx.listener(AssistantPanel::deploy_history))
1182 .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1183 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1184 .child(registrar.size_full().child(self.pane.clone()))
1185 .into_any_element()
1186 }
1187}
1188
1189impl Panel for AssistantPanel {
1190 fn persistent_name() -> &'static str {
1191 "AssistantPanel"
1192 }
1193
1194 fn position(&self, cx: &WindowContext) -> DockPosition {
1195 match AssistantSettings::get_global(cx).dock {
1196 AssistantDockPosition::Left => DockPosition::Left,
1197 AssistantDockPosition::Bottom => DockPosition::Bottom,
1198 AssistantDockPosition::Right => DockPosition::Right,
1199 }
1200 }
1201
1202 fn position_is_valid(&self, _: DockPosition) -> bool {
1203 true
1204 }
1205
1206 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1207 settings::update_settings_file::<AssistantSettings>(
1208 self.fs.clone(),
1209 cx,
1210 move |settings, _| {
1211 let dock = match position {
1212 DockPosition::Left => AssistantDockPosition::Left,
1213 DockPosition::Bottom => AssistantDockPosition::Bottom,
1214 DockPosition::Right => AssistantDockPosition::Right,
1215 };
1216 settings.set_dock(dock);
1217 },
1218 );
1219 }
1220
1221 fn size(&self, cx: &WindowContext) -> Pixels {
1222 let settings = AssistantSettings::get_global(cx);
1223 match self.position(cx) {
1224 DockPosition::Left | DockPosition::Right => {
1225 self.width.unwrap_or(settings.default_width)
1226 }
1227 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1228 }
1229 }
1230
1231 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1232 match self.position(cx) {
1233 DockPosition::Left | DockPosition::Right => self.width = size,
1234 DockPosition::Bottom => self.height = size,
1235 }
1236 cx.notify();
1237 }
1238
1239 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1240 self.pane.read(cx).is_zoomed()
1241 }
1242
1243 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1244 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1245 }
1246
1247 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1248 if active {
1249 if self.pane.read(cx).items_len() == 0 {
1250 self.new_context(cx);
1251 }
1252
1253 self.ensure_authenticated(cx);
1254 }
1255 }
1256
1257 fn pane(&self) -> Option<View<Pane>> {
1258 Some(self.pane.clone())
1259 }
1260
1261 fn remote_id() -> Option<proto::PanelId> {
1262 Some(proto::PanelId::AssistantPanel)
1263 }
1264
1265 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1266 let settings = AssistantSettings::get_global(cx);
1267 if !settings.enabled || !settings.button {
1268 return None;
1269 }
1270
1271 Some(IconName::ZedAssistant)
1272 }
1273
1274 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1275 Some("Assistant Panel")
1276 }
1277
1278 fn toggle_action(&self) -> Box<dyn Action> {
1279 Box::new(ToggleFocus)
1280 }
1281}
1282
1283impl EventEmitter<PanelEvent> for AssistantPanel {}
1284impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1285
1286impl FocusableView for AssistantPanel {
1287 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1288 self.pane.focus_handle(cx)
1289 }
1290}
1291
1292pub enum ContextEditorEvent {
1293 Edited,
1294 TabContentChanged,
1295}
1296
1297#[derive(Copy, Clone, Debug, PartialEq)]
1298struct ScrollPosition {
1299 offset_before_cursor: gpui::Point<f32>,
1300 cursor: Anchor,
1301}
1302
1303struct StepAssists {
1304 assist_ids: Vec<InlineAssistId>,
1305 editor: WeakView<Editor>,
1306}
1307
1308#[derive(Debug, Eq, PartialEq)]
1309struct ActiveWorkflowStep {
1310 range: Range<language::Anchor>,
1311 suggestions: Option<ResolvedWorkflowStepEditSuggestions>,
1312}
1313
1314pub struct ContextEditor {
1315 context: Model<Context>,
1316 fs: Arc<dyn Fs>,
1317 workspace: WeakView<Workspace>,
1318 project: Model<Project>,
1319 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1320 editor: View<Editor>,
1321 blocks: HashSet<CustomBlockId>,
1322 scroll_position: Option<ScrollPosition>,
1323 remote_id: Option<workspace::ViewId>,
1324 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
1325 pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
1326 _subscriptions: Vec<Subscription>,
1327 assists_by_step: HashMap<Range<language::Anchor>, StepAssists>,
1328 active_workflow_step: Option<ActiveWorkflowStep>,
1329 assistant_panel: WeakView<AssistantPanel>,
1330 error_message: Option<SharedString>,
1331}
1332
1333const DEFAULT_TAB_TITLE: &str = "New Context";
1334const MAX_TAB_TITLE_LEN: usize = 16;
1335
1336impl ContextEditor {
1337 fn for_context(
1338 context: Model<Context>,
1339 fs: Arc<dyn Fs>,
1340 workspace: WeakView<Workspace>,
1341 project: Model<Project>,
1342 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1343 assistant_panel: WeakView<AssistantPanel>,
1344 cx: &mut ViewContext<Self>,
1345 ) -> Self {
1346 let completion_provider = SlashCommandCompletionProvider::new(
1347 Some(cx.view().downgrade()),
1348 Some(workspace.clone()),
1349 );
1350
1351 let editor = cx.new_view(|cx| {
1352 let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
1353 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1354 editor.set_show_line_numbers(false, cx);
1355 editor.set_show_git_diff_gutter(false, cx);
1356 editor.set_show_code_actions(false, cx);
1357 editor.set_show_runnables(false, cx);
1358 editor.set_show_wrap_guides(false, cx);
1359 editor.set_show_indent_guides(false, cx);
1360 editor.set_completion_provider(Box::new(completion_provider));
1361 editor.set_collaboration_hub(Box::new(project.clone()));
1362 editor
1363 });
1364
1365 let _subscriptions = vec![
1366 cx.observe(&context, |_, _, cx| cx.notify()),
1367 cx.subscribe(&context, Self::handle_context_event),
1368 cx.subscribe(&editor, Self::handle_editor_event),
1369 cx.subscribe(&editor, Self::handle_editor_search_event),
1370 ];
1371
1372 let sections = context.read(cx).slash_command_output_sections().to_vec();
1373 let mut this = Self {
1374 context,
1375 editor,
1376 lsp_adapter_delegate,
1377 blocks: Default::default(),
1378 scroll_position: None,
1379 remote_id: None,
1380 fs,
1381 workspace,
1382 project,
1383 pending_slash_command_creases: HashMap::default(),
1384 pending_slash_command_blocks: HashMap::default(),
1385 _subscriptions,
1386 assists_by_step: HashMap::default(),
1387 active_workflow_step: None,
1388 assistant_panel,
1389 error_message: None,
1390 };
1391 this.update_message_headers(cx);
1392 this.insert_slash_command_output_sections(sections, cx);
1393 this
1394 }
1395
1396 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
1397 let command_name = DefaultSlashCommand.name();
1398 self.editor.update(cx, |editor, cx| {
1399 editor.insert(&format!("/{command_name}"), cx)
1400 });
1401 self.split(&Split, cx);
1402 let command = self.context.update(cx, |context, cx| {
1403 let first_message_id = context.messages(cx).next().unwrap().id;
1404 context.update_metadata(first_message_id, cx, |metadata| {
1405 metadata.role = Role::System;
1406 });
1407 context.reparse_slash_commands(cx);
1408 context.pending_slash_commands()[0].clone()
1409 });
1410
1411 self.run_command(
1412 command.source_range,
1413 &command.name,
1414 command.argument.as_deref(),
1415 false,
1416 self.workspace.clone(),
1417 cx,
1418 );
1419 }
1420
1421 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1422 if !self.apply_edit_step(cx) {
1423 self.error_message = None;
1424 self.send_to_model(cx);
1425 cx.notify();
1426 }
1427 }
1428
1429 fn apply_edit_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
1430 if let Some(step) = self.active_workflow_step.as_ref() {
1431 if let Some(assists) = self.assists_by_step.get(&step.range) {
1432 let assist_ids = assists.assist_ids.clone();
1433 cx.window_context().defer(|cx| {
1434 InlineAssistant::update_global(cx, |assistant, cx| {
1435 for assist_id in assist_ids {
1436 assistant.start_assist(assist_id, cx);
1437 }
1438 })
1439 });
1440
1441 !assists.assist_ids.is_empty()
1442 } else {
1443 false
1444 }
1445 } else {
1446 false
1447 }
1448 }
1449
1450 fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
1451 if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
1452 let new_selection = {
1453 let cursor = user_message
1454 .start
1455 .to_offset(self.context.read(cx).buffer().read(cx));
1456 cursor..cursor
1457 };
1458 self.editor.update(cx, |editor, cx| {
1459 editor.change_selections(
1460 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1461 cx,
1462 |selections| selections.select_ranges([new_selection]),
1463 );
1464 });
1465 // Avoid scrolling to the new cursor position so the assistant's output is stable.
1466 cx.defer(|this, _| this.scroll_position = None);
1467 }
1468 }
1469
1470 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1471 if !self
1472 .context
1473 .update(cx, |context, _| context.cancel_last_assist())
1474 {
1475 cx.propagate();
1476 }
1477 }
1478
1479 fn debug_edit_steps(&mut self, _: &DebugEditSteps, cx: &mut ViewContext<Self>) {
1480 let mut output = String::new();
1481 for (i, step) in self.context.read(cx).workflow_steps().iter().enumerate() {
1482 output.push_str(&format!("Step {}:\n", i + 1));
1483 output.push_str(&format!(
1484 "Content: {}\n",
1485 self.context
1486 .read(cx)
1487 .buffer()
1488 .read(cx)
1489 .text_for_range(step.tagged_range.clone())
1490 .collect::<String>()
1491 ));
1492 match &step.edit_suggestions {
1493 WorkflowStepEditSuggestions::Resolved(ResolvedWorkflowStepEditSuggestions {
1494 title,
1495 edit_suggestions,
1496 }) => {
1497 output.push_str("Resolution:\n");
1498 output.push_str(&format!(" {:?}\n", title));
1499 output.push_str(&format!(" {:?}\n", edit_suggestions));
1500 }
1501 WorkflowStepEditSuggestions::Pending(_) => {
1502 output.push_str("Resolution: Pending\n");
1503 }
1504 }
1505 output.push('\n');
1506 }
1507
1508 let editor = self
1509 .workspace
1510 .update(cx, |workspace, cx| Editor::new_in_workspace(workspace, cx));
1511
1512 if let Ok(editor) = editor {
1513 cx.spawn(|_, mut cx| async move {
1514 let editor = editor.await?;
1515 editor.update(&mut cx, |editor, cx| editor.set_text(output, cx))
1516 })
1517 .detach_and_notify_err(cx);
1518 }
1519 }
1520
1521 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
1522 let cursors = self.cursors(cx);
1523 self.context.update(cx, |context, cx| {
1524 let messages = context
1525 .messages_for_offsets(cursors, cx)
1526 .into_iter()
1527 .map(|message| message.id)
1528 .collect();
1529 context.cycle_message_roles(messages, cx)
1530 });
1531 }
1532
1533 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
1534 let selections = self.editor.read(cx).selections.all::<usize>(cx);
1535 selections
1536 .into_iter()
1537 .map(|selection| selection.head())
1538 .collect()
1539 }
1540
1541 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
1542 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1543 self.editor.update(cx, |editor, cx| {
1544 editor.transact(cx, |editor, cx| {
1545 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
1546 let snapshot = editor.buffer().read(cx).snapshot(cx);
1547 let newest_cursor = editor.selections.newest::<Point>(cx).head();
1548 if newest_cursor.column > 0
1549 || snapshot
1550 .chars_at(newest_cursor)
1551 .next()
1552 .map_or(false, |ch| ch != '\n')
1553 {
1554 editor.move_to_end_of_line(
1555 &MoveToEndOfLine {
1556 stop_at_soft_wraps: false,
1557 },
1558 cx,
1559 );
1560 editor.newline(&Newline, cx);
1561 }
1562
1563 editor.insert(&format!("/{name}"), cx);
1564 if command.requires_argument() {
1565 editor.insert(" ", cx);
1566 editor.show_completions(&ShowCompletions::default(), cx);
1567 }
1568 });
1569 });
1570 if !command.requires_argument() {
1571 self.confirm_command(&ConfirmCommand, cx);
1572 }
1573 }
1574 }
1575
1576 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
1577 let selections = self.editor.read(cx).selections.disjoint_anchors();
1578 let mut commands_by_range = HashMap::default();
1579 let workspace = self.workspace.clone();
1580 self.context.update(cx, |context, cx| {
1581 context.reparse_slash_commands(cx);
1582 for selection in selections.iter() {
1583 if let Some(command) =
1584 context.pending_command_for_position(selection.head().text_anchor, cx)
1585 {
1586 commands_by_range
1587 .entry(command.source_range.clone())
1588 .or_insert_with(|| command.clone());
1589 }
1590 }
1591 });
1592
1593 if commands_by_range.is_empty() {
1594 cx.propagate();
1595 } else {
1596 for command in commands_by_range.into_values() {
1597 self.run_command(
1598 command.source_range,
1599 &command.name,
1600 command.argument.as_deref(),
1601 true,
1602 workspace.clone(),
1603 cx,
1604 );
1605 }
1606 cx.stop_propagation();
1607 }
1608 }
1609
1610 pub fn run_command(
1611 &mut self,
1612 command_range: Range<language::Anchor>,
1613 name: &str,
1614 argument: Option<&str>,
1615 insert_trailing_newline: bool,
1616 workspace: WeakView<Workspace>,
1617 cx: &mut ViewContext<Self>,
1618 ) {
1619 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1620 let argument = argument.map(ToString::to_string);
1621 let output = command.run(
1622 argument.as_deref(),
1623 workspace,
1624 self.lsp_adapter_delegate.clone(),
1625 cx,
1626 );
1627 self.context.update(cx, |context, cx| {
1628 context.insert_command_output(command_range, output, insert_trailing_newline, cx)
1629 });
1630 }
1631 }
1632
1633 fn handle_context_event(
1634 &mut self,
1635 _: Model<Context>,
1636 event: &ContextEvent,
1637 cx: &mut ViewContext<Self>,
1638 ) {
1639 let context_editor = cx.view().downgrade();
1640
1641 match event {
1642 ContextEvent::MessagesEdited => {
1643 self.update_message_headers(cx);
1644 self.context.update(cx, |context, cx| {
1645 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1646 });
1647 }
1648 ContextEvent::WorkflowStepsChanged => {
1649 self.update_active_workflow_step(cx);
1650 cx.notify();
1651 }
1652 ContextEvent::SummaryChanged => {
1653 cx.emit(EditorEvent::TitleChanged);
1654 self.context.update(cx, |context, cx| {
1655 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1656 });
1657 }
1658 ContextEvent::StreamedCompletion => {
1659 self.editor.update(cx, |editor, cx| {
1660 if let Some(scroll_position) = self.scroll_position {
1661 let snapshot = editor.snapshot(cx);
1662 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1663 let scroll_top =
1664 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
1665 editor.set_scroll_position(
1666 point(scroll_position.offset_before_cursor.x, scroll_top),
1667 cx,
1668 );
1669 }
1670 });
1671 }
1672 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
1673 self.editor.update(cx, |editor, cx| {
1674 let buffer = editor.buffer().read(cx).snapshot(cx);
1675 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
1676 let excerpt_id = *excerpt_id;
1677
1678 editor.remove_creases(
1679 removed
1680 .iter()
1681 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
1682 cx,
1683 );
1684
1685 editor.remove_blocks(
1686 HashSet::from_iter(
1687 removed.iter().filter_map(|range| {
1688 self.pending_slash_command_blocks.remove(range)
1689 }),
1690 ),
1691 None,
1692 cx,
1693 );
1694
1695 let crease_ids = editor.insert_creases(
1696 updated.iter().map(|command| {
1697 let workspace = self.workspace.clone();
1698 let confirm_command = Arc::new({
1699 let context_editor = context_editor.clone();
1700 let command = command.clone();
1701 move |cx: &mut WindowContext| {
1702 context_editor
1703 .update(cx, |context_editor, cx| {
1704 context_editor.run_command(
1705 command.source_range.clone(),
1706 &command.name,
1707 command.argument.as_deref(),
1708 false,
1709 workspace.clone(),
1710 cx,
1711 );
1712 })
1713 .ok();
1714 }
1715 });
1716 let placeholder = FoldPlaceholder {
1717 render: Arc::new(move |_, _, _| Empty.into_any()),
1718 constrain_width: false,
1719 merge_adjacent: false,
1720 };
1721 let render_toggle = {
1722 let confirm_command = confirm_command.clone();
1723 let command = command.clone();
1724 move |row, _, _, _cx: &mut WindowContext| {
1725 render_pending_slash_command_gutter_decoration(
1726 row,
1727 &command.status,
1728 confirm_command.clone(),
1729 )
1730 }
1731 };
1732 let render_trailer = {
1733 let command = command.clone();
1734 move |row, _unfold, cx: &mut WindowContext| {
1735 // TODO: In the future we should investigate how we can expose
1736 // this as a hook on the `SlashCommand` trait so that we don't
1737 // need to special-case it here.
1738 if command.name == DocsSlashCommand::NAME {
1739 return render_docs_slash_command_trailer(
1740 row,
1741 command.clone(),
1742 cx,
1743 );
1744 }
1745
1746 Empty.into_any()
1747 }
1748 };
1749
1750 let start = buffer
1751 .anchor_in_excerpt(excerpt_id, command.source_range.start)
1752 .unwrap();
1753 let end = buffer
1754 .anchor_in_excerpt(excerpt_id, command.source_range.end)
1755 .unwrap();
1756 Crease::new(start..end, placeholder, render_toggle, render_trailer)
1757 }),
1758 cx,
1759 );
1760
1761 let block_ids = editor.insert_blocks(
1762 updated
1763 .iter()
1764 .filter_map(|command| match &command.status {
1765 PendingSlashCommandStatus::Error(error) => {
1766 Some((command, error.clone()))
1767 }
1768 _ => None,
1769 })
1770 .map(|(command, error_message)| BlockProperties {
1771 style: BlockStyle::Fixed,
1772 position: Anchor {
1773 buffer_id: Some(buffer_id),
1774 excerpt_id,
1775 text_anchor: command.source_range.start,
1776 },
1777 height: 1,
1778 disposition: BlockDisposition::Below,
1779 render: slash_command_error_block_renderer(error_message),
1780 }),
1781 None,
1782 cx,
1783 );
1784
1785 self.pending_slash_command_creases.extend(
1786 updated
1787 .iter()
1788 .map(|command| command.source_range.clone())
1789 .zip(crease_ids),
1790 );
1791
1792 self.pending_slash_command_blocks.extend(
1793 updated
1794 .iter()
1795 .map(|command| command.source_range.clone())
1796 .zip(block_ids),
1797 );
1798 })
1799 }
1800 ContextEvent::SlashCommandFinished {
1801 output_range,
1802 sections,
1803 run_commands_in_output,
1804 } => {
1805 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
1806
1807 if *run_commands_in_output {
1808 let commands = self.context.update(cx, |context, cx| {
1809 context.reparse_slash_commands(cx);
1810 context
1811 .pending_commands_for_range(output_range.clone(), cx)
1812 .to_vec()
1813 });
1814
1815 for command in commands {
1816 self.run_command(
1817 command.source_range,
1818 &command.name,
1819 command.argument.as_deref(),
1820 false,
1821 self.workspace.clone(),
1822 cx,
1823 );
1824 }
1825 }
1826 }
1827 ContextEvent::Operation(_) => {}
1828 ContextEvent::AssistError(error_message) => {
1829 self.error_message = Some(SharedString::from(error_message.clone()));
1830 }
1831 }
1832 }
1833
1834 fn insert_slash_command_output_sections(
1835 &mut self,
1836 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
1837 cx: &mut ViewContext<Self>,
1838 ) {
1839 self.editor.update(cx, |editor, cx| {
1840 let buffer = editor.buffer().read(cx).snapshot(cx);
1841 let excerpt_id = *buffer.as_singleton().unwrap().0;
1842 let mut buffer_rows_to_fold = BTreeSet::new();
1843 let mut creases = Vec::new();
1844 for section in sections {
1845 let start = buffer
1846 .anchor_in_excerpt(excerpt_id, section.range.start)
1847 .unwrap();
1848 let end = buffer
1849 .anchor_in_excerpt(excerpt_id, section.range.end)
1850 .unwrap();
1851 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1852 buffer_rows_to_fold.insert(buffer_row);
1853 creases.push(Crease::new(
1854 start..end,
1855 FoldPlaceholder {
1856 render: Arc::new({
1857 let editor = cx.view().downgrade();
1858 let icon = section.icon;
1859 let label = section.label.clone();
1860 move |fold_id, fold_range, _cx| {
1861 let editor = editor.clone();
1862 ButtonLike::new(fold_id)
1863 .style(ButtonStyle::Filled)
1864 .layer(ElevationIndex::ElevatedSurface)
1865 .child(Icon::new(icon))
1866 .child(Label::new(label.clone()).single_line())
1867 .on_click(move |_, cx| {
1868 editor
1869 .update(cx, |editor, cx| {
1870 let buffer_start = fold_range
1871 .start
1872 .to_point(&editor.buffer().read(cx).read(cx));
1873 let buffer_row = MultiBufferRow(buffer_start.row);
1874 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
1875 })
1876 .ok();
1877 })
1878 .into_any_element()
1879 }
1880 }),
1881 constrain_width: false,
1882 merge_adjacent: false,
1883 },
1884 render_slash_command_output_toggle,
1885 |_, _, _| Empty.into_any_element(),
1886 ));
1887 }
1888
1889 editor.insert_creases(creases, cx);
1890
1891 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1892 editor.fold_at(&FoldAt { buffer_row }, cx);
1893 }
1894 });
1895 }
1896
1897 fn handle_editor_event(
1898 &mut self,
1899 _: View<Editor>,
1900 event: &EditorEvent,
1901 cx: &mut ViewContext<Self>,
1902 ) {
1903 match event {
1904 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
1905 let cursor_scroll_position = self.cursor_scroll_position(cx);
1906 if *autoscroll {
1907 self.scroll_position = cursor_scroll_position;
1908 } else if self.scroll_position != cursor_scroll_position {
1909 self.scroll_position = None;
1910 }
1911 }
1912 EditorEvent::SelectionsChanged { .. } => {
1913 self.scroll_position = self.cursor_scroll_position(cx);
1914 self.update_active_workflow_step(cx);
1915 }
1916 _ => {}
1917 }
1918 cx.emit(event.clone());
1919 }
1920
1921 fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
1922 let new_step = self
1923 .workflow_step_range_for_cursor(cx)
1924 .as_ref()
1925 .and_then(|step_range| {
1926 let workflow_step = self
1927 .context
1928 .read(cx)
1929 .workflow_step_for_range(step_range.clone())?;
1930 Some(ActiveWorkflowStep {
1931 range: workflow_step.tagged_range.clone(),
1932 suggestions: workflow_step.edit_suggestions.as_resolved().cloned(),
1933 })
1934 });
1935 if new_step.as_ref() != self.active_workflow_step.as_ref() {
1936 if let Some(old_step) = self.active_workflow_step.take() {
1937 self.cancel_workflow_step_if_idle(old_step.range, cx);
1938 }
1939
1940 if let Some(new_step) = new_step {
1941 self.activate_workflow_step(new_step, cx);
1942 }
1943 }
1944 }
1945
1946 fn cancel_workflow_step_if_idle(
1947 &mut self,
1948 step_range: Range<language::Anchor>,
1949 cx: &mut ViewContext<Self>,
1950 ) {
1951 let Some(step_assists) = self.assists_by_step.get_mut(&step_range) else {
1952 return;
1953 };
1954 let Some(editor) = step_assists.editor.upgrade() else {
1955 self.assists_by_step.remove(&step_range);
1956 return;
1957 };
1958
1959 InlineAssistant::update_global(cx, |assistant, cx| {
1960 step_assists.assist_ids.retain(|assist_id| {
1961 match assistant.status_for_assist(*assist_id, cx) {
1962 Some(CodegenStatus::Idle) | None => {
1963 assistant.finish_assist(*assist_id, true, cx);
1964 false
1965 }
1966 _ => true,
1967 }
1968 });
1969 });
1970
1971 if step_assists.assist_ids.is_empty() {
1972 self.assists_by_step.remove(&step_range);
1973 self.workspace
1974 .update(cx, |workspace, cx| {
1975 if let Some(pane) = workspace.pane_for(&editor) {
1976 pane.update(cx, |pane, cx| {
1977 let item_id = editor.entity_id();
1978 if pane.is_active_preview_item(item_id) {
1979 pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
1980 .detach_and_log_err(cx);
1981 }
1982 });
1983 }
1984 })
1985 .ok();
1986 }
1987 }
1988
1989 fn activate_workflow_step(&mut self, step: ActiveWorkflowStep, cx: &mut ViewContext<Self>) {
1990 if let Some(step_assists) = self.assists_by_step.get(&step.range) {
1991 if let Some(editor) = step_assists.editor.upgrade() {
1992 for assist_id in &step_assists.assist_ids {
1993 match InlineAssistant::global(cx).status_for_assist(*assist_id, cx) {
1994 Some(CodegenStatus::Idle) | None => {}
1995 _ => {
1996 self.workspace
1997 .update(cx, |workspace, cx| {
1998 workspace.activate_item(&editor, false, false, cx);
1999 })
2000 .ok();
2001 InlineAssistant::update_global(cx, |assistant, cx| {
2002 assistant.scroll_to_assist(*assist_id, cx)
2003 });
2004 return;
2005 }
2006 }
2007 }
2008 }
2009 }
2010
2011 if let Some(ResolvedWorkflowStepEditSuggestions {
2012 title,
2013 edit_suggestions,
2014 }) = step.suggestions.as_ref()
2015 {
2016 if let Some((editor, assist_ids)) =
2017 self.suggest_edits(title.clone(), edit_suggestions.clone(), cx)
2018 {
2019 self.assists_by_step.insert(
2020 step.range.clone(),
2021 StepAssists {
2022 assist_ids,
2023 editor: editor.downgrade(),
2024 },
2025 );
2026 }
2027 }
2028
2029 self.active_workflow_step = Some(step);
2030 }
2031
2032 fn suggest_edits(
2033 &mut self,
2034 title: String,
2035 edit_suggestions: HashMap<Model<Buffer>, Vec<EditSuggestionGroup>>,
2036 cx: &mut ViewContext<Self>,
2037 ) -> Option<(View<Editor>, Vec<InlineAssistId>)> {
2038 let assistant_panel = self.assistant_panel.upgrade()?;
2039 if edit_suggestions.is_empty() {
2040 return None;
2041 }
2042
2043 let editor;
2044 let mut suggestion_groups = Vec::new();
2045 if edit_suggestions.len() == 1 && edit_suggestions.values().next().unwrap().len() == 1 {
2046 // If there's only one buffer and one suggestion group, open it directly
2047 let (buffer, groups) = edit_suggestions.into_iter().next().unwrap();
2048 let group = groups.into_iter().next().unwrap();
2049 editor = self
2050 .workspace
2051 .update(cx, |workspace, cx| {
2052 let active_pane = workspace.active_pane().clone();
2053 workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
2054 })
2055 .log_err()?;
2056
2057 let (&excerpt_id, _, _) = editor
2058 .read(cx)
2059 .buffer()
2060 .read(cx)
2061 .read(cx)
2062 .as_singleton()
2063 .unwrap();
2064
2065 // Scroll the editor to the suggested assist
2066 editor.update(cx, |editor, cx| {
2067 let multibuffer = editor.buffer().read(cx).snapshot(cx);
2068 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
2069 let anchor = if group.context_range.start.to_offset(buffer) == 0 {
2070 Anchor::min()
2071 } else {
2072 multibuffer
2073 .anchor_in_excerpt(excerpt_id, group.context_range.start)
2074 .unwrap()
2075 };
2076
2077 editor.set_scroll_anchor(
2078 ScrollAnchor {
2079 offset: gpui::Point::default(),
2080 anchor,
2081 },
2082 cx,
2083 );
2084 });
2085
2086 suggestion_groups.push((excerpt_id, group));
2087 } else {
2088 // If there are multiple buffers or suggestion groups, create a multibuffer
2089 let multibuffer = cx.new_model(|cx| {
2090 let replica_id = self.project.read(cx).replica_id();
2091 let mut multibuffer =
2092 MultiBuffer::new(replica_id, Capability::ReadWrite).with_title(title);
2093 for (buffer, groups) in edit_suggestions {
2094 let excerpt_ids = multibuffer.push_excerpts(
2095 buffer,
2096 groups.iter().map(|suggestion_group| ExcerptRange {
2097 context: suggestion_group.context_range.clone(),
2098 primary: None,
2099 }),
2100 cx,
2101 );
2102 suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
2103 }
2104 multibuffer
2105 });
2106
2107 editor = cx.new_view(|cx| {
2108 Editor::for_multibuffer(multibuffer, Some(self.project.clone()), true, cx)
2109 });
2110 self.workspace
2111 .update(cx, |workspace, cx| {
2112 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2113 })
2114 .log_err()?;
2115 }
2116
2117 let mut assist_ids = Vec::new();
2118 for (excerpt_id, suggestion_group) in suggestion_groups {
2119 for suggestion in suggestion_group.suggestions {
2120 assist_ids.extend(suggestion.show(
2121 &editor,
2122 excerpt_id,
2123 &self.workspace,
2124 &assistant_panel,
2125 cx,
2126 ));
2127 }
2128 }
2129 Some((editor, assist_ids))
2130 }
2131
2132 fn handle_editor_search_event(
2133 &mut self,
2134 _: View<Editor>,
2135 event: &SearchEvent,
2136 cx: &mut ViewContext<Self>,
2137 ) {
2138 cx.emit(event.clone());
2139 }
2140
2141 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2142 self.editor.update(cx, |editor, cx| {
2143 let snapshot = editor.snapshot(cx);
2144 let cursor = editor.selections.newest_anchor().head();
2145 let cursor_row = cursor
2146 .to_display_point(&snapshot.display_snapshot)
2147 .row()
2148 .as_f32();
2149 let scroll_position = editor
2150 .scroll_manager
2151 .anchor()
2152 .scroll_position(&snapshot.display_snapshot);
2153
2154 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2155 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2156 Some(ScrollPosition {
2157 cursor,
2158 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2159 })
2160 } else {
2161 None
2162 }
2163 })
2164 }
2165
2166 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2167 self.editor.update(cx, |editor, cx| {
2168 let buffer = editor.buffer().read(cx).snapshot(cx);
2169 let excerpt_id = *buffer.as_singleton().unwrap().0;
2170 let old_blocks = std::mem::take(&mut self.blocks);
2171 let new_blocks = self
2172 .context
2173 .read(cx)
2174 .messages(cx)
2175 .map(|message| BlockProperties {
2176 position: buffer
2177 .anchor_in_excerpt(excerpt_id, message.anchor)
2178 .unwrap(),
2179 height: 2,
2180 style: BlockStyle::Sticky,
2181 render: Box::new({
2182 let context = self.context.clone();
2183 move |cx| {
2184 let message_id = message.id;
2185 let sender = ButtonLike::new("role")
2186 .style(ButtonStyle::Filled)
2187 .child(match message.role {
2188 Role::User => Label::new("You").color(Color::Default),
2189 Role::Assistant => Label::new("Assistant").color(Color::Info),
2190 Role::System => Label::new("System").color(Color::Warning),
2191 })
2192 .tooltip(|cx| {
2193 Tooltip::with_meta(
2194 "Toggle message role",
2195 None,
2196 "Available roles: You (User), Assistant, System",
2197 cx,
2198 )
2199 })
2200 .on_click({
2201 let context = context.clone();
2202 move |_, cx| {
2203 context.update(cx, |context, cx| {
2204 context.cycle_message_roles(
2205 HashSet::from_iter(Some(message_id)),
2206 cx,
2207 )
2208 })
2209 }
2210 });
2211
2212 h_flex()
2213 .id(("message_header", message_id.as_u64()))
2214 .pl(cx.gutter_dimensions.full_width())
2215 .h_11()
2216 .w_full()
2217 .relative()
2218 .gap_1()
2219 .child(sender)
2220 .children(
2221 if let MessageStatus::Error(error) = message.status.clone() {
2222 Some(
2223 div()
2224 .id("error")
2225 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2226 .child(
2227 Icon::new(IconName::ExclamationTriangle)
2228 .color(Color::Error),
2229 ),
2230 )
2231 } else {
2232 None
2233 },
2234 )
2235 .into_any_element()
2236 }
2237 }),
2238 disposition: BlockDisposition::Above,
2239 })
2240 .collect::<Vec<_>>();
2241
2242 editor.remove_blocks(old_blocks, None, cx);
2243 let ids = editor.insert_blocks(new_blocks, None, cx);
2244 self.blocks = HashSet::from_iter(ids);
2245 });
2246 }
2247
2248 fn insert_selection(
2249 workspace: &mut Workspace,
2250 _: &InsertIntoEditor,
2251 cx: &mut ViewContext<Workspace>,
2252 ) {
2253 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2254 return;
2255 };
2256 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2257 return;
2258 };
2259 let Some(active_editor_view) = workspace
2260 .active_item(cx)
2261 .and_then(|item| item.act_as::<Editor>(cx))
2262 else {
2263 return;
2264 };
2265
2266 let context_editor = context_editor_view.read(cx).editor.read(cx);
2267 let anchor = context_editor.selections.newest_anchor();
2268 let text = context_editor
2269 .buffer()
2270 .read(cx)
2271 .read(cx)
2272 .text_for_range(anchor.range())
2273 .collect::<String>();
2274
2275 // If nothing is selected, don't delete the current selection; instead, be a no-op.
2276 if !text.is_empty() {
2277 active_editor_view.update(cx, |editor, cx| {
2278 editor.insert(&text, cx);
2279 editor.focus(cx);
2280 })
2281 }
2282 }
2283
2284 fn quote_selection(
2285 workspace: &mut Workspace,
2286 _: &QuoteSelection,
2287 cx: &mut ViewContext<Workspace>,
2288 ) {
2289 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2290 return;
2291 };
2292 let Some(editor) = workspace
2293 .active_item(cx)
2294 .and_then(|item| item.act_as::<Editor>(cx))
2295 else {
2296 return;
2297 };
2298
2299 let selection = editor.update(cx, |editor, cx| editor.selections.newest_adjusted(cx));
2300 let editor = editor.read(cx);
2301 let buffer = editor.buffer().read(cx).snapshot(cx);
2302 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
2303 ..editor::ToOffset::to_offset(&selection.end, &buffer);
2304 let start_language = buffer.language_at(range.start);
2305 let end_language = buffer.language_at(range.end);
2306 let language_name = if start_language == end_language {
2307 start_language.map(|language| language.code_fence_block_name())
2308 } else {
2309 None
2310 };
2311 let language_name = language_name.as_deref().unwrap_or("");
2312
2313 let selected_text = buffer.text_for_range(range).collect::<String>();
2314 let text = if selected_text.is_empty() {
2315 None
2316 } else {
2317 Some(if language_name == "markdown" {
2318 selected_text
2319 .lines()
2320 .map(|line| format!("> {}", line))
2321 .collect::<Vec<_>>()
2322 .join("\n")
2323 } else {
2324 format!("```{language_name}\n{selected_text}\n```")
2325 })
2326 };
2327
2328 // Activate the panel
2329 if !panel.focus_handle(cx).contains_focused(cx) {
2330 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2331 }
2332
2333 if let Some(text) = text {
2334 panel.update(cx, |_, cx| {
2335 // Wait to create a new context until the workspace is no longer
2336 // being updated.
2337 cx.defer(move |panel, cx| {
2338 if let Some(context) = panel
2339 .active_context_editor(cx)
2340 .or_else(|| panel.new_context(cx))
2341 {
2342 context.update(cx, |context, cx| {
2343 context
2344 .editor
2345 .update(cx, |editor, cx| editor.insert(&text, cx))
2346 });
2347 };
2348 });
2349 });
2350 }
2351 }
2352
2353 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2354 let editor = self.editor.read(cx);
2355 let context = self.context.read(cx);
2356 if editor.selections.count() == 1 {
2357 let selection = editor.selections.newest::<usize>(cx);
2358 let mut copied_text = String::new();
2359 let mut spanned_messages = 0;
2360 for message in context.messages(cx) {
2361 if message.offset_range.start >= selection.range().end {
2362 break;
2363 } else if message.offset_range.end >= selection.range().start {
2364 let range = cmp::max(message.offset_range.start, selection.range().start)
2365 ..cmp::min(message.offset_range.end, selection.range().end);
2366 if !range.is_empty() {
2367 spanned_messages += 1;
2368 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2369 for chunk in context.buffer().read(cx).text_for_range(range) {
2370 copied_text.push_str(chunk);
2371 }
2372 copied_text.push('\n');
2373 }
2374 }
2375 }
2376
2377 if spanned_messages > 1 {
2378 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2379 return;
2380 }
2381 }
2382
2383 cx.propagate();
2384 }
2385
2386 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2387 self.context.update(cx, |context, cx| {
2388 let selections = self.editor.read(cx).selections.disjoint_anchors();
2389 for selection in selections.as_ref() {
2390 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2391 let range = selection
2392 .map(|endpoint| endpoint.to_offset(&buffer))
2393 .range();
2394 context.split_message(range, cx);
2395 }
2396 });
2397 }
2398
2399 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2400 self.context.update(cx, |context, cx| {
2401 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2402 });
2403 }
2404
2405 fn title(&self, cx: &AppContext) -> Cow<str> {
2406 self.context
2407 .read(cx)
2408 .summary()
2409 .map(|summary| summary.text.clone())
2410 .map(Cow::Owned)
2411 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
2412 }
2413
2414 fn dismiss_error_message(&mut self, cx: &mut ViewContext<Self>) {
2415 self.error_message = None;
2416 cx.notify();
2417 }
2418
2419 fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
2420 let nudge = self
2421 .assistant_panel
2422 .upgrade()
2423 .map(|assistant_panel| assistant_panel.read(cx).show_zed_ai_notice);
2424
2425 if let Some(error) = self.error_message.clone() {
2426 Some(Self::render_error_popover(error, cx).into_any_element())
2427 } else if nudge.unwrap_or(false) {
2428 Some(
2429 v_flex()
2430 .elevation_3(cx)
2431 .p_2()
2432 .gap_2()
2433 .child(
2434 Label::new("Use Zed AI")
2435 .size(LabelSize::Small)
2436 .color(Color::Muted),
2437 )
2438 .child(h_flex().justify_end().child(
2439 Button::new("sign-in", "Sign in to use Zed AI").on_click(cx.listener(
2440 |this, _event, cx| {
2441 let client = this
2442 .workspace
2443 .update(cx, |workspace, _| workspace.client().clone())
2444 .log_err();
2445
2446 if let Some(client) = client {
2447 cx.spawn(|this, mut cx| async move {
2448 client.authenticate_and_connect(true, &mut cx).await?;
2449 this.update(&mut cx, |_, cx| cx.notify())
2450 })
2451 .detach_and_log_err(cx)
2452 }
2453 },
2454 )),
2455 ))
2456 .into_any_element(),
2457 )
2458 } else if let Some(configuration_error) = configuration_error(cx) {
2459 let label = match configuration_error {
2460 ConfigurationError::NoProvider => "No provider configured",
2461 ConfigurationError::ProviderNotAuthenticated => "Provider is not configured",
2462 };
2463 Some(
2464 v_flex()
2465 .elevation_3(cx)
2466 .p_2()
2467 .gap_2()
2468 .child(Label::new(label).size(LabelSize::Small).color(Color::Muted))
2469 .child(
2470 h_flex().justify_end().child(
2471 Button::new("open-configuration", "Open configuration")
2472 .icon(IconName::Settings)
2473 .icon_size(IconSize::Small)
2474 .on_click({
2475 let focus_handle = self.focus_handle(cx).clone();
2476 move |_event, cx| {
2477 focus_handle.dispatch_action(&ShowConfiguration, cx);
2478 }
2479 }),
2480 ),
2481 )
2482 .into_any_element(),
2483 )
2484 } else {
2485 None
2486 }
2487 }
2488
2489 fn render_error_popover(error: SharedString, cx: &mut ViewContext<Self>) -> Div {
2490 v_flex()
2491 .p_2()
2492 .elevation_2(cx)
2493 .bg(cx.theme().colors().surface_background)
2494 .min_w_24()
2495 .occlude()
2496 .child(
2497 Label::new("Error interacting with language model")
2498 .size(LabelSize::Small)
2499 .weight(FontWeight::BOLD)
2500 .color(Color::Muted),
2501 )
2502 .child(Label::new(error).size(LabelSize::Small))
2503 .child(
2504 h_flex().justify_end().child(
2505 Button::new("dismiss", "Dismiss")
2506 .on_click(cx.listener(|this, _, cx| this.dismiss_error_message(cx))),
2507 ),
2508 )
2509 }
2510
2511 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2512 let focus_handle = self.focus_handle(cx).clone();
2513 let button_text = match self.active_workflow_step.as_ref() {
2514 Some(step) => {
2515 if step.suggestions.is_none() {
2516 "Computing Changes..."
2517 } else {
2518 "Apply Changes"
2519 }
2520 }
2521 None => "Send",
2522 };
2523
2524 let (style, tooltip) = match token_state(&self.context, cx) {
2525 Some(TokenState::NoTokensLeft { .. }) => (
2526 ButtonStyle::Tinted(TintColor::Negative),
2527 Some(Tooltip::text("Token limit reached", cx)),
2528 ),
2529 Some(TokenState::HasMoreTokens {
2530 over_warn_threshold,
2531 ..
2532 }) => {
2533 let (style, tooltip) = if over_warn_threshold {
2534 (
2535 ButtonStyle::Tinted(TintColor::Warning),
2536 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
2537 )
2538 } else {
2539 (ButtonStyle::Filled, None)
2540 };
2541 (style, tooltip)
2542 }
2543 None => (ButtonStyle::Filled, None),
2544 };
2545
2546 ButtonLike::new("send_button")
2547 .style(style)
2548 .when_some(tooltip, |button, tooltip| {
2549 button.tooltip(move |_| tooltip.clone())
2550 })
2551 .layer(ElevationIndex::ModalSurface)
2552 .children(
2553 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2554 .map(|binding| binding.into_any_element()),
2555 )
2556 .child(Label::new(button_text))
2557 .on_click(move |_event, cx| {
2558 focus_handle.dispatch_action(&Assist, cx);
2559 })
2560 }
2561
2562 fn workflow_step_range_for_cursor(&self, cx: &AppContext) -> Option<Range<language::Anchor>> {
2563 let newest_cursor = self
2564 .editor
2565 .read(cx)
2566 .selections
2567 .newest_anchor()
2568 .head()
2569 .text_anchor;
2570 let context = self.context.read(cx);
2571 let buffer = context.buffer().read(cx);
2572
2573 let edit_steps = context.workflow_steps();
2574 edit_steps
2575 .binary_search_by(|step| {
2576 let step_range = step.tagged_range.clone();
2577 if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
2578 Ordering::Greater
2579 } else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
2580 Ordering::Less
2581 } else {
2582 Ordering::Equal
2583 }
2584 })
2585 .ok()
2586 .map(|index| edit_steps[index].tagged_range.clone())
2587 }
2588}
2589
2590impl EventEmitter<EditorEvent> for ContextEditor {}
2591impl EventEmitter<SearchEvent> for ContextEditor {}
2592
2593impl Render for ContextEditor {
2594 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2595 div()
2596 .key_context("ContextEditor")
2597 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
2598 .capture_action(cx.listener(ContextEditor::save))
2599 .capture_action(cx.listener(ContextEditor::copy))
2600 .capture_action(cx.listener(ContextEditor::cycle_message_role))
2601 .capture_action(cx.listener(ContextEditor::confirm_command))
2602 .on_action(cx.listener(ContextEditor::assist))
2603 .on_action(cx.listener(ContextEditor::split))
2604 .on_action(cx.listener(ContextEditor::debug_edit_steps))
2605 .size_full()
2606 .v_flex()
2607 .child(
2608 div()
2609 .flex_grow()
2610 .bg(cx.theme().colors().editor_background)
2611 .child(self.editor.clone()),
2612 )
2613 .child(
2614 h_flex()
2615 .flex_none()
2616 .relative()
2617 .when_some(self.render_notice(cx), |this, notice| {
2618 this.child(
2619 div()
2620 .absolute()
2621 .w_3_4()
2622 .min_w_24()
2623 .max_w_128()
2624 .right_4()
2625 .bottom_9()
2626 .child(notice),
2627 )
2628 })
2629 .child(
2630 h_flex()
2631 .w_full()
2632 .absolute()
2633 .right_4()
2634 .bottom_2()
2635 .justify_end()
2636 .child(self.render_send_button(cx)),
2637 ),
2638 )
2639 }
2640}
2641
2642impl FocusableView for ContextEditor {
2643 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2644 self.editor.focus_handle(cx)
2645 }
2646}
2647
2648impl Item for ContextEditor {
2649 type Event = editor::EditorEvent;
2650
2651 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
2652 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
2653 }
2654
2655 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2656 match event {
2657 EditorEvent::Edited { .. } => {
2658 f(item::ItemEvent::Edit);
2659 }
2660 EditorEvent::TitleChanged => {
2661 f(item::ItemEvent::UpdateTab);
2662 }
2663 _ => {}
2664 }
2665 }
2666
2667 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
2668 Some(self.title(cx).to_string().into())
2669 }
2670
2671 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
2672 Some(Box::new(handle.clone()))
2673 }
2674
2675 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
2676 self.editor.update(cx, |editor, cx| {
2677 Item::set_nav_history(editor, nav_history, cx)
2678 })
2679 }
2680
2681 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
2682 self.editor
2683 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
2684 }
2685
2686 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
2687 self.editor
2688 .update(cx, |editor, cx| Item::deactivated(editor, cx))
2689 }
2690}
2691
2692impl SearchableItem for ContextEditor {
2693 type Match = <Editor as SearchableItem>::Match;
2694
2695 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
2696 self.editor.update(cx, |editor, cx| {
2697 editor.clear_matches(cx);
2698 });
2699 }
2700
2701 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2702 self.editor
2703 .update(cx, |editor, cx| editor.update_matches(matches, cx));
2704 }
2705
2706 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
2707 self.editor
2708 .update(cx, |editor, cx| editor.query_suggestion(cx))
2709 }
2710
2711 fn activate_match(
2712 &mut self,
2713 index: usize,
2714 matches: &[Self::Match],
2715 cx: &mut ViewContext<Self>,
2716 ) {
2717 self.editor.update(cx, |editor, cx| {
2718 editor.activate_match(index, matches, cx);
2719 });
2720 }
2721
2722 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2723 self.editor
2724 .update(cx, |editor, cx| editor.select_matches(matches, cx));
2725 }
2726
2727 fn replace(
2728 &mut self,
2729 identifier: &Self::Match,
2730 query: &project::search::SearchQuery,
2731 cx: &mut ViewContext<Self>,
2732 ) {
2733 self.editor
2734 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
2735 }
2736
2737 fn find_matches(
2738 &mut self,
2739 query: Arc<project::search::SearchQuery>,
2740 cx: &mut ViewContext<Self>,
2741 ) -> Task<Vec<Self::Match>> {
2742 self.editor
2743 .update(cx, |editor, cx| editor.find_matches(query, cx))
2744 }
2745
2746 fn active_match_index(
2747 &mut self,
2748 matches: &[Self::Match],
2749 cx: &mut ViewContext<Self>,
2750 ) -> Option<usize> {
2751 self.editor
2752 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
2753 }
2754}
2755
2756impl FollowableItem for ContextEditor {
2757 fn remote_id(&self) -> Option<workspace::ViewId> {
2758 self.remote_id
2759 }
2760
2761 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
2762 let context = self.context.read(cx);
2763 Some(proto::view::Variant::ContextEditor(
2764 proto::view::ContextEditor {
2765 context_id: context.id().to_proto(),
2766 editor: if let Some(proto::view::Variant::Editor(proto)) =
2767 self.editor.read(cx).to_state_proto(cx)
2768 {
2769 Some(proto)
2770 } else {
2771 None
2772 },
2773 },
2774 ))
2775 }
2776
2777 fn from_state_proto(
2778 workspace: View<Workspace>,
2779 id: workspace::ViewId,
2780 state: &mut Option<proto::view::Variant>,
2781 cx: &mut WindowContext,
2782 ) -> Option<Task<Result<View<Self>>>> {
2783 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
2784 return None;
2785 };
2786 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
2787 unreachable!()
2788 };
2789
2790 let context_id = ContextId::from_proto(state.context_id);
2791 let editor_state = state.editor?;
2792
2793 let (project, panel) = workspace.update(cx, |workspace, cx| {
2794 Some((
2795 workspace.project().clone(),
2796 workspace.panel::<AssistantPanel>(cx)?,
2797 ))
2798 })?;
2799
2800 let context_editor =
2801 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
2802
2803 Some(cx.spawn(|mut cx| async move {
2804 let context_editor = context_editor.await?;
2805 context_editor
2806 .update(&mut cx, |context_editor, cx| {
2807 context_editor.remote_id = Some(id);
2808 context_editor.editor.update(cx, |editor, cx| {
2809 editor.apply_update_proto(
2810 &project,
2811 proto::update_view::Variant::Editor(proto::update_view::Editor {
2812 selections: editor_state.selections,
2813 pending_selection: editor_state.pending_selection,
2814 scroll_top_anchor: editor_state.scroll_top_anchor,
2815 scroll_x: editor_state.scroll_y,
2816 scroll_y: editor_state.scroll_y,
2817 ..Default::default()
2818 }),
2819 cx,
2820 )
2821 })
2822 })?
2823 .await?;
2824 Ok(context_editor)
2825 }))
2826 }
2827
2828 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
2829 Editor::to_follow_event(event)
2830 }
2831
2832 fn add_event_to_update_proto(
2833 &self,
2834 event: &Self::Event,
2835 update: &mut Option<proto::update_view::Variant>,
2836 cx: &WindowContext,
2837 ) -> bool {
2838 self.editor
2839 .read(cx)
2840 .add_event_to_update_proto(event, update, cx)
2841 }
2842
2843 fn apply_update_proto(
2844 &mut self,
2845 project: &Model<Project>,
2846 message: proto::update_view::Variant,
2847 cx: &mut ViewContext<Self>,
2848 ) -> Task<Result<()>> {
2849 self.editor.update(cx, |editor, cx| {
2850 editor.apply_update_proto(project, message, cx)
2851 })
2852 }
2853
2854 fn is_project_item(&self, _cx: &WindowContext) -> bool {
2855 true
2856 }
2857
2858 fn set_leader_peer_id(
2859 &mut self,
2860 leader_peer_id: Option<proto::PeerId>,
2861 cx: &mut ViewContext<Self>,
2862 ) {
2863 self.editor.update(cx, |editor, cx| {
2864 editor.set_leader_peer_id(leader_peer_id, cx)
2865 })
2866 }
2867
2868 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
2869 if existing.context.read(cx).id() == self.context.read(cx).id() {
2870 Some(item::Dedup::KeepExisting)
2871 } else {
2872 None
2873 }
2874 }
2875}
2876
2877pub struct ContextEditorToolbarItem {
2878 fs: Arc<dyn Fs>,
2879 workspace: WeakView<Workspace>,
2880 active_context_editor: Option<WeakView<ContextEditor>>,
2881 model_summary_editor: View<Editor>,
2882}
2883
2884impl ContextEditorToolbarItem {
2885 pub fn new(
2886 workspace: &Workspace,
2887 _model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2888 model_summary_editor: View<Editor>,
2889 ) -> Self {
2890 Self {
2891 fs: workspace.app_state().fs.clone(),
2892 workspace: workspace.weak_handle(),
2893 active_context_editor: None,
2894 model_summary_editor,
2895 }
2896 }
2897
2898 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
2899 let commands = SlashCommandRegistry::global(cx);
2900 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
2901 Some(
2902 workspace
2903 .read(cx)
2904 .active_item_as::<Editor>(cx)?
2905 .focus_handle(cx),
2906 )
2907 });
2908 let active_context_editor = self.active_context_editor.clone();
2909
2910 PopoverMenu::new("inject-context-menu")
2911 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
2912 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
2913 }))
2914 .menu(move |cx| {
2915 let active_context_editor = active_context_editor.clone()?;
2916 ContextMenu::build(cx, |mut menu, _cx| {
2917 for command_name in commands.featured_command_names() {
2918 if let Some(command) = commands.command(&command_name) {
2919 let menu_text = SharedString::from(Arc::from(command.menu_text()));
2920 menu = menu.custom_entry(
2921 {
2922 let command_name = command_name.clone();
2923 move |_cx| {
2924 h_flex()
2925 .gap_4()
2926 .w_full()
2927 .justify_between()
2928 .child(Label::new(menu_text.clone()))
2929 .child(
2930 Label::new(format!("/{command_name}"))
2931 .color(Color::Muted),
2932 )
2933 .into_any()
2934 }
2935 },
2936 {
2937 let active_context_editor = active_context_editor.clone();
2938 move |cx| {
2939 active_context_editor
2940 .update(cx, |context_editor, cx| {
2941 context_editor.insert_command(&command_name, cx)
2942 })
2943 .ok();
2944 }
2945 },
2946 )
2947 }
2948 }
2949
2950 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
2951 menu = menu
2952 .context(active_editor_focus_handle)
2953 .action("Quote Selection", Box::new(QuoteSelection));
2954 }
2955
2956 menu
2957 })
2958 .into()
2959 })
2960 }
2961
2962 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
2963 let context = &self
2964 .active_context_editor
2965 .as_ref()?
2966 .upgrade()?
2967 .read(cx)
2968 .context;
2969 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
2970 TokenState::NoTokensLeft {
2971 max_token_count,
2972 token_count,
2973 } => (Color::Error, token_count, max_token_count),
2974 TokenState::HasMoreTokens {
2975 max_token_count,
2976 token_count,
2977 over_warn_threshold,
2978 } => {
2979 let color = if over_warn_threshold {
2980 Color::Warning
2981 } else {
2982 Color::Muted
2983 };
2984 (color, token_count, max_token_count)
2985 }
2986 };
2987 Some(
2988 h_flex()
2989 .gap_0p5()
2990 .child(
2991 Label::new(humanize_token_count(token_count))
2992 .size(LabelSize::Small)
2993 .color(token_count_color),
2994 )
2995 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2996 .child(
2997 Label::new(humanize_token_count(max_token_count))
2998 .size(LabelSize::Small)
2999 .color(Color::Muted),
3000 ),
3001 )
3002 }
3003}
3004
3005impl Render for ContextEditorToolbarItem {
3006 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3007 let left_side = h_flex()
3008 .gap_2()
3009 .flex_1()
3010 .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
3011 .when(self.active_context_editor.is_some(), |left_side| {
3012 left_side
3013 .child(
3014 IconButton::new("regenerate-context", IconName::ArrowCircle)
3015 .tooltip(|cx| Tooltip::text("Regenerate Summary", cx))
3016 .on_click(cx.listener(move |_, _, cx| {
3017 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
3018 })),
3019 )
3020 .child(self.model_summary_editor.clone())
3021 });
3022
3023 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
3024 let active_model = LanguageModelRegistry::read_global(cx).active_model();
3025
3026 let right_side = h_flex()
3027 .gap_2()
3028 .child(ModelSelector::new(
3029 self.fs.clone(),
3030 ButtonLike::new("active-model")
3031 .style(ButtonStyle::Subtle)
3032 .child(
3033 h_flex()
3034 .w_full()
3035 .gap_0p5()
3036 .child(
3037 div()
3038 .overflow_x_hidden()
3039 .flex_grow()
3040 .whitespace_nowrap()
3041 .child(match (active_provider, active_model) {
3042 (Some(provider), Some(model)) => h_flex()
3043 .gap_1()
3044 .child(
3045 Icon::new(provider.icon())
3046 .color(Color::Muted)
3047 .size(IconSize::XSmall),
3048 )
3049 .child(
3050 Label::new(model.name().0)
3051 .size(LabelSize::Small)
3052 .color(Color::Muted),
3053 )
3054 .into_any_element(),
3055 _ => Label::new("No model selected")
3056 .size(LabelSize::Small)
3057 .color(Color::Muted)
3058 .into_any_element(),
3059 }),
3060 )
3061 .child(
3062 Icon::new(IconName::ChevronDown)
3063 .color(Color::Muted)
3064 .size(IconSize::XSmall),
3065 ),
3066 )
3067 .tooltip(move |cx| {
3068 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
3069 }),
3070 ))
3071 .children(self.render_remaining_tokens(cx))
3072 .child(self.render_inject_context_menu(cx));
3073
3074 h_flex()
3075 .size_full()
3076 .justify_between()
3077 .child(left_side)
3078 .child(right_side)
3079 }
3080}
3081
3082impl ToolbarItemView for ContextEditorToolbarItem {
3083 fn set_active_pane_item(
3084 &mut self,
3085 active_pane_item: Option<&dyn ItemHandle>,
3086 cx: &mut ViewContext<Self>,
3087 ) -> ToolbarItemLocation {
3088 self.active_context_editor = active_pane_item
3089 .and_then(|item| item.act_as::<ContextEditor>(cx))
3090 .map(|editor| editor.downgrade());
3091 cx.notify();
3092 if self.active_context_editor.is_none() {
3093 ToolbarItemLocation::Hidden
3094 } else {
3095 ToolbarItemLocation::PrimaryRight
3096 }
3097 }
3098
3099 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
3100 cx.notify();
3101 }
3102}
3103
3104impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
3105
3106enum ContextEditorToolbarItemEvent {
3107 RegenerateSummary,
3108}
3109impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
3110
3111pub struct ContextHistory {
3112 picker: View<Picker<SavedContextPickerDelegate>>,
3113 _subscriptions: Vec<Subscription>,
3114 assistant_panel: WeakView<AssistantPanel>,
3115}
3116
3117impl ContextHistory {
3118 fn new(
3119 project: Model<Project>,
3120 context_store: Model<ContextStore>,
3121 assistant_panel: WeakView<AssistantPanel>,
3122 cx: &mut ViewContext<Self>,
3123 ) -> Self {
3124 let picker = cx.new_view(|cx| {
3125 Picker::uniform_list(
3126 SavedContextPickerDelegate::new(project, context_store.clone()),
3127 cx,
3128 )
3129 .modal(false)
3130 .max_height(None)
3131 });
3132
3133 let _subscriptions = vec![
3134 cx.observe(&context_store, |this, _, cx| {
3135 this.picker.update(cx, |picker, cx| picker.refresh(cx));
3136 }),
3137 cx.subscribe(&picker, Self::handle_picker_event),
3138 ];
3139
3140 Self {
3141 picker,
3142 _subscriptions,
3143 assistant_panel,
3144 }
3145 }
3146
3147 fn handle_picker_event(
3148 &mut self,
3149 _: View<Picker<SavedContextPickerDelegate>>,
3150 event: &SavedContextPickerEvent,
3151 cx: &mut ViewContext<Self>,
3152 ) {
3153 let SavedContextPickerEvent::Confirmed(context) = event;
3154 self.assistant_panel
3155 .update(cx, |assistant_panel, cx| match context {
3156 ContextMetadata::Remote(metadata) => {
3157 assistant_panel
3158 .open_remote_context(metadata.id.clone(), cx)
3159 .detach_and_log_err(cx);
3160 }
3161 ContextMetadata::Saved(metadata) => {
3162 assistant_panel
3163 .open_saved_context(metadata.path.clone(), cx)
3164 .detach_and_log_err(cx);
3165 }
3166 })
3167 .ok();
3168 }
3169}
3170
3171impl Render for ContextHistory {
3172 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
3173 div().size_full().child(self.picker.clone())
3174 }
3175}
3176
3177impl FocusableView for ContextHistory {
3178 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3179 self.picker.focus_handle(cx)
3180 }
3181}
3182
3183impl EventEmitter<()> for ContextHistory {}
3184
3185impl Item for ContextHistory {
3186 type Event = ();
3187
3188 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3189 Some("History".into())
3190 }
3191}
3192
3193pub struct ConfigurationView {
3194 focus_handle: FocusHandle,
3195 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
3196 _registry_subscription: Subscription,
3197}
3198
3199impl ConfigurationView {
3200 fn new(cx: &mut ViewContext<Self>) -> Self {
3201 let focus_handle = cx.focus_handle();
3202
3203 let registry_subscription = cx.subscribe(
3204 &LanguageModelRegistry::global(cx),
3205 |this, _, event: &language_model::Event, cx| match event {
3206 language_model::Event::AddedProvider(provider_id) => {
3207 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
3208 if let Some(provider) = provider {
3209 this.add_configuration_view(&provider, cx);
3210 }
3211 }
3212 language_model::Event::RemovedProvider(provider_id) => {
3213 this.remove_configuration_view(provider_id);
3214 }
3215 _ => {}
3216 },
3217 );
3218
3219 let mut this = Self {
3220 focus_handle,
3221 configuration_views: HashMap::default(),
3222 _registry_subscription: registry_subscription,
3223 };
3224 this.build_configuration_views(cx);
3225 this
3226 }
3227
3228 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
3229 let providers = LanguageModelRegistry::read_global(cx).providers();
3230 for provider in providers {
3231 self.add_configuration_view(&provider, cx);
3232 }
3233 }
3234
3235 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
3236 self.configuration_views.remove(provider_id);
3237 }
3238
3239 fn add_configuration_view(
3240 &mut self,
3241 provider: &Arc<dyn LanguageModelProvider>,
3242 cx: &mut ViewContext<Self>,
3243 ) {
3244 let configuration_view = provider.configuration_view(cx);
3245 self.configuration_views
3246 .insert(provider.id(), configuration_view);
3247 }
3248
3249 fn render_provider_view(
3250 &mut self,
3251 provider: &Arc<dyn LanguageModelProvider>,
3252 cx: &mut ViewContext<Self>,
3253 ) -> Div {
3254 let provider_name = provider.name().0.clone();
3255 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
3256
3257 v_flex()
3258 .gap_4()
3259 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Medium))
3260 .child(
3261 div()
3262 .p(Spacing::Large.rems(cx))
3263 .bg(cx.theme().colors().title_bar_background)
3264 .border_1()
3265 .border_color(cx.theme().colors().border_variant)
3266 .rounded_md()
3267 .when(configuration_view.is_none(), |this| {
3268 this.child(div().child(Label::new(format!(
3269 "No configuration view for {}",
3270 provider_name
3271 ))))
3272 })
3273 .when_some(configuration_view, |this, configuration_view| {
3274 this.child(configuration_view)
3275 }),
3276 )
3277 .when(provider.is_authenticated(cx), move |this| {
3278 this.child(
3279 h_flex().justify_end().child(
3280 Button::new(
3281 "new-context",
3282 format!("Open new context using {}", provider_name),
3283 )
3284 .icon_position(IconPosition::Start)
3285 .icon(IconName::Plus)
3286 .style(ButtonStyle::Filled)
3287 .layer(ElevationIndex::ModalSurface)
3288 .on_click(cx.listener({
3289 let provider = provider.clone();
3290 move |_, _, cx| {
3291 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
3292 provider.clone(),
3293 ))
3294 }
3295 })),
3296 ),
3297 )
3298 })
3299 }
3300}
3301
3302impl Render for ConfigurationView {
3303 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3304 let providers = LanguageModelRegistry::read_global(cx).providers();
3305 let provider_views = providers
3306 .into_iter()
3307 .map(|provider| self.render_provider_view(&provider, cx))
3308 .collect::<Vec<_>>();
3309
3310 v_flex()
3311 .id("assistant-configuration-view")
3312 .track_focus(&self.focus_handle)
3313 .w_full()
3314 .min_h_full()
3315 .p(Spacing::XXLarge.rems(cx))
3316 .overflow_y_scroll()
3317 .gap_6()
3318 .child(
3319 v_flex()
3320 .gap_2()
3321 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium)),
3322 )
3323 .child(
3324 v_flex()
3325 .gap_2()
3326 .child(
3327 Label::new(
3328 "At least one provider must be configured to use the assistant.",
3329 )
3330 .color(Color::Muted),
3331 )
3332 .child(v_flex().mt_2().gap_4().children(provider_views)),
3333 )
3334 }
3335}
3336
3337pub enum ConfigurationViewEvent {
3338 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
3339}
3340
3341impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
3342
3343impl FocusableView for ConfigurationView {
3344 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
3345 self.focus_handle.clone()
3346 }
3347}
3348
3349impl Item for ConfigurationView {
3350 type Event = ConfigurationViewEvent;
3351
3352 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3353 Some("Configuration".into())
3354 }
3355}
3356
3357type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3358
3359fn render_slash_command_output_toggle(
3360 row: MultiBufferRow,
3361 is_folded: bool,
3362 fold: ToggleFold,
3363 _cx: &mut WindowContext,
3364) -> AnyElement {
3365 Disclosure::new(
3366 ("slash-command-output-fold-indicator", row.0 as u64),
3367 !is_folded,
3368 )
3369 .selected(is_folded)
3370 .on_click(move |_e, cx| fold(!is_folded, cx))
3371 .into_any_element()
3372}
3373
3374fn render_pending_slash_command_gutter_decoration(
3375 row: MultiBufferRow,
3376 status: &PendingSlashCommandStatus,
3377 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3378) -> AnyElement {
3379 let mut icon = IconButton::new(
3380 ("slash-command-gutter-decoration", row.0),
3381 ui::IconName::TriangleRight,
3382 )
3383 .on_click(move |_e, cx| confirm_command(cx))
3384 .icon_size(ui::IconSize::Small)
3385 .size(ui::ButtonSize::None);
3386
3387 match status {
3388 PendingSlashCommandStatus::Idle => {
3389 icon = icon.icon_color(Color::Muted);
3390 }
3391 PendingSlashCommandStatus::Running { .. } => {
3392 icon = icon.selected(true);
3393 }
3394 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
3395 }
3396
3397 icon.into_any_element()
3398}
3399
3400fn render_docs_slash_command_trailer(
3401 row: MultiBufferRow,
3402 command: PendingSlashCommand,
3403 cx: &mut WindowContext,
3404) -> AnyElement {
3405 let Some(argument) = command.argument else {
3406 return Empty.into_any();
3407 };
3408
3409 let args = DocsSlashCommandArgs::parse(&argument);
3410
3411 let Some(store) = args
3412 .provider()
3413 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
3414 else {
3415 return Empty.into_any();
3416 };
3417
3418 let Some(package) = args.package() else {
3419 return Empty.into_any();
3420 };
3421
3422 let mut children = Vec::new();
3423
3424 if store.is_indexing(&package) {
3425 children.push(
3426 div()
3427 .id(("crates-being-indexed", row.0))
3428 .child(Icon::new(IconName::ArrowCircle).with_animation(
3429 "arrow-circle",
3430 Animation::new(Duration::from_secs(4)).repeat(),
3431 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3432 ))
3433 .tooltip({
3434 let package = package.clone();
3435 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
3436 })
3437 .into_any_element(),
3438 );
3439 }
3440
3441 if let Some(latest_error) = store.latest_error_for_package(&package) {
3442 children.push(
3443 div()
3444 .id(("latest-error", row.0))
3445 .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
3446 .tooltip(move |cx| Tooltip::text(format!("failed to index: {latest_error}"), cx))
3447 .into_any_element(),
3448 )
3449 }
3450
3451 let is_indexing = store.is_indexing(&package);
3452 let latest_error = store.latest_error_for_package(&package);
3453
3454 if !is_indexing && latest_error.is_none() {
3455 return Empty.into_any();
3456 }
3457
3458 h_flex().gap_2().children(children).into_any_element()
3459}
3460
3461fn make_lsp_adapter_delegate(
3462 project: &Model<Project>,
3463 cx: &mut AppContext,
3464) -> Result<Arc<dyn LspAdapterDelegate>> {
3465 project.update(cx, |project, cx| {
3466 // TODO: Find the right worktree.
3467 let worktree = project
3468 .worktrees(cx)
3469 .next()
3470 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3471 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3472 })
3473}
3474
3475fn slash_command_error_block_renderer(message: String) -> RenderBlock {
3476 Box::new(move |_| {
3477 div()
3478 .pl_6()
3479 .child(
3480 Label::new(format!("error: {}", message))
3481 .single_line()
3482 .color(Color::Error),
3483 )
3484 .into_any()
3485 })
3486}
3487
3488enum TokenState {
3489 NoTokensLeft {
3490 max_token_count: usize,
3491 token_count: usize,
3492 },
3493 HasMoreTokens {
3494 max_token_count: usize,
3495 token_count: usize,
3496 over_warn_threshold: bool,
3497 },
3498}
3499
3500fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
3501 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3502
3503 let model = LanguageModelRegistry::read_global(cx).active_model()?;
3504 let token_count = context.read(cx).token_count()?;
3505 let max_token_count = model.max_token_count();
3506
3507 let remaining_tokens = max_token_count as isize - token_count as isize;
3508 let token_state = if remaining_tokens <= 0 {
3509 TokenState::NoTokensLeft {
3510 max_token_count,
3511 token_count,
3512 }
3513 } else {
3514 let over_warn_threshold =
3515 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3516 TokenState::HasMoreTokens {
3517 max_token_count,
3518 token_count,
3519 over_warn_threshold,
3520 }
3521 };
3522 Some(token_state)
3523}
3524
3525enum ConfigurationError {
3526 NoProvider,
3527 ProviderNotAuthenticated,
3528}
3529
3530fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
3531 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3532 let is_authenticated = provider
3533 .as_ref()
3534 .map_or(false, |provider| provider.is_authenticated(cx));
3535
3536 if provider.is_some() && is_authenticated {
3537 return None;
3538 }
3539
3540 if provider.is_none() {
3541 return Some(ConfigurationError::NoProvider);
3542 }
3543
3544 if !is_authenticated {
3545 return Some(ConfigurationError::ProviderNotAuthenticated);
3546 }
3547
3548 None
3549}