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