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