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