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