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