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