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