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