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