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 editor = editor.read(cx);
1689 let range = editor.selections.newest::<usize>(cx).range();
1690 let buffer = editor.buffer().read(cx).snapshot(cx);
1691 let start_language = buffer.language_at(range.start);
1692 let end_language = buffer.language_at(range.end);
1693 let language_name = if start_language == end_language {
1694 start_language.map(|language| language.code_fence_block_name())
1695 } else {
1696 None
1697 };
1698 let language_name = language_name.as_deref().unwrap_or("");
1699
1700 let selected_text = buffer.text_for_range(range).collect::<String>();
1701 let text = if selected_text.is_empty() {
1702 None
1703 } else {
1704 Some(if language_name == "markdown" {
1705 selected_text
1706 .lines()
1707 .map(|line| format!("> {}", line))
1708 .collect::<Vec<_>>()
1709 .join("\n")
1710 } else {
1711 format!("```{language_name}\n{selected_text}\n```")
1712 })
1713 };
1714
1715 // Activate the panel
1716 if !panel.focus_handle(cx).contains_focused(cx) {
1717 workspace.toggle_panel_focus::<AssistantPanel>(cx);
1718 }
1719
1720 if let Some(text) = text {
1721 panel.update(cx, |_, cx| {
1722 // Wait to create a new context until the workspace is no longer
1723 // being updated.
1724 cx.defer(move |panel, cx| {
1725 if let Some(context) = panel
1726 .active_context_editor(cx)
1727 .or_else(|| panel.new_context(cx))
1728 {
1729 context.update(cx, |context, cx| {
1730 context
1731 .editor
1732 .update(cx, |editor, cx| editor.insert(&text, cx))
1733 });
1734 };
1735 });
1736 });
1737 }
1738 }
1739
1740 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
1741 let editor = self.editor.read(cx);
1742 let context = self.context.read(cx);
1743 if editor.selections.count() == 1 {
1744 let selection = editor.selections.newest::<usize>(cx);
1745 let mut copied_text = String::new();
1746 let mut spanned_messages = 0;
1747 for message in context.messages(cx) {
1748 if message.offset_range.start >= selection.range().end {
1749 break;
1750 } else if message.offset_range.end >= selection.range().start {
1751 let range = cmp::max(message.offset_range.start, selection.range().start)
1752 ..cmp::min(message.offset_range.end, selection.range().end);
1753 if !range.is_empty() {
1754 spanned_messages += 1;
1755 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
1756 for chunk in context.buffer().read(cx).text_for_range(range) {
1757 copied_text.push_str(chunk);
1758 }
1759 copied_text.push('\n');
1760 }
1761 }
1762 }
1763
1764 if spanned_messages > 1 {
1765 cx.write_to_clipboard(ClipboardItem::new(copied_text));
1766 return;
1767 }
1768 }
1769
1770 cx.propagate();
1771 }
1772
1773 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
1774 self.context.update(cx, |context, cx| {
1775 let selections = self.editor.read(cx).selections.disjoint_anchors();
1776 for selection in selections.as_ref() {
1777 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1778 let range = selection
1779 .map(|endpoint| endpoint.to_offset(&buffer))
1780 .range();
1781 context.split_message(range, cx);
1782 }
1783 });
1784 }
1785
1786 fn apply_edit(&mut self, _: &ApplyEdit, cx: &mut ViewContext<Self>) {
1787 let Some(workspace) = self.workspace.upgrade() else {
1788 return;
1789 };
1790 let project = workspace.read(cx).project().clone();
1791
1792 struct Edit {
1793 old_text: String,
1794 new_text: String,
1795 }
1796
1797 let context = self.context.read(cx);
1798 let context_buffer = context.buffer().read(cx);
1799 let context_buffer_snapshot = context_buffer.snapshot();
1800
1801 let selections = self.editor.read(cx).selections.disjoint_anchors();
1802 let mut selections = selections.iter().peekable();
1803 let selected_suggestions = context
1804 .edit_suggestions()
1805 .iter()
1806 .filter(|suggestion| {
1807 while let Some(selection) = selections.peek() {
1808 if selection
1809 .end
1810 .text_anchor
1811 .cmp(&suggestion.source_range.start, context_buffer)
1812 .is_lt()
1813 {
1814 selections.next();
1815 continue;
1816 }
1817 if selection
1818 .start
1819 .text_anchor
1820 .cmp(&suggestion.source_range.end, context_buffer)
1821 .is_gt()
1822 {
1823 break;
1824 }
1825 return true;
1826 }
1827 false
1828 })
1829 .cloned()
1830 .collect::<Vec<_>>();
1831
1832 let mut opened_buffers: HashMap<PathBuf, Task<Result<Model<Buffer>>>> = HashMap::default();
1833 project.update(cx, |project, cx| {
1834 for suggestion in &selected_suggestions {
1835 opened_buffers
1836 .entry(suggestion.full_path.clone())
1837 .or_insert_with(|| {
1838 project.open_buffer_for_full_path(&suggestion.full_path, cx)
1839 });
1840 }
1841 });
1842
1843 cx.spawn(|this, mut cx| async move {
1844 let mut buffers_by_full_path = HashMap::default();
1845 for (full_path, buffer) in opened_buffers {
1846 if let Some(buffer) = buffer.await.log_err() {
1847 buffers_by_full_path.insert(full_path, buffer);
1848 }
1849 }
1850
1851 let mut suggestions_by_buffer = HashMap::default();
1852 cx.update(|cx| {
1853 for suggestion in selected_suggestions {
1854 if let Some(buffer) = buffers_by_full_path.get(&suggestion.full_path) {
1855 let (_, edits) = suggestions_by_buffer
1856 .entry(buffer.clone())
1857 .or_insert_with(|| (buffer.read(cx).snapshot(), Vec::new()));
1858
1859 let mut lines = context_buffer_snapshot
1860 .as_rope()
1861 .chunks_in_range(
1862 suggestion.source_range.to_offset(&context_buffer_snapshot),
1863 )
1864 .lines();
1865 if let Some(suggestion) = parse_next_edit_suggestion(&mut lines) {
1866 let old_text = context_buffer_snapshot
1867 .text_for_range(suggestion.old_text_range)
1868 .collect();
1869 let new_text = context_buffer_snapshot
1870 .text_for_range(suggestion.new_text_range)
1871 .collect();
1872 edits.push(Edit { old_text, new_text });
1873 }
1874 }
1875 }
1876 })?;
1877
1878 let edits_by_buffer = cx
1879 .background_executor()
1880 .spawn(async move {
1881 let mut result = HashMap::default();
1882 for (buffer, (snapshot, suggestions)) in suggestions_by_buffer {
1883 let edits =
1884 result
1885 .entry(buffer)
1886 .or_insert(Vec::<(Range<language::Anchor>, _)>::new());
1887 for suggestion in suggestions {
1888 if let Some(range) =
1889 fuzzy_search_lines(snapshot.as_rope(), &suggestion.old_text)
1890 {
1891 let edit_start = snapshot.anchor_after(range.start);
1892 let edit_end = snapshot.anchor_before(range.end);
1893 if let Err(ix) = edits.binary_search_by(|(range, _)| {
1894 range.start.cmp(&edit_start, &snapshot)
1895 }) {
1896 edits.insert(
1897 ix,
1898 (edit_start..edit_end, suggestion.new_text.clone()),
1899 );
1900 }
1901 } else {
1902 log::info!(
1903 "assistant edit did not match any text in buffer {:?}",
1904 &suggestion.old_text
1905 );
1906 }
1907 }
1908 }
1909 result
1910 })
1911 .await;
1912
1913 let mut project_transaction = ProjectTransaction::default();
1914 let (editor, workspace, title) = this.update(&mut cx, |this, cx| {
1915 for (buffer_handle, edits) in edits_by_buffer {
1916 buffer_handle.update(cx, |buffer, cx| {
1917 buffer.start_transaction();
1918 buffer.edit(
1919 edits,
1920 Some(AutoindentMode::Block {
1921 original_indent_columns: Vec::new(),
1922 }),
1923 cx,
1924 );
1925 buffer.end_transaction(cx);
1926 if let Some(transaction) = buffer.finalize_last_transaction() {
1927 project_transaction
1928 .0
1929 .insert(buffer_handle.clone(), transaction.clone());
1930 }
1931 });
1932 }
1933
1934 (
1935 this.editor.downgrade(),
1936 this.workspace.clone(),
1937 this.title(cx),
1938 )
1939 })?;
1940
1941 Editor::open_project_transaction(
1942 &editor,
1943 workspace,
1944 project_transaction,
1945 format!("Edits from {}", title),
1946 cx,
1947 )
1948 .await
1949 })
1950 .detach_and_log_err(cx);
1951 }
1952
1953 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
1954 self.context
1955 .update(cx, |context, cx| context.save(None, self.fs.clone(), cx));
1956 }
1957
1958 fn title(&self, cx: &AppContext) -> String {
1959 self.context
1960 .read(cx)
1961 .summary()
1962 .map(|summary| summary.text.clone())
1963 .unwrap_or_else(|| "New Context".into())
1964 }
1965
1966 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1967 let focus_handle = self.focus_handle(cx).clone();
1968 ButtonLike::new("send_button")
1969 .style(ButtonStyle::Filled)
1970 .layer(ElevationIndex::ModalSurface)
1971 .children(
1972 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
1973 .map(|binding| binding.into_any_element()),
1974 )
1975 .child(Label::new("Send"))
1976 .on_click(move |_event, cx| {
1977 focus_handle.dispatch_action(&Assist, cx);
1978 })
1979 }
1980}
1981
1982impl EventEmitter<EditorEvent> for ContextEditor {}
1983impl EventEmitter<SearchEvent> for ContextEditor {}
1984
1985impl Render for ContextEditor {
1986 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1987 div()
1988 .key_context("ContextEditor")
1989 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
1990 .capture_action(cx.listener(ContextEditor::save))
1991 .capture_action(cx.listener(ContextEditor::copy))
1992 .capture_action(cx.listener(ContextEditor::cycle_message_role))
1993 .capture_action(cx.listener(ContextEditor::confirm_command))
1994 .on_action(cx.listener(ContextEditor::assist))
1995 .on_action(cx.listener(ContextEditor::split))
1996 .on_action(cx.listener(ContextEditor::apply_edit))
1997 .size_full()
1998 .v_flex()
1999 .child(
2000 div()
2001 .flex_grow()
2002 .bg(cx.theme().colors().editor_background)
2003 .child(self.editor.clone())
2004 .child(
2005 h_flex()
2006 .w_full()
2007 .absolute()
2008 .bottom_0()
2009 .p_4()
2010 .justify_end()
2011 .child(self.render_send_button(cx)),
2012 ),
2013 )
2014 }
2015}
2016
2017impl FocusableView for ContextEditor {
2018 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2019 self.editor.focus_handle(cx)
2020 }
2021}
2022
2023impl Item for ContextEditor {
2024 type Event = editor::EditorEvent;
2025
2026 fn tab_content(&self, params: item::TabContentParams, cx: &WindowContext) -> AnyElement {
2027 let color = if params.selected {
2028 Color::Default
2029 } else {
2030 Color::Muted
2031 };
2032 Label::new(util::truncate_and_trailoff(
2033 &self.title(cx),
2034 Self::MAX_TAB_TITLE_LEN,
2035 ))
2036 .color(color)
2037 .into_any_element()
2038 }
2039
2040 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2041 match event {
2042 EditorEvent::Edited { .. } => {
2043 f(item::ItemEvent::Edit);
2044 f(item::ItemEvent::UpdateBreadcrumbs);
2045 }
2046 EditorEvent::TitleChanged => {
2047 f(item::ItemEvent::UpdateTab);
2048 }
2049 _ => {}
2050 }
2051 }
2052
2053 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
2054 Some(self.title(cx).into())
2055 }
2056
2057 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
2058 Some(Box::new(handle.clone()))
2059 }
2060
2061 fn breadcrumbs(
2062 &self,
2063 theme: &theme::Theme,
2064 cx: &AppContext,
2065 ) -> Option<Vec<item::BreadcrumbText>> {
2066 let editor = self.editor.read(cx);
2067 let cursor = editor.selections.newest_anchor().head();
2068 let multibuffer = &editor.buffer().read(cx);
2069 let (_, symbols) = multibuffer.symbols_containing(cursor, Some(&theme.syntax()), cx)?;
2070
2071 let settings = ThemeSettings::get_global(cx);
2072
2073 let mut breadcrumbs = Vec::new();
2074
2075 let title = self.title(cx);
2076 if title.chars().count() > Self::MAX_TAB_TITLE_LEN {
2077 breadcrumbs.push(BreadcrumbText {
2078 text: title,
2079 highlights: None,
2080 font: Some(settings.buffer_font.clone()),
2081 });
2082 }
2083
2084 breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
2085 text: symbol.text,
2086 highlights: Some(symbol.highlight_ranges),
2087 font: Some(settings.buffer_font.clone()),
2088 }));
2089 Some(breadcrumbs)
2090 }
2091
2092 fn breadcrumb_location(&self) -> ToolbarItemLocation {
2093 ToolbarItemLocation::PrimaryLeft
2094 }
2095
2096 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
2097 self.editor.update(cx, |editor, cx| {
2098 Item::set_nav_history(editor, nav_history, cx)
2099 })
2100 }
2101
2102 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
2103 self.editor
2104 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
2105 }
2106
2107 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
2108 self.editor
2109 .update(cx, |editor, cx| Item::deactivated(editor, cx))
2110 }
2111}
2112
2113impl SearchableItem for ContextEditor {
2114 type Match = <Editor as SearchableItem>::Match;
2115
2116 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
2117 self.editor.update(cx, |editor, cx| {
2118 editor.clear_matches(cx);
2119 });
2120 }
2121
2122 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2123 self.editor
2124 .update(cx, |editor, cx| editor.update_matches(matches, cx));
2125 }
2126
2127 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
2128 self.editor
2129 .update(cx, |editor, cx| editor.query_suggestion(cx))
2130 }
2131
2132 fn activate_match(
2133 &mut self,
2134 index: usize,
2135 matches: &[Self::Match],
2136 cx: &mut ViewContext<Self>,
2137 ) {
2138 self.editor.update(cx, |editor, cx| {
2139 editor.activate_match(index, matches, cx);
2140 });
2141 }
2142
2143 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2144 self.editor
2145 .update(cx, |editor, cx| editor.select_matches(matches, cx));
2146 }
2147
2148 fn replace(
2149 &mut self,
2150 identifier: &Self::Match,
2151 query: &project::search::SearchQuery,
2152 cx: &mut ViewContext<Self>,
2153 ) {
2154 self.editor
2155 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
2156 }
2157
2158 fn find_matches(
2159 &mut self,
2160 query: Arc<project::search::SearchQuery>,
2161 cx: &mut ViewContext<Self>,
2162 ) -> Task<Vec<Self::Match>> {
2163 self.editor
2164 .update(cx, |editor, cx| editor.find_matches(query, cx))
2165 }
2166
2167 fn active_match_index(
2168 &mut self,
2169 matches: &[Self::Match],
2170 cx: &mut ViewContext<Self>,
2171 ) -> Option<usize> {
2172 self.editor
2173 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
2174 }
2175}
2176
2177impl FollowableItem for ContextEditor {
2178 fn remote_id(&self) -> Option<workspace::ViewId> {
2179 self.remote_id
2180 }
2181
2182 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
2183 let context = self.context.read(cx);
2184 Some(proto::view::Variant::ContextEditor(
2185 proto::view::ContextEditor {
2186 context_id: context.id().to_proto(),
2187 editor: if let Some(proto::view::Variant::Editor(proto)) =
2188 self.editor.read(cx).to_state_proto(cx)
2189 {
2190 Some(proto)
2191 } else {
2192 None
2193 },
2194 },
2195 ))
2196 }
2197
2198 fn from_state_proto(
2199 workspace: View<Workspace>,
2200 id: workspace::ViewId,
2201 state: &mut Option<proto::view::Variant>,
2202 cx: &mut WindowContext,
2203 ) -> Option<Task<Result<View<Self>>>> {
2204 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
2205 return None;
2206 };
2207 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
2208 unreachable!()
2209 };
2210
2211 let context_id = ContextId::from_proto(state.context_id);
2212 let editor_state = state.editor?;
2213
2214 let (project, panel) = workspace.update(cx, |workspace, cx| {
2215 Some((
2216 workspace.project().clone(),
2217 workspace.panel::<AssistantPanel>(cx)?,
2218 ))
2219 })?;
2220
2221 let context_editor =
2222 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
2223
2224 Some(cx.spawn(|mut cx| async move {
2225 let context_editor = context_editor.await?;
2226 context_editor
2227 .update(&mut cx, |context_editor, cx| {
2228 context_editor.remote_id = Some(id);
2229 context_editor.editor.update(cx, |editor, cx| {
2230 editor.apply_update_proto(
2231 &project,
2232 proto::update_view::Variant::Editor(proto::update_view::Editor {
2233 selections: editor_state.selections,
2234 pending_selection: editor_state.pending_selection,
2235 scroll_top_anchor: editor_state.scroll_top_anchor,
2236 scroll_x: editor_state.scroll_y,
2237 scroll_y: editor_state.scroll_y,
2238 ..Default::default()
2239 }),
2240 cx,
2241 )
2242 })
2243 })?
2244 .await?;
2245 Ok(context_editor)
2246 }))
2247 }
2248
2249 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
2250 Editor::to_follow_event(event)
2251 }
2252
2253 fn add_event_to_update_proto(
2254 &self,
2255 event: &Self::Event,
2256 update: &mut Option<proto::update_view::Variant>,
2257 cx: &WindowContext,
2258 ) -> bool {
2259 self.editor
2260 .read(cx)
2261 .add_event_to_update_proto(event, update, cx)
2262 }
2263
2264 fn apply_update_proto(
2265 &mut self,
2266 project: &Model<Project>,
2267 message: proto::update_view::Variant,
2268 cx: &mut ViewContext<Self>,
2269 ) -> Task<Result<()>> {
2270 self.editor.update(cx, |editor, cx| {
2271 editor.apply_update_proto(project, message, cx)
2272 })
2273 }
2274
2275 fn is_project_item(&self, _cx: &WindowContext) -> bool {
2276 true
2277 }
2278
2279 fn set_leader_peer_id(
2280 &mut self,
2281 leader_peer_id: Option<proto::PeerId>,
2282 cx: &mut ViewContext<Self>,
2283 ) {
2284 self.editor.update(cx, |editor, cx| {
2285 editor.set_leader_peer_id(leader_peer_id, cx)
2286 })
2287 }
2288
2289 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
2290 if existing.context.read(cx).id() == self.context.read(cx).id() {
2291 Some(item::Dedup::KeepExisting)
2292 } else {
2293 None
2294 }
2295 }
2296}
2297
2298pub struct ContextEditorToolbarItem {
2299 fs: Arc<dyn Fs>,
2300 workspace: WeakView<Workspace>,
2301 active_context_editor: Option<WeakView<ContextEditor>>,
2302 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2303}
2304
2305impl ContextEditorToolbarItem {
2306 pub fn new(
2307 workspace: &Workspace,
2308 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2309 ) -> Self {
2310 Self {
2311 fs: workspace.app_state().fs.clone(),
2312 workspace: workspace.weak_handle(),
2313 active_context_editor: None,
2314 model_selector_menu_handle,
2315 }
2316 }
2317
2318 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
2319 let commands = SlashCommandRegistry::global(cx);
2320 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
2321 Some(
2322 workspace
2323 .read(cx)
2324 .active_item_as::<Editor>(cx)?
2325 .focus_handle(cx),
2326 )
2327 });
2328 let active_context_editor = self.active_context_editor.clone();
2329
2330 PopoverMenu::new("inject-context-menu")
2331 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
2332 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
2333 }))
2334 .menu(move |cx| {
2335 let active_context_editor = active_context_editor.clone()?;
2336 ContextMenu::build(cx, |mut menu, _cx| {
2337 for command_name in commands.featured_command_names() {
2338 if let Some(command) = commands.command(&command_name) {
2339 let menu_text = SharedString::from(Arc::from(command.menu_text()));
2340 menu = menu.custom_entry(
2341 {
2342 let command_name = command_name.clone();
2343 move |_cx| {
2344 h_flex()
2345 .gap_4()
2346 .w_full()
2347 .justify_between()
2348 .child(Label::new(menu_text.clone()))
2349 .child(
2350 Label::new(format!("/{command_name}"))
2351 .color(Color::Muted),
2352 )
2353 .into_any()
2354 }
2355 },
2356 {
2357 let active_context_editor = active_context_editor.clone();
2358 move |cx| {
2359 active_context_editor
2360 .update(cx, |context_editor, cx| {
2361 context_editor.insert_command(&command_name, cx)
2362 })
2363 .ok();
2364 }
2365 },
2366 )
2367 }
2368 }
2369
2370 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
2371 menu = menu
2372 .context(active_editor_focus_handle)
2373 .action("Quote Selection", Box::new(QuoteSelection));
2374 }
2375
2376 menu
2377 })
2378 .into()
2379 })
2380 }
2381
2382 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
2383 let model = CompletionProvider::global(cx).model();
2384 let context = &self
2385 .active_context_editor
2386 .as_ref()?
2387 .upgrade()?
2388 .read(cx)
2389 .context;
2390 let token_count = context.read(cx).token_count()?;
2391 let max_token_count = model.max_token_count();
2392
2393 let remaining_tokens = max_token_count as isize - token_count as isize;
2394 let token_count_color = if remaining_tokens <= 0 {
2395 Color::Error
2396 } else if token_count as f32 / max_token_count as f32 >= 0.8 {
2397 Color::Warning
2398 } else {
2399 Color::Muted
2400 };
2401
2402 Some(
2403 h_flex()
2404 .gap_0p5()
2405 .child(
2406 Label::new(humanize_token_count(token_count))
2407 .size(LabelSize::Small)
2408 .color(token_count_color),
2409 )
2410 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2411 .child(
2412 Label::new(humanize_token_count(max_token_count))
2413 .size(LabelSize::Small)
2414 .color(Color::Muted),
2415 ),
2416 )
2417 }
2418}
2419
2420impl Render for ContextEditorToolbarItem {
2421 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2422 h_flex()
2423 .gap_2()
2424 .child(ModelSelector::new(
2425 self.model_selector_menu_handle.clone(),
2426 self.fs.clone(),
2427 ))
2428 .children(self.render_remaining_tokens(cx))
2429 .child(self.render_inject_context_menu(cx))
2430 }
2431}
2432
2433impl ToolbarItemView for ContextEditorToolbarItem {
2434 fn set_active_pane_item(
2435 &mut self,
2436 active_pane_item: Option<&dyn ItemHandle>,
2437 cx: &mut ViewContext<Self>,
2438 ) -> ToolbarItemLocation {
2439 self.active_context_editor = active_pane_item
2440 .and_then(|item| item.act_as::<ContextEditor>(cx))
2441 .map(|editor| editor.downgrade());
2442 cx.notify();
2443 if self.active_context_editor.is_none() {
2444 ToolbarItemLocation::Hidden
2445 } else {
2446 ToolbarItemLocation::PrimaryRight
2447 }
2448 }
2449
2450 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
2451 cx.notify();
2452 }
2453}
2454
2455impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
2456
2457pub struct ContextHistory {
2458 picker: View<Picker<SavedContextPickerDelegate>>,
2459 _subscriptions: Vec<Subscription>,
2460 assistant_panel: WeakView<AssistantPanel>,
2461}
2462
2463impl ContextHistory {
2464 fn new(
2465 project: Model<Project>,
2466 context_store: Model<ContextStore>,
2467 assistant_panel: WeakView<AssistantPanel>,
2468 cx: &mut ViewContext<Self>,
2469 ) -> Self {
2470 let picker = cx.new_view(|cx| {
2471 Picker::uniform_list(
2472 SavedContextPickerDelegate::new(project, context_store.clone()),
2473 cx,
2474 )
2475 .modal(false)
2476 .max_height(None)
2477 });
2478
2479 let _subscriptions = vec![
2480 cx.observe(&context_store, |this, _, cx| {
2481 this.picker.update(cx, |picker, cx| picker.refresh(cx));
2482 }),
2483 cx.subscribe(&picker, Self::handle_picker_event),
2484 ];
2485
2486 Self {
2487 picker,
2488 _subscriptions,
2489 assistant_panel,
2490 }
2491 }
2492
2493 fn handle_picker_event(
2494 &mut self,
2495 _: View<Picker<SavedContextPickerDelegate>>,
2496 event: &SavedContextPickerEvent,
2497 cx: &mut ViewContext<Self>,
2498 ) {
2499 let SavedContextPickerEvent::Confirmed(context) = event;
2500 self.assistant_panel
2501 .update(cx, |assistant_panel, cx| match context {
2502 ContextMetadata::Remote(metadata) => {
2503 assistant_panel
2504 .open_remote_context(metadata.id.clone(), cx)
2505 .detach_and_log_err(cx);
2506 }
2507 ContextMetadata::Saved(metadata) => {
2508 assistant_panel
2509 .open_saved_context(metadata.path.clone(), cx)
2510 .detach_and_log_err(cx);
2511 }
2512 })
2513 .ok();
2514 }
2515}
2516
2517impl Render for ContextHistory {
2518 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
2519 div().size_full().child(self.picker.clone())
2520 }
2521}
2522
2523impl FocusableView for ContextHistory {
2524 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2525 self.picker.focus_handle(cx)
2526 }
2527}
2528
2529impl EventEmitter<()> for ContextHistory {}
2530
2531impl Item for ContextHistory {
2532 type Event = ();
2533
2534 fn tab_content(&self, params: item::TabContentParams, _: &WindowContext) -> AnyElement {
2535 let color = if params.selected {
2536 Color::Default
2537 } else {
2538 Color::Muted
2539 };
2540 Label::new("History").color(color).into_any_element()
2541 }
2542}
2543
2544type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
2545
2546fn render_slash_command_output_toggle(
2547 row: MultiBufferRow,
2548 is_folded: bool,
2549 fold: ToggleFold,
2550 _cx: &mut WindowContext,
2551) -> AnyElement {
2552 Disclosure::new(
2553 ("slash-command-output-fold-indicator", row.0 as u64),
2554 !is_folded,
2555 )
2556 .selected(is_folded)
2557 .on_click(move |_e, cx| fold(!is_folded, cx))
2558 .into_any_element()
2559}
2560
2561fn render_pending_slash_command_gutter_decoration(
2562 row: MultiBufferRow,
2563 status: &PendingSlashCommandStatus,
2564 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
2565) -> AnyElement {
2566 let mut icon = IconButton::new(
2567 ("slash-command-gutter-decoration", row.0),
2568 ui::IconName::TriangleRight,
2569 )
2570 .on_click(move |_e, cx| confirm_command(cx))
2571 .icon_size(ui::IconSize::Small)
2572 .size(ui::ButtonSize::None);
2573
2574 match status {
2575 PendingSlashCommandStatus::Idle => {
2576 icon = icon.icon_color(Color::Muted);
2577 }
2578 PendingSlashCommandStatus::Running { .. } => {
2579 icon = icon.selected(true);
2580 }
2581 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
2582 }
2583
2584 icon.into_any_element()
2585}
2586
2587fn render_docs_slash_command_trailer(
2588 row: MultiBufferRow,
2589 command: PendingSlashCommand,
2590 cx: &mut WindowContext,
2591) -> AnyElement {
2592 let Some(argument) = command.argument else {
2593 return Empty.into_any();
2594 };
2595
2596 let args = DocsSlashCommandArgs::parse(&argument);
2597
2598 let Some(store) = args
2599 .provider()
2600 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
2601 else {
2602 return Empty.into_any();
2603 };
2604
2605 let Some(package) = args.package() else {
2606 return Empty.into_any();
2607 };
2608
2609 let mut children = Vec::new();
2610
2611 if store.is_indexing(&package) {
2612 children.push(
2613 div()
2614 .id(("crates-being-indexed", row.0))
2615 .child(Icon::new(IconName::ArrowCircle).with_animation(
2616 "arrow-circle",
2617 Animation::new(Duration::from_secs(4)).repeat(),
2618 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
2619 ))
2620 .tooltip({
2621 let package = package.clone();
2622 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
2623 })
2624 .into_any_element(),
2625 );
2626 }
2627
2628 if let Some(latest_error) = store.latest_error_for_package(&package) {
2629 children.push(
2630 div()
2631 .id(("latest-error", row.0))
2632 .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
2633 .tooltip(move |cx| Tooltip::text(format!("failed to index: {latest_error}"), cx))
2634 .into_any_element(),
2635 )
2636 }
2637
2638 let is_indexing = store.is_indexing(&package);
2639 let latest_error = store.latest_error_for_package(&package);
2640
2641 if !is_indexing && latest_error.is_none() {
2642 return Empty.into_any();
2643 }
2644
2645 h_flex().gap_2().children(children).into_any_element()
2646}
2647
2648fn make_lsp_adapter_delegate(
2649 project: &Model<Project>,
2650 cx: &mut AppContext,
2651) -> Result<Arc<dyn LspAdapterDelegate>> {
2652 project.update(cx, |project, cx| {
2653 // TODO: Find the right worktree.
2654 let worktree = project
2655 .worktrees()
2656 .next()
2657 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
2658 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
2659 })
2660}
2661
2662fn slash_command_error_block_renderer(message: String) -> RenderBlock {
2663 Box::new(move |_| {
2664 div()
2665 .pl_6()
2666 .child(
2667 Label::new(format!("error: {}", message))
2668 .single_line()
2669 .color(Color::Error),
2670 )
2671 .into_any()
2672 })
2673}