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