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