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