1use crate::{
2 assistant_settings::{AssistantDockPosition, AssistantSettings},
3 humanize_token_count,
4 prompt_library::open_prompt_library,
5 prompts::PromptBuilder,
6 slash_command::{
7 default_command::DefaultSlashCommand,
8 docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
9 file_command::{self, codeblock_fence_for_path},
10 SlashCommandCompletionProvider, SlashCommandRegistry,
11 },
12 slash_command_picker,
13 terminal_inline_assistant::TerminalInlineAssistant,
14 Assist, AssistantPatch, AssistantPatchStatus, CacheStatus, ConfirmCommand, Content, Context,
15 ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
16 DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
17 InsertIntoEditor, Message, MessageId, MessageMetadata, MessageStatus, ModelPickerDelegate,
18 ModelSelector, NewContext, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
19 RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
20 ToggleModelSelector,
21};
22use anyhow::Result;
23use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
24use assistant_tool::ToolRegistry;
25use client::{proto, zed_urls, Client, Status};
26use collections::{BTreeSet, HashMap, HashSet};
27use editor::{
28 actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
29 display_map::{
30 BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata,
31 CustomBlockId, FoldId, RenderBlock, ToDisplayPoint,
32 },
33 scroll::{Autoscroll, AutoscrollStrategy},
34 Anchor, Editor, EditorEvent, ProposedChangeLocation, ProposedChangesEditor, RowExt,
35 ToOffset as _, ToPoint,
36};
37use editor::{display_map::CreaseId, FoldPlaceholder};
38use fs::Fs;
39use futures::FutureExt;
40use gpui::{
41 canvas, div, img, percentage, point, pulsating_between, size, Action, Animation, AnimationExt,
42 AnyElement, AnyView, AppContext, AsyncWindowContext, ClipboardEntry, ClipboardItem,
43 CursorStyle, Empty, Entity, EventEmitter, ExternalPaths, FocusHandle, FocusableView,
44 FontWeight, InteractiveElement, IntoElement, Model, ParentElement, Pixels, Render, RenderImage,
45 SharedString, Size, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
46 UpdateGlobal, View, VisualContext, WeakView, WindowContext,
47};
48use indexed_docs::IndexedDocsStore;
49use language::{
50 language_settings::SoftWrap, BufferSnapshot, LanguageRegistry, LspAdapterDelegate, ToOffset,
51};
52use language_model::{
53 provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
54 LanguageModelRegistry, Role,
55};
56use language_model::{LanguageModelImage, LanguageModelToolUse};
57use multi_buffer::MultiBufferRow;
58use picker::{Picker, PickerDelegate};
59use project::lsp_store::LocalLspAdapterDelegate;
60use project::{Project, Worktree};
61use rope::Point;
62use search::{buffer_search::DivRegistrar, BufferSearchBar};
63use serde::{Deserialize, Serialize};
64use settings::{update_settings_file, Settings};
65use smol::stream::StreamExt;
66use std::{
67 any::TypeId,
68 borrow::Cow,
69 cmp,
70 ops::{ControlFlow, Range},
71 path::PathBuf,
72 sync::Arc,
73 time::Duration,
74};
75use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
76use text::SelectionGoal;
77use ui::{
78 prelude::*,
79 utils::{format_distance_from_now, DateTimeType},
80 Avatar, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
81 ListItemSpacing, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip,
82};
83use util::{maybe, ResultExt};
84use workspace::{
85 dock::{DockPosition, Panel, PanelEvent},
86 item::{self, FollowableItem, Item, ItemHandle},
87 notifications::NotificationId,
88 pane::{self, SaveIntent},
89 searchable::{SearchEvent, SearchableItem},
90 DraggedSelection, Pane, Save, ShowConfiguration, Toast, ToggleZoom, ToolbarItemEvent,
91 ToolbarItemLocation, ToolbarItemView, Workspace,
92};
93use workspace::{searchable::SearchableItemHandle, DraggedTab};
94use zed_actions::InlineAssist;
95
96pub fn init(cx: &mut AppContext) {
97 workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
98 cx.observe_new_views(
99 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
100 workspace
101 .register_action(|workspace, _: &ToggleFocus, cx| {
102 let settings = AssistantSettings::get_global(cx);
103 if !settings.enabled {
104 return;
105 }
106
107 workspace.toggle_panel_focus::<AssistantPanel>(cx);
108 })
109 .register_action(AssistantPanel::inline_assist)
110 .register_action(ContextEditor::quote_selection)
111 .register_action(ContextEditor::insert_selection)
112 .register_action(ContextEditor::copy_code)
113 .register_action(ContextEditor::insert_dragged_files)
114 .register_action(AssistantPanel::show_configuration)
115 .register_action(AssistantPanel::create_new_context);
116 },
117 )
118 .detach();
119
120 cx.observe_new_views(
121 |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
122 let settings = AssistantSettings::get_global(cx);
123 terminal_panel.asssistant_enabled(settings.enabled, cx);
124 },
125 )
126 .detach();
127}
128
129pub enum AssistantPanelEvent {
130 ContextEdited,
131}
132
133pub struct AssistantPanel {
134 pane: View<Pane>,
135 workspace: WeakView<Workspace>,
136 width: Option<Pixels>,
137 height: Option<Pixels>,
138 project: Model<Project>,
139 context_store: Model<ContextStore>,
140 languages: Arc<LanguageRegistry>,
141 fs: Arc<dyn Fs>,
142 subscriptions: Vec<Subscription>,
143 model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
144 model_summary_editor: View<Editor>,
145 authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
146 configuration_subscription: Option<Subscription>,
147 client_status: Option<client::Status>,
148 watch_client_status: Option<Task<()>>,
149 show_zed_ai_notice: bool,
150}
151
152#[derive(Clone)]
153enum ContextMetadata {
154 Remote(RemoteContextMetadata),
155 Saved(SavedContextMetadata),
156}
157
158struct SavedContextPickerDelegate {
159 store: Model<ContextStore>,
160 project: Model<Project>,
161 matches: Vec<ContextMetadata>,
162 selected_index: usize,
163}
164
165enum SavedContextPickerEvent {
166 Confirmed(ContextMetadata),
167}
168
169enum InlineAssistTarget {
170 Editor(View<Editor>, bool),
171 Terminal(View<TerminalView>),
172}
173
174impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
175
176impl SavedContextPickerDelegate {
177 fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
178 Self {
179 project,
180 store,
181 matches: Vec::new(),
182 selected_index: 0,
183 }
184 }
185}
186
187impl PickerDelegate for SavedContextPickerDelegate {
188 type ListItem = ListItem;
189
190 fn match_count(&self) -> usize {
191 self.matches.len()
192 }
193
194 fn selected_index(&self) -> usize {
195 self.selected_index
196 }
197
198 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
199 self.selected_index = ix;
200 }
201
202 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
203 "Search...".into()
204 }
205
206 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
207 let search = self.store.read(cx).search(query, cx);
208 cx.spawn(|this, mut cx| async move {
209 let matches = search.await;
210 this.update(&mut cx, |this, cx| {
211 let host_contexts = this.delegate.store.read(cx).host_contexts();
212 this.delegate.matches = host_contexts
213 .iter()
214 .cloned()
215 .map(ContextMetadata::Remote)
216 .chain(matches.into_iter().map(ContextMetadata::Saved))
217 .collect();
218 this.delegate.selected_index = 0;
219 cx.notify();
220 })
221 .ok();
222 })
223 }
224
225 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
226 if let Some(metadata) = self.matches.get(self.selected_index) {
227 cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
228 }
229 }
230
231 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
232
233 fn render_match(
234 &self,
235 ix: usize,
236 selected: bool,
237 cx: &mut ViewContext<Picker<Self>>,
238 ) -> Option<Self::ListItem> {
239 let context = self.matches.get(ix)?;
240 let item = match context {
241 ContextMetadata::Remote(context) => {
242 let host_user = self.project.read(cx).host().and_then(|collaborator| {
243 self.project
244 .read(cx)
245 .user_store()
246 .read(cx)
247 .get_cached_user(collaborator.user_id)
248 });
249 div()
250 .flex()
251 .w_full()
252 .justify_between()
253 .gap_2()
254 .child(
255 h_flex().flex_1().overflow_x_hidden().child(
256 Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
257 .size(LabelSize::Small),
258 ),
259 )
260 .child(
261 h_flex()
262 .gap_2()
263 .children(if let Some(host_user) = host_user {
264 vec![
265 Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
266 Label::new(format!("Shared by @{}", host_user.github_login))
267 .color(Color::Muted)
268 .size(LabelSize::Small)
269 .into_any_element(),
270 ]
271 } else {
272 vec![Label::new("Shared by host")
273 .color(Color::Muted)
274 .size(LabelSize::Small)
275 .into_any_element()]
276 }),
277 )
278 }
279 ContextMetadata::Saved(context) => div()
280 .flex()
281 .w_full()
282 .justify_between()
283 .gap_2()
284 .child(
285 h_flex()
286 .flex_1()
287 .child(Label::new(context.title.clone()).size(LabelSize::Small))
288 .overflow_x_hidden(),
289 )
290 .child(
291 Label::new(format_distance_from_now(
292 DateTimeType::Local(context.mtime),
293 false,
294 true,
295 true,
296 ))
297 .color(Color::Muted)
298 .size(LabelSize::Small),
299 ),
300 };
301 Some(
302 ListItem::new(ix)
303 .inset(true)
304 .spacing(ListItemSpacing::Sparse)
305 .selected(selected)
306 .child(item),
307 )
308 }
309}
310
311impl AssistantPanel {
312 pub fn load(
313 workspace: WeakView<Workspace>,
314 prompt_builder: Arc<PromptBuilder>,
315 cx: AsyncWindowContext,
316 ) -> Task<Result<View<Self>>> {
317 cx.spawn(|mut cx| async move {
318 let context_store = workspace
319 .update(&mut cx, |workspace, cx| {
320 let project = workspace.project().clone();
321 ContextStore::new(project, prompt_builder.clone(), cx)
322 })?
323 .await?;
324
325 workspace.update(&mut cx, |workspace, cx| {
326 // TODO: deserialize state.
327 cx.new_view(|cx| Self::new(workspace, context_store, cx))
328 })
329 })
330 }
331
332 fn new(
333 workspace: &Workspace,
334 context_store: Model<ContextStore>,
335 cx: &mut ViewContext<Self>,
336 ) -> Self {
337 let model_selector_menu_handle = PopoverMenuHandle::default();
338 let model_summary_editor = cx.new_view(Editor::single_line);
339 let context_editor_toolbar = cx.new_view(|_| {
340 ContextEditorToolbarItem::new(
341 workspace,
342 model_selector_menu_handle.clone(),
343 model_summary_editor.clone(),
344 )
345 });
346
347 let pane = cx.new_view(|cx| {
348 let mut pane = Pane::new(
349 workspace.weak_handle(),
350 workspace.project().clone(),
351 Default::default(),
352 None,
353 NewContext.boxed_clone(),
354 cx,
355 );
356
357 let project = workspace.project().clone();
358 pane.set_custom_drop_handle(cx, move |_, dropped_item, cx| {
359 let action = maybe!({
360 if project.read(cx).is_local() {
361 if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
362 return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
363 }
364 }
365
366 let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
367 {
368 if &tab.pane == cx.view() {
369 return None;
370 }
371 let item = tab.pane.read(cx).item_for_index(tab.ix);
372 Some(
373 item.and_then(|item| item.project_path(cx))
374 .into_iter()
375 .collect::<Vec<_>>(),
376 )
377 } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>()
378 {
379 Some(
380 selection
381 .items()
382 .filter_map(|item| {
383 project.read(cx).path_for_entry(item.entry_id, cx)
384 })
385 .collect::<Vec<_>>(),
386 )
387 } else {
388 None
389 }?;
390
391 let paths = project_paths
392 .into_iter()
393 .filter_map(|project_path| {
394 let worktree = project
395 .read(cx)
396 .worktree_for_id(project_path.worktree_id, cx)?;
397
398 let mut full_path = PathBuf::from(worktree.read(cx).root_name());
399 full_path.push(&project_path.path);
400 Some(full_path)
401 })
402 .collect::<Vec<_>>();
403
404 Some(InsertDraggedFiles::ProjectPaths(paths))
405 });
406
407 if let Some(action) = action {
408 cx.dispatch_action(action.boxed_clone());
409 }
410
411 ControlFlow::Break(())
412 });
413
414 pane.set_can_split(false, cx);
415 pane.set_can_navigate(true, cx);
416 pane.display_nav_history_buttons(None);
417 pane.set_should_display_tab_bar(|_| true);
418 pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
419 let focus_handle = pane.focus_handle(cx);
420 let left_children = IconButton::new("history", IconName::HistoryRerun)
421 .icon_size(IconSize::Small)
422 .on_click(cx.listener({
423 let focus_handle = focus_handle.clone();
424 move |_, _, cx| {
425 focus_handle.focus(cx);
426 cx.dispatch_action(DeployHistory.boxed_clone())
427 }
428 }))
429 .tooltip({
430 let focus_handle = focus_handle.clone();
431 move |cx| {
432 Tooltip::for_action_in(
433 "Open History",
434 &DeployHistory,
435 &focus_handle,
436 cx,
437 )
438 }
439 })
440 .selected(
441 pane.active_item()
442 .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
443 );
444 let _pane = cx.view().clone();
445 let right_children =
446 h_flex()
447 .gap(Spacing::XSmall.rems(cx))
448 .child(
449 IconButton::new("new-context", IconName::Plus)
450 .on_click(cx.listener(|_, _, cx| {
451 cx.dispatch_action(NewContext.boxed_clone())
452 }))
453 .tooltip(move |cx| {
454 Tooltip::for_action_in(
455 "New Context",
456 &NewContext,
457 &focus_handle,
458 cx,
459 )
460 }),
461 )
462 .child(
463 PopoverMenu::new("assistant-panel-popover-menu")
464 .trigger(
465 IconButton::new("menu", IconName::EllipsisVertical)
466 .icon_size(IconSize::Small)
467 .tooltip(|cx| Tooltip::text("Toggle Assistant Menu", cx)),
468 )
469 .menu(move |cx| {
470 let zoom_label = if _pane.read(cx).is_zoomed() {
471 "Zoom Out"
472 } else {
473 "Zoom In"
474 };
475 let focus_handle = _pane.focus_handle(cx);
476 Some(ContextMenu::build(cx, move |menu, _| {
477 menu.context(focus_handle.clone())
478 .action("New Context", Box::new(NewContext))
479 .action("History", Box::new(DeployHistory))
480 .action("Prompt Library", Box::new(DeployPromptLibrary))
481 .action("Configure", Box::new(ShowConfiguration))
482 .action(zoom_label, Box::new(ToggleZoom))
483 }))
484 }),
485 )
486 .into_any_element()
487 .into();
488
489 (Some(left_children.into_any_element()), right_children)
490 });
491 pane.toolbar().update(cx, |toolbar, cx| {
492 toolbar.add_item(context_editor_toolbar.clone(), cx);
493 toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
494 });
495 pane
496 });
497
498 let subscriptions = vec![
499 cx.observe(&pane, |_, _, cx| cx.notify()),
500 cx.subscribe(&pane, Self::handle_pane_event),
501 cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
502 cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
503 cx.subscribe(&context_store, Self::handle_context_store_event),
504 cx.subscribe(
505 &LanguageModelRegistry::global(cx),
506 |this, _, event: &language_model::Event, cx| match event {
507 language_model::Event::ActiveModelChanged => {
508 this.completion_provider_changed(cx);
509 }
510 language_model::Event::ProviderStateChanged => {
511 this.ensure_authenticated(cx);
512 cx.notify()
513 }
514 language_model::Event::AddedProvider(_)
515 | language_model::Event::RemovedProvider(_) => {
516 this.ensure_authenticated(cx);
517 }
518 },
519 ),
520 ];
521
522 let watch_client_status = Self::watch_client_status(workspace.client().clone(), cx);
523
524 let mut this = Self {
525 pane,
526 workspace: workspace.weak_handle(),
527 width: None,
528 height: None,
529 project: workspace.project().clone(),
530 context_store,
531 languages: workspace.app_state().languages.clone(),
532 fs: workspace.app_state().fs.clone(),
533 subscriptions,
534 model_selector_menu_handle,
535 model_summary_editor,
536 authenticate_provider_task: None,
537 configuration_subscription: None,
538 client_status: None,
539 watch_client_status: Some(watch_client_status),
540 show_zed_ai_notice: false,
541 };
542 this.new_context(cx);
543 this
544 }
545
546 fn watch_client_status(client: Arc<Client>, cx: &mut ViewContext<Self>) -> Task<()> {
547 let mut status_rx = client.status();
548
549 cx.spawn(|this, mut cx| async move {
550 while let Some(status) = status_rx.next().await {
551 this.update(&mut cx, |this, cx| {
552 if this.client_status.is_none()
553 || this
554 .client_status
555 .map_or(false, |old_status| old_status != status)
556 {
557 this.update_zed_ai_notice_visibility(status, cx);
558 }
559 this.client_status = Some(status);
560 })
561 .log_err();
562 }
563 this.update(&mut cx, |this, _cx| this.watch_client_status = None)
564 .log_err();
565 })
566 }
567
568 fn handle_pane_event(
569 &mut self,
570 pane: View<Pane>,
571 event: &pane::Event,
572 cx: &mut ViewContext<Self>,
573 ) {
574 let update_model_summary = match event {
575 pane::Event::Remove { .. } => {
576 cx.emit(PanelEvent::Close);
577 false
578 }
579 pane::Event::ZoomIn => {
580 cx.emit(PanelEvent::ZoomIn);
581 false
582 }
583 pane::Event::ZoomOut => {
584 cx.emit(PanelEvent::ZoomOut);
585 false
586 }
587
588 pane::Event::AddItem { item } => {
589 self.workspace
590 .update(cx, |workspace, cx| {
591 item.added_to_pane(workspace, self.pane.clone(), cx)
592 })
593 .ok();
594 true
595 }
596
597 pane::Event::ActivateItem { local } => {
598 if *local {
599 self.workspace
600 .update(cx, |workspace, cx| {
601 workspace.unfollow_in_pane(&pane, cx);
602 })
603 .ok();
604 }
605 cx.emit(AssistantPanelEvent::ContextEdited);
606 true
607 }
608 pane::Event::RemovedItem { .. } => {
609 let has_configuration_view = self
610 .pane
611 .read(cx)
612 .items_of_type::<ConfigurationView>()
613 .next()
614 .is_some();
615
616 if !has_configuration_view {
617 self.configuration_subscription = None;
618 }
619
620 cx.emit(AssistantPanelEvent::ContextEdited);
621 true
622 }
623
624 _ => false,
625 };
626
627 if update_model_summary {
628 if let Some(editor) = self.active_context_editor(cx) {
629 self.show_updated_summary(&editor, cx)
630 }
631 }
632 }
633
634 fn handle_summary_editor_event(
635 &mut self,
636 model_summary_editor: View<Editor>,
637 event: &EditorEvent,
638 cx: &mut ViewContext<Self>,
639 ) {
640 if matches!(event, EditorEvent::Edited { .. }) {
641 if let Some(context_editor) = self.active_context_editor(cx) {
642 let new_summary = model_summary_editor.read(cx).text(cx);
643 context_editor.update(cx, |context_editor, cx| {
644 context_editor.context.update(cx, |context, cx| {
645 if context.summary().is_none()
646 && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
647 {
648 return;
649 }
650 context.custom_summary(new_summary, cx)
651 });
652 });
653 }
654 }
655 }
656
657 fn update_zed_ai_notice_visibility(
658 &mut self,
659 client_status: Status,
660 cx: &mut ViewContext<Self>,
661 ) {
662 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
663
664 // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
665 // the provider, we want to show a nudge to sign in.
666 let show_zed_ai_notice = client_status.is_signed_out()
667 && active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID);
668
669 self.show_zed_ai_notice = show_zed_ai_notice;
670 cx.notify();
671 }
672
673 fn handle_toolbar_event(
674 &mut self,
675 _: View<ContextEditorToolbarItem>,
676 _: &ContextEditorToolbarItemEvent,
677 cx: &mut ViewContext<Self>,
678 ) {
679 if let Some(context_editor) = self.active_context_editor(cx) {
680 context_editor.update(cx, |context_editor, cx| {
681 context_editor.context.update(cx, |context, cx| {
682 context.summarize(true, cx);
683 })
684 })
685 }
686 }
687
688 fn handle_context_store_event(
689 &mut self,
690 _context_store: Model<ContextStore>,
691 event: &ContextStoreEvent,
692 cx: &mut ViewContext<Self>,
693 ) {
694 let ContextStoreEvent::ContextCreated(context_id) = event;
695 let Some(context) = self
696 .context_store
697 .read(cx)
698 .loaded_context_for_id(&context_id, cx)
699 else {
700 log::error!("no context found with ID: {}", context_id.to_proto());
701 return;
702 };
703 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
704 .log_err()
705 .flatten();
706
707 let assistant_panel = cx.view().downgrade();
708 let editor = cx.new_view(|cx| {
709 let mut editor = ContextEditor::for_context(
710 context,
711 self.fs.clone(),
712 self.workspace.clone(),
713 self.project.clone(),
714 lsp_adapter_delegate,
715 assistant_panel,
716 cx,
717 );
718 editor.insert_default_prompt(cx);
719 editor
720 });
721
722 self.show_context(editor.clone(), cx);
723 }
724
725 fn completion_provider_changed(&mut self, cx: &mut ViewContext<Self>) {
726 if let Some(editor) = self.active_context_editor(cx) {
727 editor.update(cx, |active_context, cx| {
728 active_context
729 .context
730 .update(cx, |context, cx| context.completion_provider_changed(cx))
731 })
732 }
733
734 let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
735 .active_provider()
736 .map(|p| p.id())
737 else {
738 return;
739 };
740
741 if self
742 .authenticate_provider_task
743 .as_ref()
744 .map_or(true, |(old_provider_id, _)| {
745 *old_provider_id != new_provider_id
746 })
747 {
748 self.authenticate_provider_task = None;
749 self.ensure_authenticated(cx);
750 }
751
752 if let Some(status) = self.client_status {
753 self.update_zed_ai_notice_visibility(status, cx);
754 }
755 }
756
757 fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) {
758 if self.is_authenticated(cx) {
759 return;
760 }
761
762 let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
763 return;
764 };
765
766 let load_credentials = self.authenticate(cx);
767
768 if self.authenticate_provider_task.is_none() {
769 self.authenticate_provider_task = Some((
770 provider.id(),
771 cx.spawn(|this, mut cx| async move {
772 if let Some(future) = load_credentials {
773 let _ = future.await;
774 }
775 this.update(&mut cx, |this, _cx| {
776 this.authenticate_provider_task = None;
777 })
778 .log_err();
779 }),
780 ));
781 }
782 }
783
784 pub fn inline_assist(
785 workspace: &mut Workspace,
786 action: &InlineAssist,
787 cx: &mut ViewContext<Workspace>,
788 ) {
789 let settings = AssistantSettings::get_global(cx);
790 if !settings.enabled {
791 return;
792 }
793
794 let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
795 return;
796 };
797
798 let Some(inline_assist_target) =
799 Self::resolve_inline_assist_target(workspace, &assistant_panel, cx)
800 else {
801 return;
802 };
803
804 let initial_prompt = action.prompt.clone();
805
806 if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
807 match inline_assist_target {
808 InlineAssistTarget::Editor(active_editor, include_context) => {
809 InlineAssistant::update_global(cx, |assistant, cx| {
810 assistant.assist(
811 &active_editor,
812 Some(cx.view().downgrade()),
813 include_context.then_some(&assistant_panel),
814 initial_prompt,
815 cx,
816 )
817 })
818 }
819 InlineAssistTarget::Terminal(active_terminal) => {
820 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
821 assistant.assist(
822 &active_terminal,
823 Some(cx.view().downgrade()),
824 Some(&assistant_panel),
825 initial_prompt,
826 cx,
827 )
828 })
829 }
830 }
831 } else {
832 let assistant_panel = assistant_panel.downgrade();
833 cx.spawn(|workspace, mut cx| async move {
834 let Some(task) =
835 assistant_panel.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
836 else {
837 let answer = cx
838 .prompt(
839 gpui::PromptLevel::Warning,
840 "No language model provider configured",
841 None,
842 &["Configure", "Cancel"],
843 )
844 .await
845 .ok();
846 if let Some(answer) = answer {
847 if answer == 0 {
848 cx.update(|cx| cx.dispatch_action(Box::new(ShowConfiguration)))
849 .ok();
850 }
851 }
852 return Ok(());
853 };
854 task.await?;
855 if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
856 cx.update(|cx| match inline_assist_target {
857 InlineAssistTarget::Editor(active_editor, include_context) => {
858 let assistant_panel = if include_context {
859 assistant_panel.upgrade()
860 } else {
861 None
862 };
863 InlineAssistant::update_global(cx, |assistant, cx| {
864 assistant.assist(
865 &active_editor,
866 Some(workspace),
867 assistant_panel.as_ref(),
868 initial_prompt,
869 cx,
870 )
871 })
872 }
873 InlineAssistTarget::Terminal(active_terminal) => {
874 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
875 assistant.assist(
876 &active_terminal,
877 Some(workspace),
878 assistant_panel.upgrade().as_ref(),
879 initial_prompt,
880 cx,
881 )
882 })
883 }
884 })?
885 } else {
886 workspace.update(&mut cx, |workspace, cx| {
887 workspace.focus_panel::<AssistantPanel>(cx)
888 })?;
889 }
890
891 anyhow::Ok(())
892 })
893 .detach_and_log_err(cx)
894 }
895 }
896
897 fn resolve_inline_assist_target(
898 workspace: &mut Workspace,
899 assistant_panel: &View<AssistantPanel>,
900 cx: &mut WindowContext,
901 ) -> Option<InlineAssistTarget> {
902 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
903 if terminal_panel
904 .read(cx)
905 .focus_handle(cx)
906 .contains_focused(cx)
907 {
908 if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
909 pane.read(cx)
910 .active_item()
911 .and_then(|t| t.downcast::<TerminalView>())
912 }) {
913 return Some(InlineAssistTarget::Terminal(terminal_view));
914 }
915 }
916 }
917 let context_editor =
918 assistant_panel
919 .read(cx)
920 .active_context_editor(cx)
921 .and_then(|editor| {
922 let editor = &editor.read(cx).editor;
923 if editor.read(cx).is_focused(cx) {
924 Some(editor.clone())
925 } else {
926 None
927 }
928 });
929
930 if let Some(context_editor) = context_editor {
931 Some(InlineAssistTarget::Editor(context_editor, false))
932 } else if let Some(workspace_editor) = workspace
933 .active_item(cx)
934 .and_then(|item| item.act_as::<Editor>(cx))
935 {
936 Some(InlineAssistTarget::Editor(workspace_editor, true))
937 } else if let Some(terminal_view) = workspace
938 .active_item(cx)
939 .and_then(|item| item.act_as::<TerminalView>(cx))
940 {
941 Some(InlineAssistTarget::Terminal(terminal_view))
942 } else {
943 None
944 }
945 }
946
947 pub fn create_new_context(
948 workspace: &mut Workspace,
949 _: &NewContext,
950 cx: &mut ViewContext<Workspace>,
951 ) {
952 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
953 let did_create_context = panel
954 .update(cx, |panel, cx| {
955 panel.new_context(cx)?;
956
957 Some(())
958 })
959 .is_some();
960 if did_create_context {
961 ContextEditor::quote_selection(workspace, &Default::default(), cx);
962 }
963 }
964 }
965
966 fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
967 let project = self.project.read(cx);
968 if project.is_via_collab() {
969 let task = self
970 .context_store
971 .update(cx, |store, cx| store.create_remote_context(cx));
972
973 cx.spawn(|this, mut cx| async move {
974 let context = task.await?;
975
976 this.update(&mut cx, |this, cx| {
977 let workspace = this.workspace.clone();
978 let project = this.project.clone();
979 let lsp_adapter_delegate =
980 make_lsp_adapter_delegate(&project, cx).log_err().flatten();
981
982 let fs = this.fs.clone();
983 let project = this.project.clone();
984 let weak_assistant_panel = cx.view().downgrade();
985
986 let editor = cx.new_view(|cx| {
987 ContextEditor::for_context(
988 context,
989 fs,
990 workspace,
991 project,
992 lsp_adapter_delegate,
993 weak_assistant_panel,
994 cx,
995 )
996 });
997
998 this.show_context(editor, cx);
999
1000 anyhow::Ok(())
1001 })??;
1002
1003 anyhow::Ok(())
1004 })
1005 .detach_and_log_err(cx);
1006
1007 None
1008 } else {
1009 let context = self.context_store.update(cx, |store, cx| store.create(cx));
1010 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
1011 .log_err()
1012 .flatten();
1013
1014 let assistant_panel = cx.view().downgrade();
1015 let editor = cx.new_view(|cx| {
1016 let mut editor = ContextEditor::for_context(
1017 context,
1018 self.fs.clone(),
1019 self.workspace.clone(),
1020 self.project.clone(),
1021 lsp_adapter_delegate,
1022 assistant_panel,
1023 cx,
1024 );
1025 editor.insert_default_prompt(cx);
1026 editor
1027 });
1028
1029 self.show_context(editor.clone(), cx);
1030 let workspace = self.workspace.clone();
1031 cx.spawn(move |_, mut cx| async move {
1032 workspace
1033 .update(&mut cx, |workspace, cx| {
1034 workspace.focus_panel::<AssistantPanel>(cx);
1035 })
1036 .ok();
1037 })
1038 .detach();
1039 Some(editor)
1040 }
1041 }
1042
1043 fn show_context(&mut self, context_editor: View<ContextEditor>, cx: &mut ViewContext<Self>) {
1044 let focus = self.focus_handle(cx).contains_focused(cx);
1045 let prev_len = self.pane.read(cx).items_len();
1046 self.pane.update(cx, |pane, cx| {
1047 pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx)
1048 });
1049
1050 if prev_len != self.pane.read(cx).items_len() {
1051 self.subscriptions
1052 .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
1053 }
1054
1055 self.show_updated_summary(&context_editor, cx);
1056
1057 cx.emit(AssistantPanelEvent::ContextEdited);
1058 cx.notify();
1059 }
1060
1061 fn show_updated_summary(
1062 &self,
1063 context_editor: &View<ContextEditor>,
1064 cx: &mut ViewContext<Self>,
1065 ) {
1066 context_editor.update(cx, |context_editor, cx| {
1067 let new_summary = context_editor.title(cx).to_string();
1068 self.model_summary_editor.update(cx, |summary_editor, cx| {
1069 if summary_editor.text(cx) != new_summary {
1070 summary_editor.set_text(new_summary, cx);
1071 }
1072 });
1073 });
1074 }
1075
1076 fn handle_context_editor_event(
1077 &mut self,
1078 context_editor: View<ContextEditor>,
1079 event: &EditorEvent,
1080 cx: &mut ViewContext<Self>,
1081 ) {
1082 match event {
1083 EditorEvent::TitleChanged => {
1084 self.show_updated_summary(&context_editor, cx);
1085 cx.notify()
1086 }
1087 EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
1088 _ => {}
1089 }
1090 }
1091
1092 fn show_configuration(
1093 workspace: &mut Workspace,
1094 _: &ShowConfiguration,
1095 cx: &mut ViewContext<Workspace>,
1096 ) {
1097 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1098 return;
1099 };
1100
1101 if !panel.focus_handle(cx).contains_focused(cx) {
1102 workspace.toggle_panel_focus::<AssistantPanel>(cx);
1103 }
1104
1105 panel.update(cx, |this, cx| {
1106 this.show_configuration_tab(cx);
1107 })
1108 }
1109
1110 fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
1111 let configuration_item_ix = self
1112 .pane
1113 .read(cx)
1114 .items()
1115 .position(|item| item.downcast::<ConfigurationView>().is_some());
1116
1117 if let Some(configuration_item_ix) = configuration_item_ix {
1118 self.pane.update(cx, |pane, cx| {
1119 pane.activate_item(configuration_item_ix, true, true, cx);
1120 });
1121 } else {
1122 let configuration = cx.new_view(ConfigurationView::new);
1123 self.configuration_subscription = Some(cx.subscribe(
1124 &configuration,
1125 |this, _, event: &ConfigurationViewEvent, cx| match event {
1126 ConfigurationViewEvent::NewProviderContextEditor(provider) => {
1127 if LanguageModelRegistry::read_global(cx)
1128 .active_provider()
1129 .map_or(true, |p| p.id() != provider.id())
1130 {
1131 if let Some(model) = provider.provided_models(cx).first().cloned() {
1132 update_settings_file::<AssistantSettings>(
1133 this.fs.clone(),
1134 cx,
1135 move |settings, _| settings.set_model(model),
1136 );
1137 }
1138 }
1139
1140 this.new_context(cx);
1141 }
1142 },
1143 ));
1144 self.pane.update(cx, |pane, cx| {
1145 pane.add_item(Box::new(configuration), true, true, None, cx);
1146 });
1147 }
1148 }
1149
1150 fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
1151 let history_item_ix = self
1152 .pane
1153 .read(cx)
1154 .items()
1155 .position(|item| item.downcast::<ContextHistory>().is_some());
1156
1157 if let Some(history_item_ix) = history_item_ix {
1158 self.pane.update(cx, |pane, cx| {
1159 pane.activate_item(history_item_ix, true, true, cx);
1160 });
1161 } else {
1162 let assistant_panel = cx.view().downgrade();
1163 let history = cx.new_view(|cx| {
1164 ContextHistory::new(
1165 self.project.clone(),
1166 self.context_store.clone(),
1167 assistant_panel,
1168 cx,
1169 )
1170 });
1171 self.pane.update(cx, |pane, cx| {
1172 pane.add_item(Box::new(history), true, true, None, cx);
1173 });
1174 }
1175 }
1176
1177 fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
1178 open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx);
1179 }
1180
1181 fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
1182 self.model_selector_menu_handle.toggle(cx);
1183 }
1184
1185 fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
1186 self.pane
1187 .read(cx)
1188 .active_item()?
1189 .downcast::<ContextEditor>()
1190 }
1191
1192 pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
1193 Some(self.active_context_editor(cx)?.read(cx).context.clone())
1194 }
1195
1196 fn open_saved_context(
1197 &mut self,
1198 path: PathBuf,
1199 cx: &mut ViewContext<Self>,
1200 ) -> Task<Result<()>> {
1201 let existing_context = self.pane.read(cx).items().find_map(|item| {
1202 item.downcast::<ContextEditor>()
1203 .filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path))
1204 });
1205 if let Some(existing_context) = existing_context {
1206 return cx.spawn(|this, mut cx| async move {
1207 this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
1208 });
1209 }
1210
1211 let context = self
1212 .context_store
1213 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1214 let fs = self.fs.clone();
1215 let project = self.project.clone();
1216 let workspace = self.workspace.clone();
1217
1218 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
1219
1220 cx.spawn(|this, mut cx| async move {
1221 let context = context.await?;
1222 let assistant_panel = this.clone();
1223 this.update(&mut cx, |this, cx| {
1224 let editor = cx.new_view(|cx| {
1225 ContextEditor::for_context(
1226 context,
1227 fs,
1228 workspace,
1229 project,
1230 lsp_adapter_delegate,
1231 assistant_panel,
1232 cx,
1233 )
1234 });
1235 this.show_context(editor, cx);
1236 anyhow::Ok(())
1237 })??;
1238 Ok(())
1239 })
1240 }
1241
1242 fn open_remote_context(
1243 &mut self,
1244 id: ContextId,
1245 cx: &mut ViewContext<Self>,
1246 ) -> Task<Result<View<ContextEditor>>> {
1247 let existing_context = self.pane.read(cx).items().find_map(|item| {
1248 item.downcast::<ContextEditor>()
1249 .filter(|editor| *editor.read(cx).context.read(cx).id() == id)
1250 });
1251 if let Some(existing_context) = existing_context {
1252 return cx.spawn(|this, mut cx| async move {
1253 this.update(&mut cx, |this, cx| {
1254 this.show_context(existing_context.clone(), cx)
1255 })?;
1256 Ok(existing_context)
1257 });
1258 }
1259
1260 let context = self
1261 .context_store
1262 .update(cx, |store, cx| store.open_remote_context(id, cx));
1263 let fs = self.fs.clone();
1264 let workspace = self.workspace.clone();
1265 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
1266 .log_err()
1267 .flatten();
1268
1269 cx.spawn(|this, mut cx| async move {
1270 let context = context.await?;
1271 let assistant_panel = this.clone();
1272 this.update(&mut cx, |this, cx| {
1273 let editor = cx.new_view(|cx| {
1274 ContextEditor::for_context(
1275 context,
1276 fs,
1277 workspace,
1278 this.project.clone(),
1279 lsp_adapter_delegate,
1280 assistant_panel,
1281 cx,
1282 )
1283 });
1284 this.show_context(editor.clone(), cx);
1285 anyhow::Ok(editor)
1286 })?
1287 })
1288 }
1289
1290 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1291 LanguageModelRegistry::read_global(cx)
1292 .active_provider()
1293 .map_or(false, |provider| provider.is_authenticated(cx))
1294 }
1295
1296 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
1297 LanguageModelRegistry::read_global(cx)
1298 .active_provider()
1299 .map_or(None, |provider| Some(provider.authenticate(cx)))
1300 }
1301}
1302
1303impl Render for AssistantPanel {
1304 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1305 let mut registrar = DivRegistrar::new(
1306 |panel, cx| {
1307 panel
1308 .pane
1309 .read(cx)
1310 .toolbar()
1311 .read(cx)
1312 .item_of_type::<BufferSearchBar>()
1313 },
1314 cx,
1315 );
1316 BufferSearchBar::register(&mut registrar);
1317 let registrar = registrar.into_div();
1318
1319 v_flex()
1320 .key_context("AssistantPanel")
1321 .size_full()
1322 .on_action(cx.listener(|this, _: &NewContext, cx| {
1323 this.new_context(cx);
1324 }))
1325 .on_action(
1326 cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
1327 )
1328 .on_action(cx.listener(AssistantPanel::deploy_history))
1329 .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1330 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1331 .child(registrar.size_full().child(self.pane.clone()))
1332 .into_any_element()
1333 }
1334}
1335
1336impl Panel for AssistantPanel {
1337 fn persistent_name() -> &'static str {
1338 "AssistantPanel"
1339 }
1340
1341 fn position(&self, cx: &WindowContext) -> DockPosition {
1342 match AssistantSettings::get_global(cx).dock {
1343 AssistantDockPosition::Left => DockPosition::Left,
1344 AssistantDockPosition::Bottom => DockPosition::Bottom,
1345 AssistantDockPosition::Right => DockPosition::Right,
1346 }
1347 }
1348
1349 fn position_is_valid(&self, _: DockPosition) -> bool {
1350 true
1351 }
1352
1353 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1354 settings::update_settings_file::<AssistantSettings>(
1355 self.fs.clone(),
1356 cx,
1357 move |settings, _| {
1358 let dock = match position {
1359 DockPosition::Left => AssistantDockPosition::Left,
1360 DockPosition::Bottom => AssistantDockPosition::Bottom,
1361 DockPosition::Right => AssistantDockPosition::Right,
1362 };
1363 settings.set_dock(dock);
1364 },
1365 );
1366 }
1367
1368 fn size(&self, cx: &WindowContext) -> Pixels {
1369 let settings = AssistantSettings::get_global(cx);
1370 match self.position(cx) {
1371 DockPosition::Left | DockPosition::Right => {
1372 self.width.unwrap_or(settings.default_width)
1373 }
1374 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1375 }
1376 }
1377
1378 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1379 match self.position(cx) {
1380 DockPosition::Left | DockPosition::Right => self.width = size,
1381 DockPosition::Bottom => self.height = size,
1382 }
1383 cx.notify();
1384 }
1385
1386 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1387 self.pane.read(cx).is_zoomed()
1388 }
1389
1390 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1391 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1392 }
1393
1394 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1395 if active {
1396 if self.pane.read(cx).items_len() == 0 {
1397 self.new_context(cx);
1398 }
1399
1400 self.ensure_authenticated(cx);
1401 }
1402 }
1403
1404 fn pane(&self) -> Option<View<Pane>> {
1405 Some(self.pane.clone())
1406 }
1407
1408 fn remote_id() -> Option<proto::PanelId> {
1409 Some(proto::PanelId::AssistantPanel)
1410 }
1411
1412 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1413 let settings = AssistantSettings::get_global(cx);
1414 if !settings.enabled || !settings.button {
1415 return None;
1416 }
1417
1418 Some(IconName::ZedAssistant)
1419 }
1420
1421 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1422 Some("Assistant Panel")
1423 }
1424
1425 fn toggle_action(&self) -> Box<dyn Action> {
1426 Box::new(ToggleFocus)
1427 }
1428}
1429
1430impl EventEmitter<PanelEvent> for AssistantPanel {}
1431impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1432
1433impl FocusableView for AssistantPanel {
1434 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1435 self.pane.focus_handle(cx)
1436 }
1437}
1438
1439pub enum ContextEditorEvent {
1440 Edited,
1441 TabContentChanged,
1442}
1443
1444#[derive(Copy, Clone, Debug, PartialEq)]
1445struct ScrollPosition {
1446 offset_before_cursor: gpui::Point<f32>,
1447 cursor: Anchor,
1448}
1449
1450struct PatchViewState {
1451 footer_block_id: CustomBlockId,
1452 crease_id: CreaseId,
1453 editor: Option<PatchEditorState>,
1454 update_task: Option<Task<()>>,
1455}
1456
1457struct PatchEditorState {
1458 editor: WeakView<ProposedChangesEditor>,
1459 opened_patch: AssistantPatch,
1460}
1461
1462type MessageHeader = MessageMetadata;
1463
1464#[derive(Clone)]
1465enum AssistError {
1466 FileRequired,
1467 PaymentRequired,
1468 MaxMonthlySpendReached,
1469 Message(SharedString),
1470}
1471
1472pub struct ContextEditor {
1473 context: Model<Context>,
1474 fs: Arc<dyn Fs>,
1475 workspace: WeakView<Workspace>,
1476 project: Model<Project>,
1477 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1478 editor: View<Editor>,
1479 blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
1480 image_blocks: HashSet<CustomBlockId>,
1481 scroll_position: Option<ScrollPosition>,
1482 remote_id: Option<workspace::ViewId>,
1483 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
1484 pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
1485 pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
1486 _subscriptions: Vec<Subscription>,
1487 patches: HashMap<Range<language::Anchor>, PatchViewState>,
1488 active_patch: Option<Range<language::Anchor>>,
1489 assistant_panel: WeakView<AssistantPanel>,
1490 last_error: Option<AssistError>,
1491 show_accept_terms: bool,
1492 pub(crate) slash_menu_handle:
1493 PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
1494 // dragged_file_worktrees is used to keep references to worktrees that were added
1495 // when the user drag/dropped an external file onto the context editor. Since
1496 // the worktree is not part of the project panel, it would be dropped as soon as
1497 // the file is opened. In order to keep the worktree alive for the duration of the
1498 // context editor, we keep a reference here.
1499 dragged_file_worktrees: Vec<Model<Worktree>>,
1500}
1501
1502const DEFAULT_TAB_TITLE: &str = "New Context";
1503const MAX_TAB_TITLE_LEN: usize = 16;
1504
1505impl ContextEditor {
1506 fn for_context(
1507 context: Model<Context>,
1508 fs: Arc<dyn Fs>,
1509 workspace: WeakView<Workspace>,
1510 project: Model<Project>,
1511 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1512 assistant_panel: WeakView<AssistantPanel>,
1513 cx: &mut ViewContext<Self>,
1514 ) -> Self {
1515 let completion_provider = SlashCommandCompletionProvider::new(
1516 Some(cx.view().downgrade()),
1517 Some(workspace.clone()),
1518 );
1519
1520 let editor = cx.new_view(|cx| {
1521 let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
1522 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1523 editor.set_show_line_numbers(false, cx);
1524 editor.set_show_git_diff_gutter(false, cx);
1525 editor.set_show_code_actions(false, cx);
1526 editor.set_show_runnables(false, cx);
1527 editor.set_show_wrap_guides(false, cx);
1528 editor.set_show_indent_guides(false, cx);
1529 editor.set_completion_provider(Some(Box::new(completion_provider)));
1530 editor.set_collaboration_hub(Box::new(project.clone()));
1531 editor
1532 });
1533
1534 let _subscriptions = vec![
1535 cx.observe(&context, |_, _, cx| cx.notify()),
1536 cx.subscribe(&context, Self::handle_context_event),
1537 cx.subscribe(&editor, Self::handle_editor_event),
1538 cx.subscribe(&editor, Self::handle_editor_search_event),
1539 ];
1540
1541 let sections = context.read(cx).slash_command_output_sections().to_vec();
1542 let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
1543 let mut this = Self {
1544 context,
1545 editor,
1546 lsp_adapter_delegate,
1547 blocks: Default::default(),
1548 image_blocks: Default::default(),
1549 scroll_position: None,
1550 remote_id: None,
1551 fs,
1552 workspace,
1553 project,
1554 pending_slash_command_creases: HashMap::default(),
1555 pending_slash_command_blocks: HashMap::default(),
1556 pending_tool_use_creases: HashMap::default(),
1557 _subscriptions,
1558 patches: HashMap::default(),
1559 active_patch: None,
1560 assistant_panel,
1561 last_error: None,
1562 show_accept_terms: false,
1563 slash_menu_handle: Default::default(),
1564 dragged_file_worktrees: Vec::new(),
1565 };
1566 this.update_message_headers(cx);
1567 this.update_image_blocks(cx);
1568 this.insert_slash_command_output_sections(sections, false, cx);
1569 this.patches_updated(&Vec::new(), &patch_ranges, cx);
1570 this
1571 }
1572
1573 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
1574 let command_name = DefaultSlashCommand.name();
1575 self.editor.update(cx, |editor, cx| {
1576 editor.insert(&format!("/{command_name}\n\n"), cx)
1577 });
1578 let command = self.context.update(cx, |context, cx| {
1579 context.reparse(cx);
1580 context.pending_slash_commands()[0].clone()
1581 });
1582 self.run_command(
1583 command.source_range,
1584 &command.name,
1585 &command.arguments,
1586 false,
1587 false,
1588 self.workspace.clone(),
1589 cx,
1590 );
1591 }
1592
1593 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1594 self.send_to_model(RequestType::Chat, cx);
1595 }
1596
1597 fn edit(&mut self, _: &Edit, cx: &mut ViewContext<Self>) {
1598 self.send_to_model(RequestType::SuggestEdits, cx);
1599 }
1600
1601 fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
1602 if let Some((_range, patch)) = self.active_patch() {
1603 if let Some(editor) = patch
1604 .editor
1605 .as_ref()
1606 .and_then(|state| state.editor.upgrade())
1607 {
1608 cx.focus_view(&editor);
1609 return true;
1610 }
1611 }
1612
1613 false
1614 }
1615
1616 fn send_to_model(&mut self, request_type: RequestType, cx: &mut ViewContext<Self>) {
1617 let provider = LanguageModelRegistry::read_global(cx).active_provider();
1618 if provider
1619 .as_ref()
1620 .map_or(false, |provider| provider.must_accept_terms(cx))
1621 {
1622 self.show_accept_terms = true;
1623 cx.notify();
1624 return;
1625 }
1626
1627 if self.focus_active_patch(cx) {
1628 return;
1629 }
1630
1631 self.last_error = None;
1632
1633 if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
1634 self.last_error = Some(AssistError::FileRequired);
1635 cx.notify();
1636 } else if let Some(user_message) = self
1637 .context
1638 .update(cx, |context, cx| context.assist(request_type, cx))
1639 {
1640 let new_selection = {
1641 let cursor = user_message
1642 .start
1643 .to_offset(self.context.read(cx).buffer().read(cx));
1644 cursor..cursor
1645 };
1646 self.editor.update(cx, |editor, cx| {
1647 editor.change_selections(
1648 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1649 cx,
1650 |selections| selections.select_ranges([new_selection]),
1651 );
1652 });
1653 // Avoid scrolling to the new cursor position so the assistant's output is stable.
1654 cx.defer(|this, _| this.scroll_position = None);
1655 }
1656
1657 cx.notify();
1658 }
1659
1660 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1661 self.last_error = None;
1662
1663 if self
1664 .context
1665 .update(cx, |context, cx| context.cancel_last_assist(cx))
1666 {
1667 return;
1668 }
1669
1670 cx.propagate();
1671 }
1672
1673 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
1674 let cursors = self.cursors(cx);
1675 self.context.update(cx, |context, cx| {
1676 let messages = context
1677 .messages_for_offsets(cursors, cx)
1678 .into_iter()
1679 .map(|message| message.id)
1680 .collect();
1681 context.cycle_message_roles(messages, cx)
1682 });
1683 }
1684
1685 fn cursors(&self, cx: &mut WindowContext) -> Vec<usize> {
1686 let selections = self
1687 .editor
1688 .update(cx, |editor, cx| editor.selections.all::<usize>(cx));
1689 selections
1690 .into_iter()
1691 .map(|selection| selection.head())
1692 .collect()
1693 }
1694
1695 pub fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
1696 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1697 self.editor.update(cx, |editor, cx| {
1698 editor.transact(cx, |editor, cx| {
1699 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
1700 let snapshot = editor.buffer().read(cx).snapshot(cx);
1701 let newest_cursor = editor.selections.newest::<Point>(cx).head();
1702 if newest_cursor.column > 0
1703 || snapshot
1704 .chars_at(newest_cursor)
1705 .next()
1706 .map_or(false, |ch| ch != '\n')
1707 {
1708 editor.move_to_end_of_line(
1709 &MoveToEndOfLine {
1710 stop_at_soft_wraps: false,
1711 },
1712 cx,
1713 );
1714 editor.newline(&Newline, cx);
1715 }
1716
1717 editor.insert(&format!("/{name}"), cx);
1718 if command.accepts_arguments() {
1719 editor.insert(" ", cx);
1720 editor.show_completions(&ShowCompletions::default(), cx);
1721 }
1722 });
1723 });
1724 if !command.requires_argument() {
1725 self.confirm_command(&ConfirmCommand, cx);
1726 }
1727 }
1728 }
1729
1730 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
1731 if self.editor.read(cx).has_active_completions_menu() {
1732 return;
1733 }
1734
1735 let selections = self.editor.read(cx).selections.disjoint_anchors();
1736 let mut commands_by_range = HashMap::default();
1737 let workspace = self.workspace.clone();
1738 self.context.update(cx, |context, cx| {
1739 context.reparse(cx);
1740 for selection in selections.iter() {
1741 if let Some(command) =
1742 context.pending_command_for_position(selection.head().text_anchor, cx)
1743 {
1744 commands_by_range
1745 .entry(command.source_range.clone())
1746 .or_insert_with(|| command.clone());
1747 }
1748 }
1749 });
1750
1751 if commands_by_range.is_empty() {
1752 cx.propagate();
1753 } else {
1754 for command in commands_by_range.into_values() {
1755 self.run_command(
1756 command.source_range,
1757 &command.name,
1758 &command.arguments,
1759 true,
1760 false,
1761 workspace.clone(),
1762 cx,
1763 );
1764 }
1765 cx.stop_propagation();
1766 }
1767 }
1768
1769 #[allow(clippy::too_many_arguments)]
1770 pub fn run_command(
1771 &mut self,
1772 command_range: Range<language::Anchor>,
1773 name: &str,
1774 arguments: &[String],
1775 ensure_trailing_newline: bool,
1776 expand_result: bool,
1777 workspace: WeakView<Workspace>,
1778 cx: &mut ViewContext<Self>,
1779 ) {
1780 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1781 let context = self.context.read(cx);
1782 let sections = context
1783 .slash_command_output_sections()
1784 .into_iter()
1785 .filter(|section| section.is_valid(context.buffer().read(cx)))
1786 .cloned()
1787 .collect::<Vec<_>>();
1788 let snapshot = context.buffer().read(cx).snapshot();
1789 let output = command.run(
1790 arguments,
1791 §ions,
1792 snapshot,
1793 workspace,
1794 self.lsp_adapter_delegate.clone(),
1795 cx,
1796 );
1797 self.context.update(cx, |context, cx| {
1798 context.insert_command_output(
1799 command_range,
1800 output,
1801 ensure_trailing_newline,
1802 expand_result,
1803 cx,
1804 )
1805 });
1806 }
1807 }
1808
1809 fn handle_context_event(
1810 &mut self,
1811 _: Model<Context>,
1812 event: &ContextEvent,
1813 cx: &mut ViewContext<Self>,
1814 ) {
1815 let context_editor = cx.view().downgrade();
1816
1817 match event {
1818 ContextEvent::MessagesEdited => {
1819 self.update_message_headers(cx);
1820 self.update_image_blocks(cx);
1821 self.context.update(cx, |context, cx| {
1822 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1823 });
1824 }
1825 ContextEvent::SummaryChanged => {
1826 cx.emit(EditorEvent::TitleChanged);
1827 self.context.update(cx, |context, cx| {
1828 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1829 });
1830 }
1831 ContextEvent::StreamedCompletion => {
1832 self.editor.update(cx, |editor, cx| {
1833 if let Some(scroll_position) = self.scroll_position {
1834 let snapshot = editor.snapshot(cx);
1835 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1836 let scroll_top =
1837 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
1838 editor.set_scroll_position(
1839 point(scroll_position.offset_before_cursor.x, scroll_top),
1840 cx,
1841 );
1842 }
1843
1844 let new_tool_uses = self
1845 .context
1846 .read(cx)
1847 .pending_tool_uses()
1848 .into_iter()
1849 .filter(|tool_use| {
1850 !self
1851 .pending_tool_use_creases
1852 .contains_key(&tool_use.source_range)
1853 })
1854 .cloned()
1855 .collect::<Vec<_>>();
1856
1857 let buffer = editor.buffer().read(cx).snapshot(cx);
1858 let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
1859 let excerpt_id = *excerpt_id;
1860
1861 let mut buffer_rows_to_fold = BTreeSet::new();
1862
1863 let creases = new_tool_uses
1864 .iter()
1865 .map(|tool_use| {
1866 let placeholder = FoldPlaceholder {
1867 render: render_fold_icon_button(
1868 cx.view().downgrade(),
1869 IconName::PocketKnife,
1870 tool_use.name.clone().into(),
1871 ),
1872 constrain_width: false,
1873 merge_adjacent: false,
1874 };
1875 let render_trailer =
1876 move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
1877
1878 let start = buffer
1879 .anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
1880 .unwrap();
1881 let end = buffer
1882 .anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
1883 .unwrap();
1884
1885 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1886 buffer_rows_to_fold.insert(buffer_row);
1887
1888 self.context.update(cx, |context, cx| {
1889 context.insert_content(
1890 Content::ToolUse {
1891 range: tool_use.source_range.clone(),
1892 tool_use: LanguageModelToolUse {
1893 id: tool_use.id.to_string(),
1894 name: tool_use.name.clone(),
1895 input: tool_use.input.clone(),
1896 },
1897 },
1898 cx,
1899 );
1900 });
1901
1902 Crease::new(
1903 start..end,
1904 placeholder,
1905 fold_toggle("tool-use"),
1906 render_trailer,
1907 )
1908 })
1909 .collect::<Vec<_>>();
1910
1911 let crease_ids = editor.insert_creases(creases, cx);
1912
1913 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1914 editor.fold_at(&FoldAt { buffer_row }, cx);
1915 }
1916
1917 self.pending_tool_use_creases.extend(
1918 new_tool_uses
1919 .iter()
1920 .map(|tool_use| tool_use.source_range.clone())
1921 .zip(crease_ids),
1922 );
1923 });
1924 }
1925 ContextEvent::PatchesUpdated { removed, updated } => {
1926 self.patches_updated(removed, updated, cx);
1927 }
1928 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
1929 self.editor.update(cx, |editor, cx| {
1930 let buffer = editor.buffer().read(cx).snapshot(cx);
1931 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
1932 let excerpt_id = *excerpt_id;
1933
1934 editor.remove_creases(
1935 removed
1936 .iter()
1937 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
1938 cx,
1939 );
1940
1941 editor.remove_blocks(
1942 HashSet::from_iter(
1943 removed.iter().filter_map(|range| {
1944 self.pending_slash_command_blocks.remove(range)
1945 }),
1946 ),
1947 None,
1948 cx,
1949 );
1950
1951 let crease_ids = editor.insert_creases(
1952 updated.iter().map(|command| {
1953 let workspace = self.workspace.clone();
1954 let confirm_command = Arc::new({
1955 let context_editor = context_editor.clone();
1956 let command = command.clone();
1957 move |cx: &mut WindowContext| {
1958 context_editor
1959 .update(cx, |context_editor, cx| {
1960 context_editor.run_command(
1961 command.source_range.clone(),
1962 &command.name,
1963 &command.arguments,
1964 false,
1965 false,
1966 workspace.clone(),
1967 cx,
1968 );
1969 })
1970 .ok();
1971 }
1972 });
1973 let placeholder = FoldPlaceholder {
1974 render: Arc::new(move |_, _, _| Empty.into_any()),
1975 constrain_width: false,
1976 merge_adjacent: false,
1977 };
1978 let render_toggle = {
1979 let confirm_command = confirm_command.clone();
1980 let command = command.clone();
1981 move |row, _, _, _cx: &mut WindowContext| {
1982 render_pending_slash_command_gutter_decoration(
1983 row,
1984 &command.status,
1985 confirm_command.clone(),
1986 )
1987 }
1988 };
1989 let render_trailer = {
1990 let command = command.clone();
1991 move |row, _unfold, cx: &mut WindowContext| {
1992 // TODO: In the future we should investigate how we can expose
1993 // this as a hook on the `SlashCommand` trait so that we don't
1994 // need to special-case it here.
1995 if command.name == DocsSlashCommand::NAME {
1996 return render_docs_slash_command_trailer(
1997 row,
1998 command.clone(),
1999 cx,
2000 );
2001 }
2002
2003 Empty.into_any()
2004 }
2005 };
2006
2007 let start = buffer
2008 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2009 .unwrap();
2010 let end = buffer
2011 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2012 .unwrap();
2013 Crease::new(start..end, placeholder, render_toggle, render_trailer)
2014 }),
2015 cx,
2016 );
2017
2018 let block_ids = editor.insert_blocks(
2019 updated
2020 .iter()
2021 .filter_map(|command| match &command.status {
2022 PendingSlashCommandStatus::Error(error) => {
2023 Some((command, error.clone()))
2024 }
2025 _ => None,
2026 })
2027 .map(|(command, error_message)| BlockProperties {
2028 style: BlockStyle::Fixed,
2029 height: 1,
2030 placement: BlockPlacement::Below(Anchor {
2031 buffer_id: Some(buffer_id),
2032 excerpt_id,
2033 text_anchor: command.source_range.start,
2034 }),
2035 render: slash_command_error_block_renderer(error_message),
2036 priority: 0,
2037 }),
2038 None,
2039 cx,
2040 );
2041
2042 self.pending_slash_command_creases.extend(
2043 updated
2044 .iter()
2045 .map(|command| command.source_range.clone())
2046 .zip(crease_ids),
2047 );
2048
2049 self.pending_slash_command_blocks.extend(
2050 updated
2051 .iter()
2052 .map(|command| command.source_range.clone())
2053 .zip(block_ids),
2054 );
2055 })
2056 }
2057 ContextEvent::SlashCommandFinished {
2058 output_range,
2059 sections,
2060 run_commands_in_output,
2061 expand_result,
2062 } => {
2063 self.insert_slash_command_output_sections(
2064 sections.iter().cloned(),
2065 *expand_result,
2066 cx,
2067 );
2068
2069 if *run_commands_in_output {
2070 let commands = self.context.update(cx, |context, cx| {
2071 context.reparse(cx);
2072 context
2073 .pending_commands_for_range(output_range.clone(), cx)
2074 .to_vec()
2075 });
2076
2077 for command in commands {
2078 self.run_command(
2079 command.source_range,
2080 &command.name,
2081 &command.arguments,
2082 false,
2083 false,
2084 self.workspace.clone(),
2085 cx,
2086 );
2087 }
2088 }
2089 }
2090 ContextEvent::UsePendingTools => {
2091 let pending_tool_uses = self
2092 .context
2093 .read(cx)
2094 .pending_tool_uses()
2095 .into_iter()
2096 .filter(|tool_use| tool_use.status.is_idle())
2097 .cloned()
2098 .collect::<Vec<_>>();
2099
2100 for tool_use in pending_tool_uses {
2101 let tool_registry = ToolRegistry::global(cx);
2102 if let Some(tool) = tool_registry.tool(&tool_use.name) {
2103 let task = tool.run(tool_use.input, self.workspace.clone(), cx);
2104
2105 self.context.update(cx, |context, cx| {
2106 context.insert_tool_output(tool_use.id.clone(), task, cx);
2107 });
2108 }
2109 }
2110 }
2111 ContextEvent::ToolFinished {
2112 tool_use_id,
2113 output_range,
2114 } => {
2115 self.editor.update(cx, |editor, cx| {
2116 let buffer = editor.buffer().read(cx).snapshot(cx);
2117 let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
2118 let excerpt_id = *excerpt_id;
2119
2120 let placeholder = FoldPlaceholder {
2121 render: render_fold_icon_button(
2122 cx.view().downgrade(),
2123 IconName::PocketKnife,
2124 format!("Tool Result: {tool_use_id}").into(),
2125 ),
2126 constrain_width: false,
2127 merge_adjacent: false,
2128 };
2129 let render_trailer =
2130 move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
2131
2132 let start = buffer
2133 .anchor_in_excerpt(excerpt_id, output_range.start)
2134 .unwrap();
2135 let end = buffer
2136 .anchor_in_excerpt(excerpt_id, output_range.end)
2137 .unwrap();
2138
2139 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2140
2141 let crease = Crease::new(
2142 start..end,
2143 placeholder,
2144 fold_toggle("tool-use"),
2145 render_trailer,
2146 );
2147
2148 editor.insert_creases([crease], cx);
2149 editor.fold_at(&FoldAt { buffer_row }, cx);
2150 });
2151 }
2152 ContextEvent::Operation(_) => {}
2153 ContextEvent::ShowAssistError(error_message) => {
2154 self.last_error = Some(AssistError::Message(error_message.clone()));
2155 }
2156 ContextEvent::ShowPaymentRequiredError => {
2157 self.last_error = Some(AssistError::PaymentRequired);
2158 }
2159 ContextEvent::ShowMaxMonthlySpendReachedError => {
2160 self.last_error = Some(AssistError::MaxMonthlySpendReached);
2161 }
2162 }
2163 }
2164
2165 fn patches_updated(
2166 &mut self,
2167 removed: &Vec<Range<text::Anchor>>,
2168 updated: &Vec<Range<text::Anchor>>,
2169 cx: &mut ViewContext<ContextEditor>,
2170 ) {
2171 let this = cx.view().downgrade();
2172 let mut removed_crease_ids = Vec::new();
2173 let mut removed_block_ids = HashSet::default();
2174 let mut editors_to_close = Vec::new();
2175 for range in removed {
2176 if let Some(state) = self.patches.remove(range) {
2177 editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
2178 removed_block_ids.insert(state.footer_block_id);
2179 removed_crease_ids.push(state.crease_id);
2180 }
2181 }
2182
2183 self.editor.update(cx, |editor, cx| {
2184 let snapshot = editor.snapshot(cx);
2185 let multibuffer = &snapshot.buffer_snapshot;
2186 let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
2187
2188 let mut replaced_blocks = HashMap::default();
2189 for range in updated {
2190 let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
2191 continue;
2192 };
2193
2194 let path_count = patch.path_count();
2195 let patch_start = multibuffer
2196 .anchor_in_excerpt(excerpt_id, patch.range.start)
2197 .unwrap();
2198 let patch_end = multibuffer
2199 .anchor_in_excerpt(excerpt_id, patch.range.end)
2200 .unwrap();
2201 let render_block: RenderBlock = Box::new({
2202 let this = this.clone();
2203 let patch_range = range.clone();
2204 move |cx: &mut BlockContext<'_, '_>| {
2205 let max_width = cx.max_width;
2206 let gutter_width = cx.gutter_dimensions.full_width();
2207 let block_id = cx.block_id;
2208 this.update(&mut **cx, |this, cx| {
2209 this.render_patch_footer(
2210 patch_range.clone(),
2211 max_width,
2212 gutter_width,
2213 block_id,
2214 cx,
2215 )
2216 })
2217 .ok()
2218 .flatten()
2219 .unwrap_or_else(|| Empty.into_any())
2220 }
2221 });
2222
2223 let header_placeholder = FoldPlaceholder {
2224 render: {
2225 let this = this.clone();
2226 let patch_range = range.clone();
2227 Arc::new(move |fold_id, _range, cx| {
2228 this.update(cx, |this, cx| {
2229 this.render_patch_header(patch_range.clone(), fold_id, cx)
2230 })
2231 .ok()
2232 .flatten()
2233 .unwrap_or_else(|| Empty.into_any())
2234 })
2235 },
2236 constrain_width: false,
2237 merge_adjacent: false,
2238 };
2239
2240 let should_refold;
2241 if let Some(state) = self.patches.get_mut(&range) {
2242 replaced_blocks.insert(state.footer_block_id, render_block);
2243 if let Some(editor_state) = &state.editor {
2244 if editor_state.opened_patch != patch {
2245 state.update_task = Some({
2246 let this = this.clone();
2247 cx.spawn(|_, cx| async move {
2248 Self::update_patch_editor(this.clone(), patch, cx)
2249 .await
2250 .log_err();
2251 })
2252 });
2253 }
2254 }
2255
2256 should_refold =
2257 snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
2258 } else {
2259 let block_ids = editor.insert_blocks(
2260 [BlockProperties {
2261 height: path_count as u32 + 1,
2262 style: BlockStyle::Flex,
2263 render: render_block,
2264 placement: BlockPlacement::Below(patch_start),
2265 priority: 0,
2266 }],
2267 None,
2268 cx,
2269 );
2270
2271 let new_crease_ids = editor.insert_creases(
2272 [Crease::new(
2273 patch_start..patch_end,
2274 header_placeholder.clone(),
2275 fold_toggle("patch-header"),
2276 |_, _, _| Empty.into_any_element(),
2277 )],
2278 cx,
2279 );
2280
2281 self.patches.insert(
2282 range.clone(),
2283 PatchViewState {
2284 footer_block_id: block_ids[0],
2285 crease_id: new_crease_ids[0],
2286 editor: None,
2287 update_task: None,
2288 },
2289 );
2290
2291 should_refold = true;
2292 }
2293
2294 if should_refold {
2295 editor.unfold_ranges([patch_start..patch_end], true, false, cx);
2296 editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
2297 }
2298 }
2299
2300 editor.remove_creases(removed_crease_ids, cx);
2301 editor.remove_blocks(removed_block_ids, None, cx);
2302 editor.replace_blocks(replaced_blocks, None, cx);
2303 });
2304
2305 for editor in editors_to_close {
2306 self.close_patch_editor(editor, cx);
2307 }
2308
2309 self.update_active_patch(cx);
2310 }
2311
2312 fn insert_slash_command_output_sections(
2313 &mut self,
2314 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2315 expand_result: bool,
2316 cx: &mut ViewContext<Self>,
2317 ) {
2318 self.editor.update(cx, |editor, cx| {
2319 let buffer = editor.buffer().read(cx).snapshot(cx);
2320 let excerpt_id = *buffer.as_singleton().unwrap().0;
2321 let mut buffer_rows_to_fold = BTreeSet::new();
2322 let mut creases = Vec::new();
2323 for section in sections {
2324 let start = buffer
2325 .anchor_in_excerpt(excerpt_id, section.range.start)
2326 .unwrap();
2327 let end = buffer
2328 .anchor_in_excerpt(excerpt_id, section.range.end)
2329 .unwrap();
2330 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2331 buffer_rows_to_fold.insert(buffer_row);
2332 creases.push(
2333 Crease::new(
2334 start..end,
2335 FoldPlaceholder {
2336 render: render_fold_icon_button(
2337 cx.view().downgrade(),
2338 section.icon,
2339 section.label.clone(),
2340 ),
2341 constrain_width: false,
2342 merge_adjacent: false,
2343 },
2344 render_slash_command_output_toggle,
2345 |_, _, _| Empty.into_any_element(),
2346 )
2347 .with_metadata(CreaseMetadata {
2348 icon: section.icon,
2349 label: section.label,
2350 }),
2351 );
2352 }
2353
2354 editor.insert_creases(creases, cx);
2355
2356 if expand_result {
2357 buffer_rows_to_fold.clear();
2358 }
2359 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2360 editor.fold_at(&FoldAt { buffer_row }, cx);
2361 }
2362 });
2363 }
2364
2365 fn handle_editor_event(
2366 &mut self,
2367 _: View<Editor>,
2368 event: &EditorEvent,
2369 cx: &mut ViewContext<Self>,
2370 ) {
2371 match event {
2372 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2373 let cursor_scroll_position = self.cursor_scroll_position(cx);
2374 if *autoscroll {
2375 self.scroll_position = cursor_scroll_position;
2376 } else if self.scroll_position != cursor_scroll_position {
2377 self.scroll_position = None;
2378 }
2379 }
2380 EditorEvent::SelectionsChanged { .. } => {
2381 self.scroll_position = self.cursor_scroll_position(cx);
2382 self.update_active_patch(cx);
2383 }
2384 _ => {}
2385 }
2386 cx.emit(event.clone());
2387 }
2388
2389 fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
2390 let patch = self.active_patch.as_ref()?;
2391 Some((patch.clone(), self.patches.get(&patch)?))
2392 }
2393
2394 fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
2395 let newest_cursor = self.editor.update(cx, |editor, cx| {
2396 editor.selections.newest::<Point>(cx).head()
2397 });
2398 let context = self.context.read(cx);
2399
2400 let new_patch = context.patch_containing(newest_cursor, cx).cloned();
2401
2402 if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
2403 return;
2404 }
2405
2406 if let Some(old_patch_range) = self.active_patch.take() {
2407 if let Some(patch_state) = self.patches.get_mut(&old_patch_range) {
2408 if let Some(state) = patch_state.editor.take() {
2409 if let Some(editor) = state.editor.upgrade() {
2410 self.close_patch_editor(editor, cx);
2411 }
2412 }
2413 }
2414 }
2415
2416 if let Some(new_patch) = new_patch {
2417 self.active_patch = Some(new_patch.range.clone());
2418
2419 if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
2420 let mut editor = None;
2421 if let Some(state) = &patch_state.editor {
2422 if let Some(opened_editor) = state.editor.upgrade() {
2423 editor = Some(opened_editor);
2424 }
2425 }
2426
2427 if let Some(editor) = editor {
2428 self.workspace
2429 .update(cx, |workspace, cx| {
2430 workspace.activate_item(&editor, true, false, cx);
2431 })
2432 .ok();
2433 } else {
2434 patch_state.update_task = Some(cx.spawn(move |this, cx| async move {
2435 Self::open_patch_editor(this, new_patch, cx).await.log_err();
2436 }));
2437 }
2438 }
2439 }
2440 }
2441
2442 fn close_patch_editor(
2443 &mut self,
2444 editor: View<ProposedChangesEditor>,
2445 cx: &mut ViewContext<ContextEditor>,
2446 ) {
2447 self.workspace
2448 .update(cx, |workspace, cx| {
2449 if let Some(pane) = workspace.pane_for(&editor) {
2450 pane.update(cx, |pane, cx| {
2451 let item_id = editor.entity_id();
2452 if !editor.read(cx).focus_handle(cx).is_focused(cx) {
2453 pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
2454 .detach_and_log_err(cx);
2455 }
2456 });
2457 }
2458 })
2459 .ok();
2460 }
2461
2462 async fn open_patch_editor(
2463 this: WeakView<Self>,
2464 patch: AssistantPatch,
2465 mut cx: AsyncWindowContext,
2466 ) -> Result<()> {
2467 let project = this.update(&mut cx, |this, _| this.project.clone())?;
2468 let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
2469
2470 let editor = cx.new_view(|cx| {
2471 let editor = ProposedChangesEditor::new(
2472 patch.title.clone(),
2473 resolved_patch
2474 .edit_groups
2475 .iter()
2476 .map(|(buffer, groups)| ProposedChangeLocation {
2477 buffer: buffer.clone(),
2478 ranges: groups
2479 .iter()
2480 .map(|group| group.context_range.clone())
2481 .collect(),
2482 })
2483 .collect(),
2484 Some(project.clone()),
2485 cx,
2486 );
2487 resolved_patch.apply(&editor, cx);
2488 editor
2489 })?;
2490
2491 this.update(&mut cx, |this, cx| {
2492 if let Some(patch_state) = this.patches.get_mut(&patch.range) {
2493 patch_state.editor = Some(PatchEditorState {
2494 editor: editor.downgrade(),
2495 opened_patch: patch,
2496 });
2497 patch_state.update_task.take();
2498 }
2499
2500 this.workspace
2501 .update(cx, |workspace, cx| {
2502 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2503 })
2504 .log_err();
2505 })?;
2506
2507 Ok(())
2508 }
2509
2510 async fn update_patch_editor(
2511 this: WeakView<Self>,
2512 patch: AssistantPatch,
2513 mut cx: AsyncWindowContext,
2514 ) -> Result<()> {
2515 let project = this.update(&mut cx, |this, _| this.project.clone())?;
2516 let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
2517 this.update(&mut cx, |this, cx| {
2518 let patch_state = this.patches.get_mut(&patch.range)?;
2519
2520 let locations = resolved_patch
2521 .edit_groups
2522 .iter()
2523 .map(|(buffer, groups)| ProposedChangeLocation {
2524 buffer: buffer.clone(),
2525 ranges: groups
2526 .iter()
2527 .map(|group| group.context_range.clone())
2528 .collect(),
2529 })
2530 .collect();
2531
2532 if let Some(state) = &mut patch_state.editor {
2533 if let Some(editor) = state.editor.upgrade() {
2534 editor.update(cx, |editor, cx| {
2535 editor.set_title(patch.title.clone(), cx);
2536 editor.reset_locations(locations, cx);
2537 resolved_patch.apply(editor, cx);
2538 });
2539
2540 state.opened_patch = patch;
2541 } else {
2542 patch_state.editor.take();
2543 }
2544 }
2545 patch_state.update_task.take();
2546
2547 Some(())
2548 })?;
2549 Ok(())
2550 }
2551
2552 fn handle_editor_search_event(
2553 &mut self,
2554 _: View<Editor>,
2555 event: &SearchEvent,
2556 cx: &mut ViewContext<Self>,
2557 ) {
2558 cx.emit(event.clone());
2559 }
2560
2561 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2562 self.editor.update(cx, |editor, cx| {
2563 let snapshot = editor.snapshot(cx);
2564 let cursor = editor.selections.newest_anchor().head();
2565 let cursor_row = cursor
2566 .to_display_point(&snapshot.display_snapshot)
2567 .row()
2568 .as_f32();
2569 let scroll_position = editor
2570 .scroll_manager
2571 .anchor()
2572 .scroll_position(&snapshot.display_snapshot);
2573
2574 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2575 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2576 Some(ScrollPosition {
2577 cursor,
2578 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2579 })
2580 } else {
2581 None
2582 }
2583 })
2584 }
2585
2586 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2587 self.editor.update(cx, |editor, cx| {
2588 let buffer = editor.buffer().read(cx).snapshot(cx);
2589
2590 let excerpt_id = *buffer.as_singleton().unwrap().0;
2591 let mut old_blocks = std::mem::take(&mut self.blocks);
2592 let mut blocks_to_remove: HashMap<_, _> = old_blocks
2593 .iter()
2594 .map(|(message_id, (_, block_id))| (*message_id, *block_id))
2595 .collect();
2596 let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
2597
2598 let render_block = |message: MessageMetadata| -> RenderBlock {
2599 Box::new({
2600 let context = self.context.clone();
2601 move |cx| {
2602 let message_id = MessageId(message.timestamp);
2603 let llm_loading = message.role == Role::Assistant
2604 && message.status == MessageStatus::Pending;
2605
2606 let (label, spinner, note) = match message.role {
2607 Role::User => (
2608 Label::new("You").color(Color::Default).into_any_element(),
2609 None,
2610 None,
2611 ),
2612 Role::Assistant => {
2613 let base_label = Label::new("Assistant").color(Color::Info);
2614 let mut spinner = None;
2615 let mut note = None;
2616 let animated_label = if llm_loading {
2617 base_label
2618 .with_animation(
2619 "pulsating-label",
2620 Animation::new(Duration::from_secs(2))
2621 .repeat()
2622 .with_easing(pulsating_between(0.3, 0.9)),
2623 |label, delta| label.alpha(delta),
2624 )
2625 .into_any_element()
2626 } else {
2627 base_label.into_any_element()
2628 };
2629 if llm_loading {
2630 spinner = Some(
2631 Icon::new(IconName::ArrowCircle)
2632 .size(IconSize::XSmall)
2633 .color(Color::Muted)
2634 .with_animation(
2635 "arrow-circle",
2636 Animation::new(Duration::from_secs(2)).repeat(),
2637 |icon, delta| {
2638 icon.transform(Transformation::rotate(
2639 percentage(delta),
2640 ))
2641 },
2642 )
2643 .into_any_element(),
2644 );
2645 note = Some(
2646 div()
2647 .font(
2648 theme::ThemeSettings::get_global(cx)
2649 .buffer_font
2650 .clone(),
2651 )
2652 .child(
2653 Label::new("Press 'esc' to cancel")
2654 .color(Color::Muted)
2655 .size(LabelSize::XSmall),
2656 )
2657 .into_any_element(),
2658 );
2659 }
2660 (animated_label, spinner, note)
2661 }
2662 Role::System => (
2663 Label::new("System")
2664 .color(Color::Warning)
2665 .into_any_element(),
2666 None,
2667 None,
2668 ),
2669 };
2670
2671 let sender = h_flex()
2672 .items_center()
2673 .gap_2()
2674 .child(
2675 ButtonLike::new("role")
2676 .style(ButtonStyle::Filled)
2677 .child(
2678 h_flex()
2679 .items_center()
2680 .gap_1p5()
2681 .child(label)
2682 .children(spinner),
2683 )
2684 .tooltip(|cx| {
2685 Tooltip::with_meta(
2686 "Toggle message role",
2687 None,
2688 "Available roles: You (User), Assistant, System",
2689 cx,
2690 )
2691 })
2692 .on_click({
2693 let context = context.clone();
2694 move |_, cx| {
2695 context.update(cx, |context, cx| {
2696 context.cycle_message_roles(
2697 HashSet::from_iter(Some(message_id)),
2698 cx,
2699 )
2700 })
2701 }
2702 }),
2703 )
2704 .children(note);
2705
2706 h_flex()
2707 .id(("message_header", message_id.as_u64()))
2708 .pl(cx.gutter_dimensions.full_width())
2709 .h_11()
2710 .w_full()
2711 .relative()
2712 .gap_1()
2713 .child(sender)
2714 .children(match &message.cache {
2715 Some(cache) if cache.is_final_anchor => match cache.status {
2716 CacheStatus::Cached => Some(
2717 div()
2718 .id("cached")
2719 .child(
2720 Icon::new(IconName::DatabaseZap)
2721 .size(IconSize::XSmall)
2722 .color(Color::Hint),
2723 )
2724 .tooltip(|cx| {
2725 Tooltip::with_meta(
2726 "Context cached",
2727 None,
2728 "Large messages cached to optimize performance",
2729 cx,
2730 )
2731 })
2732 .into_any_element(),
2733 ),
2734 CacheStatus::Pending => Some(
2735 div()
2736 .child(
2737 Icon::new(IconName::Ellipsis)
2738 .size(IconSize::XSmall)
2739 .color(Color::Hint),
2740 )
2741 .into_any_element(),
2742 ),
2743 },
2744 _ => None,
2745 })
2746 .children(match &message.status {
2747 MessageStatus::Error(error) => Some(
2748 Button::new("show-error", "Error")
2749 .color(Color::Error)
2750 .selected_label_color(Color::Error)
2751 .selected_icon_color(Color::Error)
2752 .icon(IconName::XCircle)
2753 .icon_color(Color::Error)
2754 .icon_size(IconSize::Small)
2755 .icon_position(IconPosition::Start)
2756 .tooltip(move |cx| {
2757 Tooltip::with_meta(
2758 "Error interacting with language model",
2759 None,
2760 "Click for more details",
2761 cx,
2762 )
2763 })
2764 .on_click({
2765 let context = context.clone();
2766 let error = error.clone();
2767 move |_, cx| {
2768 context.update(cx, |_, cx| {
2769 cx.emit(ContextEvent::ShowAssistError(
2770 error.clone(),
2771 ));
2772 });
2773 }
2774 })
2775 .into_any_element(),
2776 ),
2777 MessageStatus::Canceled => Some(
2778 ButtonLike::new("canceled")
2779 .child(Icon::new(IconName::XCircle).color(Color::Disabled))
2780 .child(
2781 Label::new("Canceled")
2782 .size(LabelSize::Small)
2783 .color(Color::Disabled),
2784 )
2785 .tooltip(move |cx| {
2786 Tooltip::with_meta(
2787 "Canceled",
2788 None,
2789 "Interaction with the assistant was canceled",
2790 cx,
2791 )
2792 })
2793 .into_any_element(),
2794 ),
2795 _ => None,
2796 })
2797 .into_any_element()
2798 }
2799 })
2800 };
2801 let create_block_properties = |message: &Message| BlockProperties {
2802 height: 2,
2803 style: BlockStyle::Sticky,
2804 placement: BlockPlacement::Above(
2805 buffer
2806 .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
2807 .unwrap(),
2808 ),
2809 priority: usize::MAX,
2810 render: render_block(MessageMetadata::from(message)),
2811 };
2812 let mut new_blocks = vec![];
2813 let mut block_index_to_message = vec![];
2814 for message in self.context.read(cx).messages(cx) {
2815 if let Some(_) = blocks_to_remove.remove(&message.id) {
2816 // This is an old message that we might modify.
2817 let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
2818 debug_assert!(
2819 false,
2820 "old_blocks should contain a message_id we've just removed."
2821 );
2822 continue;
2823 };
2824 // Should we modify it?
2825 let message_meta = MessageMetadata::from(&message);
2826 if meta != &message_meta {
2827 blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
2828 *meta = message_meta;
2829 }
2830 } else {
2831 // This is a new message.
2832 new_blocks.push(create_block_properties(&message));
2833 block_index_to_message.push((message.id, MessageMetadata::from(&message)));
2834 }
2835 }
2836 editor.replace_blocks(blocks_to_replace, None, cx);
2837 editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
2838
2839 let ids = editor.insert_blocks(new_blocks, None, cx);
2840 old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
2841 |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
2842 ));
2843 self.blocks = old_blocks;
2844 });
2845 }
2846
2847 /// Returns either the selected text, or the content of the Markdown code
2848 /// block surrounding the cursor.
2849 fn get_selection_or_code_block(
2850 context_editor_view: &View<ContextEditor>,
2851 cx: &mut ViewContext<Workspace>,
2852 ) -> Option<(String, bool)> {
2853 const CODE_FENCE_DELIMITER: &'static str = "```";
2854
2855 let context_editor = context_editor_view.read(cx).editor.clone();
2856 context_editor.update(cx, |context_editor, cx| {
2857 if context_editor.selections.newest::<Point>(cx).is_empty() {
2858 let snapshot = context_editor.buffer().read(cx).snapshot(cx);
2859 let (_, _, snapshot) = snapshot.as_singleton()?;
2860
2861 let head = context_editor.selections.newest::<Point>(cx).head();
2862 let offset = snapshot.point_to_offset(head);
2863
2864 let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
2865 let mut text = snapshot
2866 .text_for_range(surrounding_code_block_range)
2867 .collect::<String>();
2868
2869 // If there is no newline trailing the closing three-backticks, then
2870 // tree-sitter-md extends the range of the content node to include
2871 // the backticks.
2872 if text.ends_with(CODE_FENCE_DELIMITER) {
2873 text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
2874 }
2875
2876 (!text.is_empty()).then_some((text, true))
2877 } else {
2878 let anchor = context_editor.selections.newest_anchor();
2879 let text = context_editor
2880 .buffer()
2881 .read(cx)
2882 .read(cx)
2883 .text_for_range(anchor.range())
2884 .collect::<String>();
2885
2886 (!text.is_empty()).then_some((text, false))
2887 }
2888 })
2889 }
2890
2891 fn insert_selection(
2892 workspace: &mut Workspace,
2893 _: &InsertIntoEditor,
2894 cx: &mut ViewContext<Workspace>,
2895 ) {
2896 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2897 return;
2898 };
2899 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2900 return;
2901 };
2902 let Some(active_editor_view) = workspace
2903 .active_item(cx)
2904 .and_then(|item| item.act_as::<Editor>(cx))
2905 else {
2906 return;
2907 };
2908
2909 if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
2910 active_editor_view.update(cx, |editor, cx| {
2911 editor.insert(&text, cx);
2912 editor.focus(cx);
2913 })
2914 }
2915 }
2916
2917 fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext<Workspace>) {
2918 let result = maybe!({
2919 let panel = workspace.panel::<AssistantPanel>(cx)?;
2920 let context_editor_view = panel.read(cx).active_context_editor(cx)?;
2921 Self::get_selection_or_code_block(&context_editor_view, cx)
2922 });
2923 let Some((text, is_code_block)) = result else {
2924 return;
2925 };
2926
2927 cx.write_to_clipboard(ClipboardItem::new_string(text));
2928
2929 struct CopyToClipboardToast;
2930 workspace.show_toast(
2931 Toast::new(
2932 NotificationId::unique::<CopyToClipboardToast>(),
2933 format!(
2934 "{} copied to clipboard.",
2935 if is_code_block {
2936 "Code block"
2937 } else {
2938 "Selection"
2939 }
2940 ),
2941 )
2942 .autohide(),
2943 cx,
2944 );
2945 }
2946
2947 fn insert_dragged_files(
2948 workspace: &mut Workspace,
2949 action: &InsertDraggedFiles,
2950 cx: &mut ViewContext<Workspace>,
2951 ) {
2952 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2953 return;
2954 };
2955 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2956 return;
2957 };
2958
2959 let project = workspace.project().clone();
2960
2961 let paths = match action {
2962 InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
2963 InsertDraggedFiles::ExternalFiles(paths) => {
2964 let tasks = paths
2965 .clone()
2966 .into_iter()
2967 .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
2968 .collect::<Vec<_>>();
2969
2970 cx.spawn(move |_, cx| async move {
2971 let mut paths = vec![];
2972 let mut worktrees = vec![];
2973
2974 let opened_paths = futures::future::join_all(tasks).await;
2975 for (worktree, project_path) in opened_paths.into_iter().flatten() {
2976 let Ok(worktree_root_name) =
2977 worktree.read_with(&cx, |worktree, _| worktree.root_name().to_string())
2978 else {
2979 continue;
2980 };
2981
2982 let mut full_path = PathBuf::from(worktree_root_name.clone());
2983 full_path.push(&project_path.path);
2984 paths.push(full_path);
2985 worktrees.push(worktree);
2986 }
2987
2988 (paths, worktrees)
2989 })
2990 }
2991 };
2992
2993 cx.spawn(|_, mut cx| async move {
2994 let (paths, dragged_file_worktrees) = paths.await;
2995 let cmd_name = file_command::FileSlashCommand.name();
2996
2997 context_editor_view
2998 .update(&mut cx, |context_editor, cx| {
2999 let file_argument = paths
3000 .into_iter()
3001 .map(|path| path.to_string_lossy().to_string())
3002 .collect::<Vec<_>>()
3003 .join(" ");
3004
3005 context_editor.editor.update(cx, |editor, cx| {
3006 editor.insert("\n", cx);
3007 editor.insert(&format!("/{} {}", cmd_name, file_argument), cx);
3008 });
3009
3010 context_editor.confirm_command(&ConfirmCommand, cx);
3011
3012 context_editor
3013 .dragged_file_worktrees
3014 .extend(dragged_file_worktrees);
3015 })
3016 .log_err();
3017 })
3018 .detach();
3019 }
3020
3021 fn quote_selection(
3022 workspace: &mut Workspace,
3023 _: &QuoteSelection,
3024 cx: &mut ViewContext<Workspace>,
3025 ) {
3026 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
3027 return;
3028 };
3029
3030 let Some(creases) = selections_creases(workspace, cx) else {
3031 return;
3032 };
3033
3034 if creases.is_empty() {
3035 return;
3036 }
3037 // Activate the panel
3038 if !panel.focus_handle(cx).contains_focused(cx) {
3039 workspace.toggle_panel_focus::<AssistantPanel>(cx);
3040 }
3041
3042 panel.update(cx, |_, cx| {
3043 // Wait to create a new context until the workspace is no longer
3044 // being updated.
3045 cx.defer(move |panel, cx| {
3046 if let Some(context) = panel
3047 .active_context_editor(cx)
3048 .or_else(|| panel.new_context(cx))
3049 {
3050 context.update(cx, |context, cx| {
3051 context.editor.update(cx, |editor, cx| {
3052 editor.insert("\n", cx);
3053 for (text, crease_title) in creases {
3054 let point = editor.selections.newest::<Point>(cx).head();
3055 let start_row = MultiBufferRow(point.row);
3056
3057 editor.insert(&text, cx);
3058
3059 let snapshot = editor.buffer().read(cx).snapshot(cx);
3060 let anchor_before = snapshot.anchor_after(point);
3061 let anchor_after = editor
3062 .selections
3063 .newest_anchor()
3064 .head()
3065 .bias_left(&snapshot);
3066
3067 editor.insert("\n", cx);
3068
3069 let fold_placeholder = quote_selection_fold_placeholder(
3070 crease_title,
3071 cx.view().downgrade(),
3072 );
3073 let crease = Crease::new(
3074 anchor_before..anchor_after,
3075 fold_placeholder,
3076 render_quote_selection_output_toggle,
3077 |_, _, _| Empty.into_any(),
3078 );
3079 editor.insert_creases(vec![crease], cx);
3080 editor.fold_at(
3081 &FoldAt {
3082 buffer_row: start_row,
3083 },
3084 cx,
3085 );
3086 }
3087 })
3088 });
3089 };
3090 });
3091 });
3092 }
3093
3094 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
3095 if self.editor.read(cx).selections.count() == 1 {
3096 let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
3097 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
3098 copied_text,
3099 metadata,
3100 ));
3101 cx.stop_propagation();
3102 return;
3103 }
3104
3105 cx.propagate();
3106 }
3107
3108 fn cut(&mut self, _: &editor::actions::Cut, cx: &mut ViewContext<Self>) {
3109 if self.editor.read(cx).selections.count() == 1 {
3110 let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
3111
3112 self.editor.update(cx, |editor, cx| {
3113 editor.transact(cx, |this, cx| {
3114 this.change_selections(Some(Autoscroll::fit()), cx, |s| {
3115 s.select(selections);
3116 });
3117 this.insert("", cx);
3118 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
3119 copied_text,
3120 metadata,
3121 ));
3122 });
3123 });
3124
3125 cx.stop_propagation();
3126 return;
3127 }
3128
3129 cx.propagate();
3130 }
3131
3132 fn get_clipboard_contents(
3133 &mut self,
3134 cx: &mut ViewContext<Self>,
3135 ) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
3136 let (snapshot, selection, creases) = self.editor.update(cx, |editor, cx| {
3137 let mut selection = editor.selections.newest::<Point>(cx);
3138 let snapshot = editor.buffer().read(cx).snapshot(cx);
3139
3140 let is_entire_line = selection.is_empty() || editor.selections.line_mode;
3141 if is_entire_line {
3142 selection.start = Point::new(selection.start.row, 0);
3143 selection.end =
3144 cmp::min(snapshot.max_point(), Point::new(selection.start.row + 1, 0));
3145 selection.goal = SelectionGoal::None;
3146 }
3147
3148 let selection_start = snapshot.point_to_offset(selection.start);
3149
3150 (
3151 snapshot.clone(),
3152 selection.clone(),
3153 editor.display_map.update(cx, |display_map, cx| {
3154 display_map
3155 .snapshot(cx)
3156 .crease_snapshot
3157 .creases_in_range(
3158 MultiBufferRow(selection.start.row)
3159 ..MultiBufferRow(selection.end.row + 1),
3160 &snapshot,
3161 )
3162 .filter_map(|crease| {
3163 if let Some(metadata) = &crease.metadata {
3164 let start = crease
3165 .range
3166 .start
3167 .to_offset(&snapshot)
3168 .saturating_sub(selection_start);
3169 let end = crease
3170 .range
3171 .end
3172 .to_offset(&snapshot)
3173 .saturating_sub(selection_start);
3174
3175 let range_relative_to_selection = start..end;
3176
3177 if range_relative_to_selection.is_empty() {
3178 None
3179 } else {
3180 Some(SelectedCreaseMetadata {
3181 range_relative_to_selection,
3182 crease: metadata.clone(),
3183 })
3184 }
3185 } else {
3186 None
3187 }
3188 })
3189 .collect::<Vec<_>>()
3190 }),
3191 )
3192 });
3193
3194 let selection = selection.map(|point| snapshot.point_to_offset(point));
3195 let context = self.context.read(cx);
3196
3197 let mut text = String::new();
3198 for message in context.messages(cx) {
3199 if message.offset_range.start >= selection.range().end {
3200 break;
3201 } else if message.offset_range.end >= selection.range().start {
3202 let range = cmp::max(message.offset_range.start, selection.range().start)
3203 ..cmp::min(message.offset_range.end, selection.range().end);
3204 if !range.is_empty() {
3205 for chunk in context.buffer().read(cx).text_for_range(range) {
3206 text.push_str(chunk);
3207 }
3208 if message.offset_range.end < selection.range().end {
3209 text.push('\n');
3210 }
3211 }
3212 }
3213 }
3214
3215 (text, CopyMetadata { creases }, vec![selection])
3216 }
3217
3218 fn paste(&mut self, action: &editor::actions::Paste, cx: &mut ViewContext<Self>) {
3219 cx.stop_propagation();
3220
3221 let images = if let Some(item) = cx.read_from_clipboard() {
3222 item.into_entries()
3223 .filter_map(|entry| {
3224 if let ClipboardEntry::Image(image) = entry {
3225 Some(image)
3226 } else {
3227 None
3228 }
3229 })
3230 .collect()
3231 } else {
3232 Vec::new()
3233 };
3234
3235 let metadata = if let Some(item) = cx.read_from_clipboard() {
3236 item.entries().first().and_then(|entry| {
3237 if let ClipboardEntry::String(text) = entry {
3238 text.metadata_json::<CopyMetadata>()
3239 } else {
3240 None
3241 }
3242 })
3243 } else {
3244 None
3245 };
3246
3247 if images.is_empty() {
3248 self.editor.update(cx, |editor, cx| {
3249 let paste_position = editor.selections.newest::<usize>(cx).head();
3250 editor.paste(action, cx);
3251
3252 if let Some(metadata) = metadata {
3253 let buffer = editor.buffer().read(cx).snapshot(cx);
3254
3255 let mut buffer_rows_to_fold = BTreeSet::new();
3256 let weak_editor = cx.view().downgrade();
3257 editor.insert_creases(
3258 metadata.creases.into_iter().map(|metadata| {
3259 let start = buffer.anchor_after(
3260 paste_position + metadata.range_relative_to_selection.start,
3261 );
3262 let end = buffer.anchor_before(
3263 paste_position + metadata.range_relative_to_selection.end,
3264 );
3265
3266 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
3267 buffer_rows_to_fold.insert(buffer_row);
3268 Crease::new(
3269 start..end,
3270 FoldPlaceholder {
3271 constrain_width: false,
3272 render: render_fold_icon_button(
3273 weak_editor.clone(),
3274 metadata.crease.icon,
3275 metadata.crease.label.clone(),
3276 ),
3277 merge_adjacent: false,
3278 },
3279 render_slash_command_output_toggle,
3280 |_, _, _| Empty.into_any(),
3281 )
3282 .with_metadata(metadata.crease.clone())
3283 }),
3284 cx,
3285 );
3286 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
3287 editor.fold_at(&FoldAt { buffer_row }, cx);
3288 }
3289 }
3290 });
3291 } else {
3292 let mut image_positions = Vec::new();
3293 self.editor.update(cx, |editor, cx| {
3294 editor.transact(cx, |editor, cx| {
3295 let edits = editor
3296 .selections
3297 .all::<usize>(cx)
3298 .into_iter()
3299 .map(|selection| (selection.start..selection.end, "\n"));
3300 editor.edit(edits, cx);
3301
3302 let snapshot = editor.buffer().read(cx).snapshot(cx);
3303 for selection in editor.selections.all::<usize>(cx) {
3304 image_positions.push(snapshot.anchor_before(selection.end));
3305 }
3306 });
3307 });
3308
3309 self.context.update(cx, |context, cx| {
3310 for image in images {
3311 let Some(render_image) = image.to_image_data(cx).log_err() else {
3312 continue;
3313 };
3314 let image_id = image.id();
3315 let image_task = LanguageModelImage::from_image(image, cx).shared();
3316
3317 for image_position in image_positions.iter() {
3318 context.insert_content(
3319 Content::Image {
3320 anchor: image_position.text_anchor,
3321 image_id,
3322 image: image_task.clone(),
3323 render_image: render_image.clone(),
3324 },
3325 cx,
3326 );
3327 }
3328 }
3329 });
3330 }
3331 }
3332
3333 fn update_image_blocks(&mut self, cx: &mut ViewContext<Self>) {
3334 self.editor.update(cx, |editor, cx| {
3335 let buffer = editor.buffer().read(cx).snapshot(cx);
3336 let excerpt_id = *buffer.as_singleton().unwrap().0;
3337 let old_blocks = std::mem::take(&mut self.image_blocks);
3338 let new_blocks = self
3339 .context
3340 .read(cx)
3341 .contents(cx)
3342 .filter_map(|content| {
3343 if let Content::Image {
3344 anchor,
3345 render_image,
3346 ..
3347 } = content
3348 {
3349 Some((anchor, render_image))
3350 } else {
3351 None
3352 }
3353 })
3354 .filter_map(|(anchor, render_image)| {
3355 const MAX_HEIGHT_IN_LINES: u32 = 8;
3356 let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
3357 let image = render_image.clone();
3358 anchor.is_valid(&buffer).then(|| BlockProperties {
3359 placement: BlockPlacement::Above(anchor),
3360 height: MAX_HEIGHT_IN_LINES,
3361 style: BlockStyle::Sticky,
3362 render: Box::new(move |cx| {
3363 let image_size = size_for_image(
3364 &image,
3365 size(
3366 cx.max_width - cx.gutter_dimensions.full_width(),
3367 MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
3368 ),
3369 );
3370 h_flex()
3371 .pl(cx.gutter_dimensions.full_width())
3372 .child(
3373 img(image.clone())
3374 .object_fit(gpui::ObjectFit::ScaleDown)
3375 .w(image_size.width)
3376 .h(image_size.height),
3377 )
3378 .into_any_element()
3379 }),
3380 priority: 0,
3381 })
3382 })
3383 .collect::<Vec<_>>();
3384
3385 editor.remove_blocks(old_blocks, None, cx);
3386 let ids = editor.insert_blocks(new_blocks, None, cx);
3387 self.image_blocks = HashSet::from_iter(ids);
3388 });
3389 }
3390
3391 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
3392 self.context.update(cx, |context, cx| {
3393 let selections = self.editor.read(cx).selections.disjoint_anchors();
3394 for selection in selections.as_ref() {
3395 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
3396 let range = selection
3397 .map(|endpoint| endpoint.to_offset(&buffer))
3398 .range();
3399 context.split_message(range, cx);
3400 }
3401 });
3402 }
3403
3404 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3405 self.context.update(cx, |context, cx| {
3406 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
3407 });
3408 }
3409
3410 fn title(&self, cx: &AppContext) -> Cow<str> {
3411 self.context
3412 .read(cx)
3413 .summary()
3414 .map(|summary| summary.text.clone())
3415 .map(Cow::Owned)
3416 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
3417 }
3418
3419 fn render_patch_header(
3420 &self,
3421 range: Range<text::Anchor>,
3422 _id: FoldId,
3423 cx: &mut ViewContext<Self>,
3424 ) -> Option<AnyElement> {
3425 let patch = self.context.read(cx).patch_for_range(&range, cx)?;
3426 let theme = cx.theme().clone();
3427 Some(
3428 h_flex()
3429 .px_1()
3430 .py_0p5()
3431 .border_b_1()
3432 .border_color(theme.status().info_border)
3433 .gap_1()
3434 .child(Icon::new(IconName::Diff).size(IconSize::Small))
3435 .child(Label::new(patch.title.clone()).size(LabelSize::Small))
3436 .into_any(),
3437 )
3438 }
3439
3440 fn render_patch_footer(
3441 &mut self,
3442 range: Range<text::Anchor>,
3443 max_width: Pixels,
3444 gutter_width: Pixels,
3445 id: BlockId,
3446 cx: &mut ViewContext<Self>,
3447 ) -> Option<AnyElement> {
3448 let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
3449 let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
3450 let excerpt_id = *excerpt_id;
3451 let anchor = snapshot
3452 .buffer_snapshot
3453 .anchor_in_excerpt(excerpt_id, range.start)
3454 .unwrap();
3455
3456 if !snapshot.intersects_fold(anchor) {
3457 return None;
3458 }
3459
3460 let patch = self.context.read(cx).patch_for_range(&range, cx)?;
3461 let paths = patch
3462 .paths()
3463 .map(|p| SharedString::from(p.to_string()))
3464 .collect::<BTreeSet<_>>();
3465
3466 Some(
3467 v_flex()
3468 .id(id)
3469 .pl(gutter_width)
3470 .w(max_width)
3471 .py_2()
3472 .cursor(CursorStyle::PointingHand)
3473 .on_click(cx.listener(move |this, _, cx| {
3474 this.editor.update(cx, |editor, cx| {
3475 editor.change_selections(None, cx, |selections| {
3476 selections.select_ranges(vec![anchor..anchor]);
3477 });
3478 });
3479 this.focus_active_patch(cx);
3480 }))
3481 .children(paths.into_iter().map(|path| {
3482 h_flex()
3483 .pl_1()
3484 .gap_1()
3485 .child(Icon::new(IconName::File).size(IconSize::Small))
3486 .child(Label::new(path).size(LabelSize::Small))
3487 }))
3488 .when(patch.status == AssistantPatchStatus::Pending, |div| {
3489 div.child(
3490 Label::new("Generating")
3491 .color(Color::Muted)
3492 .size(LabelSize::Small)
3493 .with_animation(
3494 "pulsating-label",
3495 Animation::new(Duration::from_secs(2))
3496 .repeat()
3497 .with_easing(pulsating_between(0.4, 1.)),
3498 |label, delta| label.alpha(delta),
3499 ),
3500 )
3501 })
3502 .into_any(),
3503 )
3504 }
3505
3506 fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
3507 use feature_flags::FeatureFlagAppExt;
3508 let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
3509 assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
3510 });
3511
3512 if nudge.map_or(false, |value| value) {
3513 Some(
3514 h_flex()
3515 .p_3()
3516 .border_b_1()
3517 .border_color(cx.theme().colors().border_variant)
3518 .bg(cx.theme().colors().editor_background)
3519 .justify_between()
3520 .child(
3521 h_flex()
3522 .gap_3()
3523 .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
3524 .child(Label::new("Zed AI is here! Get started by signing in →")),
3525 )
3526 .child(
3527 Button::new("sign-in", "Sign in")
3528 .size(ButtonSize::Compact)
3529 .style(ButtonStyle::Filled)
3530 .on_click(cx.listener(|this, _event, cx| {
3531 let client = this
3532 .workspace
3533 .update(cx, |workspace, _| workspace.client().clone())
3534 .log_err();
3535
3536 if let Some(client) = client {
3537 cx.spawn(|this, mut cx| async move {
3538 client.authenticate_and_connect(true, &mut cx).await?;
3539 this.update(&mut cx, |_, cx| cx.notify())
3540 })
3541 .detach_and_log_err(cx)
3542 }
3543 })),
3544 )
3545 .into_any_element(),
3546 )
3547 } else if let Some(configuration_error) = configuration_error(cx) {
3548 let label = match configuration_error {
3549 ConfigurationError::NoProvider => "No LLM provider selected.",
3550 ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
3551 };
3552 Some(
3553 h_flex()
3554 .px_3()
3555 .py_2()
3556 .border_b_1()
3557 .border_color(cx.theme().colors().border_variant)
3558 .bg(cx.theme().colors().editor_background)
3559 .justify_between()
3560 .child(
3561 h_flex()
3562 .gap_3()
3563 .child(
3564 Icon::new(IconName::Warning)
3565 .size(IconSize::Small)
3566 .color(Color::Warning),
3567 )
3568 .child(Label::new(label)),
3569 )
3570 .child(
3571 Button::new("open-configuration", "Configure Providers")
3572 .size(ButtonSize::Compact)
3573 .icon(Some(IconName::SlidersVertical))
3574 .icon_size(IconSize::Small)
3575 .icon_position(IconPosition::Start)
3576 .style(ButtonStyle::Filled)
3577 .on_click({
3578 let focus_handle = self.focus_handle(cx).clone();
3579 move |_event, cx| {
3580 focus_handle.dispatch_action(&ShowConfiguration, cx);
3581 }
3582 }),
3583 )
3584 .into_any_element(),
3585 )
3586 } else {
3587 None
3588 }
3589 }
3590
3591 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3592 let focus_handle = self.focus_handle(cx).clone();
3593
3594 let (style, tooltip) = match token_state(&self.context, cx) {
3595 Some(TokenState::NoTokensLeft { .. }) => (
3596 ButtonStyle::Tinted(TintColor::Negative),
3597 Some(Tooltip::text("Token limit reached", cx)),
3598 ),
3599 Some(TokenState::HasMoreTokens {
3600 over_warn_threshold,
3601 ..
3602 }) => {
3603 let (style, tooltip) = if over_warn_threshold {
3604 (
3605 ButtonStyle::Tinted(TintColor::Warning),
3606 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
3607 )
3608 } else {
3609 (ButtonStyle::Filled, None)
3610 };
3611 (style, tooltip)
3612 }
3613 None => (ButtonStyle::Filled, None),
3614 };
3615
3616 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3617
3618 let has_configuration_error = configuration_error(cx).is_some();
3619 let needs_to_accept_terms = self.show_accept_terms
3620 && provider
3621 .as_ref()
3622 .map_or(false, |provider| provider.must_accept_terms(cx));
3623 let disabled = has_configuration_error || needs_to_accept_terms;
3624
3625 ButtonLike::new("send_button")
3626 .disabled(disabled)
3627 .style(style)
3628 .when_some(tooltip, |button, tooltip| {
3629 button.tooltip(move |_| tooltip.clone())
3630 })
3631 .layer(ElevationIndex::ModalSurface)
3632 .child(Label::new(
3633 if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
3634 "Chat"
3635 } else {
3636 "Send"
3637 },
3638 ))
3639 .children(
3640 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
3641 .map(|binding| binding.into_any_element()),
3642 )
3643 .on_click(move |_event, cx| {
3644 focus_handle.dispatch_action(&Assist, cx);
3645 })
3646 }
3647
3648 fn render_edit_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3649 let focus_handle = self.focus_handle(cx).clone();
3650
3651 let (style, tooltip) = match token_state(&self.context, cx) {
3652 Some(TokenState::NoTokensLeft { .. }) => (
3653 ButtonStyle::Tinted(TintColor::Negative),
3654 Some(Tooltip::text("Token limit reached", cx)),
3655 ),
3656 Some(TokenState::HasMoreTokens {
3657 over_warn_threshold,
3658 ..
3659 }) => {
3660 let (style, tooltip) = if over_warn_threshold {
3661 (
3662 ButtonStyle::Tinted(TintColor::Warning),
3663 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
3664 )
3665 } else {
3666 (ButtonStyle::Filled, None)
3667 };
3668 (style, tooltip)
3669 }
3670 None => (ButtonStyle::Filled, None),
3671 };
3672
3673 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3674
3675 let has_configuration_error = configuration_error(cx).is_some();
3676 let needs_to_accept_terms = self.show_accept_terms
3677 && provider
3678 .as_ref()
3679 .map_or(false, |provider| provider.must_accept_terms(cx));
3680 let disabled = has_configuration_error || needs_to_accept_terms;
3681
3682 ButtonLike::new("edit_button")
3683 .disabled(disabled)
3684 .style(style)
3685 .when_some(tooltip, |button, tooltip| {
3686 button.tooltip(move |_| tooltip.clone())
3687 })
3688 .layer(ElevationIndex::ModalSurface)
3689 .child(Label::new("Suggest Edits"))
3690 .children(
3691 KeyBinding::for_action_in(&Edit, &focus_handle, cx)
3692 .map(|binding| binding.into_any_element()),
3693 )
3694 .on_click(move |_event, cx| {
3695 focus_handle.dispatch_action(&Edit, cx);
3696 })
3697 }
3698
3699 fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
3700 let last_error = self.last_error.as_ref()?;
3701
3702 Some(
3703 div()
3704 .absolute()
3705 .right_3()
3706 .bottom_12()
3707 .max_w_96()
3708 .py_2()
3709 .px_3()
3710 .elevation_2(cx)
3711 .occlude()
3712 .child(match last_error {
3713 AssistError::FileRequired => self.render_file_required_error(cx),
3714 AssistError::PaymentRequired => self.render_payment_required_error(cx),
3715 AssistError::MaxMonthlySpendReached => {
3716 self.render_max_monthly_spend_reached_error(cx)
3717 }
3718 AssistError::Message(error_message) => {
3719 self.render_assist_error(error_message, cx)
3720 }
3721 })
3722 .into_any(),
3723 )
3724 }
3725
3726 fn render_file_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
3727 v_flex()
3728 .gap_0p5()
3729 .child(
3730 h_flex()
3731 .gap_1p5()
3732 .items_center()
3733 .child(Icon::new(IconName::Warning).color(Color::Warning))
3734 .child(
3735 Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
3736 ),
3737 )
3738 .child(
3739 div()
3740 .id("error-message")
3741 .max_h_24()
3742 .overflow_y_scroll()
3743 .child(Label::new(
3744 "To include files, type /file or /tab in your prompt.",
3745 )),
3746 )
3747 .child(
3748 h_flex()
3749 .justify_end()
3750 .mt_1()
3751 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
3752 |this, _, cx| {
3753 this.last_error = None;
3754 cx.notify();
3755 },
3756 ))),
3757 )
3758 .into_any()
3759 }
3760
3761 fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
3762 const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
3763
3764 v_flex()
3765 .gap_0p5()
3766 .child(
3767 h_flex()
3768 .gap_1p5()
3769 .items_center()
3770 .child(Icon::new(IconName::XCircle).color(Color::Error))
3771 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
3772 )
3773 .child(
3774 div()
3775 .id("error-message")
3776 .max_h_24()
3777 .overflow_y_scroll()
3778 .child(Label::new(ERROR_MESSAGE)),
3779 )
3780 .child(
3781 h_flex()
3782 .justify_end()
3783 .mt_1()
3784 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
3785 |this, _, cx| {
3786 this.last_error = None;
3787 cx.open_url(&zed_urls::account_url(cx));
3788 cx.notify();
3789 },
3790 )))
3791 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
3792 |this, _, cx| {
3793 this.last_error = None;
3794 cx.notify();
3795 },
3796 ))),
3797 )
3798 .into_any()
3799 }
3800
3801 fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
3802 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
3803
3804 v_flex()
3805 .gap_0p5()
3806 .child(
3807 h_flex()
3808 .gap_1p5()
3809 .items_center()
3810 .child(Icon::new(IconName::XCircle).color(Color::Error))
3811 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
3812 )
3813 .child(
3814 div()
3815 .id("error-message")
3816 .max_h_24()
3817 .overflow_y_scroll()
3818 .child(Label::new(ERROR_MESSAGE)),
3819 )
3820 .child(
3821 h_flex()
3822 .justify_end()
3823 .mt_1()
3824 .child(
3825 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
3826 cx.listener(|this, _, cx| {
3827 this.last_error = None;
3828 cx.open_url(&zed_urls::account_url(cx));
3829 cx.notify();
3830 }),
3831 ),
3832 )
3833 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
3834 |this, _, cx| {
3835 this.last_error = None;
3836 cx.notify();
3837 },
3838 ))),
3839 )
3840 .into_any()
3841 }
3842
3843 fn render_assist_error(
3844 &self,
3845 error_message: &SharedString,
3846 cx: &mut ViewContext<Self>,
3847 ) -> AnyElement {
3848 v_flex()
3849 .gap_0p5()
3850 .child(
3851 h_flex()
3852 .gap_1p5()
3853 .items_center()
3854 .child(Icon::new(IconName::XCircle).color(Color::Error))
3855 .child(
3856 Label::new("Error interacting with language model")
3857 .weight(FontWeight::MEDIUM),
3858 ),
3859 )
3860 .child(
3861 div()
3862 .id("error-message")
3863 .max_h_24()
3864 .overflow_y_scroll()
3865 .child(Label::new(error_message.clone())),
3866 )
3867 .child(
3868 h_flex()
3869 .justify_end()
3870 .mt_1()
3871 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
3872 |this, _, cx| {
3873 this.last_error = None;
3874 cx.notify();
3875 },
3876 ))),
3877 )
3878 .into_any()
3879 }
3880}
3881
3882/// Returns the contents of the *outermost* fenced code block that contains the given offset.
3883fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
3884 const CODE_BLOCK_NODE: &'static str = "fenced_code_block";
3885 const CODE_BLOCK_CONTENT: &'static str = "code_fence_content";
3886
3887 let layer = snapshot.syntax_layers().next()?;
3888
3889 let root_node = layer.node();
3890 let mut cursor = root_node.walk();
3891
3892 // Go to the first child for the given offset
3893 while cursor.goto_first_child_for_byte(offset).is_some() {
3894 // If we're at the end of the node, go to the next one.
3895 // Example: if you have a fenced-code-block, and you're on the start of the line
3896 // right after the closing ```, you want to skip the fenced-code-block and
3897 // go to the next sibling.
3898 if cursor.node().end_byte() == offset {
3899 cursor.goto_next_sibling();
3900 }
3901
3902 if cursor.node().start_byte() > offset {
3903 break;
3904 }
3905
3906 // We found the fenced code block.
3907 if cursor.node().kind() == CODE_BLOCK_NODE {
3908 // Now we need to find the child node that contains the code.
3909 cursor.goto_first_child();
3910 loop {
3911 if cursor.node().kind() == CODE_BLOCK_CONTENT {
3912 return Some(cursor.node().byte_range());
3913 }
3914 if !cursor.goto_next_sibling() {
3915 break;
3916 }
3917 }
3918 }
3919 }
3920
3921 None
3922}
3923
3924pub fn selections_creases(
3925 workspace: &mut workspace::Workspace,
3926 cx: &mut ViewContext<Workspace>,
3927) -> Option<Vec<(String, String)>> {
3928 let editor = workspace
3929 .active_item(cx)
3930 .and_then(|item| item.act_as::<Editor>(cx))?;
3931
3932 let mut creases = vec![];
3933 editor.update(cx, |editor, cx| {
3934 let selections = editor.selections.all_adjusted(cx);
3935 let buffer = editor.buffer().read(cx).snapshot(cx);
3936 for selection in selections {
3937 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
3938 ..editor::ToOffset::to_offset(&selection.end, &buffer);
3939 let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
3940 if selected_text.is_empty() {
3941 continue;
3942 }
3943 let start_language = buffer.language_at(range.start);
3944 let end_language = buffer.language_at(range.end);
3945 let language_name = if start_language == end_language {
3946 start_language.map(|language| language.code_fence_block_name())
3947 } else {
3948 None
3949 };
3950 let language_name = language_name.as_deref().unwrap_or("");
3951 let filename = buffer
3952 .file_at(selection.start)
3953 .map(|file| file.full_path(cx));
3954 let text = if language_name == "markdown" {
3955 selected_text
3956 .lines()
3957 .map(|line| format!("> {}", line))
3958 .collect::<Vec<_>>()
3959 .join("\n")
3960 } else {
3961 let start_symbols = buffer
3962 .symbols_containing(selection.start, None)
3963 .map(|(_, symbols)| symbols);
3964 let end_symbols = buffer
3965 .symbols_containing(selection.end, None)
3966 .map(|(_, symbols)| symbols);
3967
3968 let outline_text =
3969 if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
3970 Some(
3971 start_symbols
3972 .into_iter()
3973 .zip(end_symbols)
3974 .take_while(|(a, b)| a == b)
3975 .map(|(a, _)| a.text)
3976 .collect::<Vec<_>>()
3977 .join(" > "),
3978 )
3979 } else {
3980 None
3981 };
3982
3983 let line_comment_prefix = start_language
3984 .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
3985
3986 let fence = codeblock_fence_for_path(
3987 filename.as_deref(),
3988 Some(selection.start.row..=selection.end.row),
3989 );
3990
3991 if let Some((line_comment_prefix, outline_text)) =
3992 line_comment_prefix.zip(outline_text)
3993 {
3994 let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
3995 format!("{fence}{breadcrumb}{selected_text}\n```")
3996 } else {
3997 format!("{fence}{selected_text}\n```")
3998 }
3999 };
4000 let crease_title = if let Some(path) = filename {
4001 let start_line = selection.start.row + 1;
4002 let end_line = selection.end.row + 1;
4003 if start_line == end_line {
4004 format!("{}, Line {}", path.display(), start_line)
4005 } else {
4006 format!("{}, Lines {} to {}", path.display(), start_line, end_line)
4007 }
4008 } else {
4009 "Quoted selection".to_string()
4010 };
4011 creases.push((text, crease_title));
4012 }
4013 });
4014 Some(creases)
4015}
4016
4017fn render_fold_icon_button(
4018 editor: WeakView<Editor>,
4019 icon: IconName,
4020 label: SharedString,
4021) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement> {
4022 Arc::new(move |fold_id, fold_range, _cx| {
4023 let editor = editor.clone();
4024 ButtonLike::new(fold_id)
4025 .style(ButtonStyle::Filled)
4026 .layer(ElevationIndex::ElevatedSurface)
4027 .child(Icon::new(icon))
4028 .child(Label::new(label.clone()).single_line())
4029 .on_click(move |_, cx| {
4030 editor
4031 .update(cx, |editor, cx| {
4032 let buffer_start = fold_range
4033 .start
4034 .to_point(&editor.buffer().read(cx).read(cx));
4035 let buffer_row = MultiBufferRow(buffer_start.row);
4036 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
4037 })
4038 .ok();
4039 })
4040 .into_any_element()
4041 })
4042}
4043
4044#[derive(Debug, Clone, Serialize, Deserialize)]
4045struct CopyMetadata {
4046 creases: Vec<SelectedCreaseMetadata>,
4047}
4048
4049#[derive(Debug, Clone, Serialize, Deserialize)]
4050struct SelectedCreaseMetadata {
4051 range_relative_to_selection: Range<usize>,
4052 crease: CreaseMetadata,
4053}
4054
4055impl EventEmitter<EditorEvent> for ContextEditor {}
4056impl EventEmitter<SearchEvent> for ContextEditor {}
4057
4058impl Render for ContextEditor {
4059 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4060 let provider = LanguageModelRegistry::read_global(cx).active_provider();
4061 let accept_terms = if self.show_accept_terms {
4062 provider
4063 .as_ref()
4064 .and_then(|provider| provider.render_accept_terms(cx))
4065 } else {
4066 None
4067 };
4068
4069 v_flex()
4070 .key_context("ContextEditor")
4071 .capture_action(cx.listener(ContextEditor::cancel))
4072 .capture_action(cx.listener(ContextEditor::save))
4073 .capture_action(cx.listener(ContextEditor::copy))
4074 .capture_action(cx.listener(ContextEditor::cut))
4075 .capture_action(cx.listener(ContextEditor::paste))
4076 .capture_action(cx.listener(ContextEditor::cycle_message_role))
4077 .capture_action(cx.listener(ContextEditor::confirm_command))
4078 .on_action(cx.listener(ContextEditor::edit))
4079 .on_action(cx.listener(ContextEditor::assist))
4080 .on_action(cx.listener(ContextEditor::split))
4081 .size_full()
4082 .children(self.render_notice(cx))
4083 .child(
4084 div()
4085 .flex_grow()
4086 .bg(cx.theme().colors().editor_background)
4087 .child(self.editor.clone()),
4088 )
4089 .when_some(accept_terms, |this, element| {
4090 this.child(
4091 div()
4092 .absolute()
4093 .right_3()
4094 .bottom_12()
4095 .max_w_96()
4096 .py_2()
4097 .px_3()
4098 .elevation_2(cx)
4099 .bg(cx.theme().colors().surface_background)
4100 .occlude()
4101 .child(element),
4102 )
4103 })
4104 .children(self.render_last_error(cx))
4105 .child(
4106 h_flex().w_full().relative().child(
4107 h_flex()
4108 .p_2()
4109 .w_full()
4110 .border_t_1()
4111 .border_color(cx.theme().colors().border_variant)
4112 .bg(cx.theme().colors().editor_background)
4113 .child(
4114 h_flex()
4115 .gap_1()
4116 .child(render_inject_context_menu(cx.view().downgrade(), cx)),
4117 )
4118 .child(
4119 h_flex()
4120 .w_full()
4121 .justify_end()
4122 .when(
4123 AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
4124 |buttons| {
4125 buttons
4126 .items_center()
4127 .gap_1p5()
4128 .child(self.render_edit_button(cx))
4129 .child(
4130 Label::new("or")
4131 .size(LabelSize::Small)
4132 .color(Color::Muted),
4133 )
4134 },
4135 )
4136 .child(self.render_send_button(cx)),
4137 ),
4138 ),
4139 )
4140 }
4141}
4142
4143impl FocusableView for ContextEditor {
4144 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4145 self.editor.focus_handle(cx)
4146 }
4147}
4148
4149impl Item for ContextEditor {
4150 type Event = editor::EditorEvent;
4151
4152 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
4153 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
4154 }
4155
4156 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
4157 match event {
4158 EditorEvent::Edited { .. } => {
4159 f(item::ItemEvent::Edit);
4160 }
4161 EditorEvent::TitleChanged => {
4162 f(item::ItemEvent::UpdateTab);
4163 }
4164 _ => {}
4165 }
4166 }
4167
4168 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
4169 Some(self.title(cx).to_string().into())
4170 }
4171
4172 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
4173 Some(Box::new(handle.clone()))
4174 }
4175
4176 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
4177 self.editor.update(cx, |editor, cx| {
4178 Item::set_nav_history(editor, nav_history, cx)
4179 })
4180 }
4181
4182 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
4183 self.editor
4184 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
4185 }
4186
4187 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
4188 self.editor.update(cx, Item::deactivated)
4189 }
4190
4191 fn act_as_type<'a>(
4192 &'a self,
4193 type_id: TypeId,
4194 self_handle: &'a View<Self>,
4195 _: &'a AppContext,
4196 ) -> Option<AnyView> {
4197 if type_id == TypeId::of::<Self>() {
4198 Some(self_handle.to_any())
4199 } else if type_id == TypeId::of::<Editor>() {
4200 Some(self.editor.to_any())
4201 } else {
4202 None
4203 }
4204 }
4205}
4206
4207impl SearchableItem for ContextEditor {
4208 type Match = <Editor as SearchableItem>::Match;
4209
4210 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
4211 self.editor.update(cx, |editor, cx| {
4212 editor.clear_matches(cx);
4213 });
4214 }
4215
4216 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
4217 self.editor
4218 .update(cx, |editor, cx| editor.update_matches(matches, cx));
4219 }
4220
4221 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
4222 self.editor
4223 .update(cx, |editor, cx| editor.query_suggestion(cx))
4224 }
4225
4226 fn activate_match(
4227 &mut self,
4228 index: usize,
4229 matches: &[Self::Match],
4230 cx: &mut ViewContext<Self>,
4231 ) {
4232 self.editor.update(cx, |editor, cx| {
4233 editor.activate_match(index, matches, cx);
4234 });
4235 }
4236
4237 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
4238 self.editor
4239 .update(cx, |editor, cx| editor.select_matches(matches, cx));
4240 }
4241
4242 fn replace(
4243 &mut self,
4244 identifier: &Self::Match,
4245 query: &project::search::SearchQuery,
4246 cx: &mut ViewContext<Self>,
4247 ) {
4248 self.editor
4249 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
4250 }
4251
4252 fn find_matches(
4253 &mut self,
4254 query: Arc<project::search::SearchQuery>,
4255 cx: &mut ViewContext<Self>,
4256 ) -> Task<Vec<Self::Match>> {
4257 self.editor
4258 .update(cx, |editor, cx| editor.find_matches(query, cx))
4259 }
4260
4261 fn active_match_index(
4262 &mut self,
4263 matches: &[Self::Match],
4264 cx: &mut ViewContext<Self>,
4265 ) -> Option<usize> {
4266 self.editor
4267 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
4268 }
4269}
4270
4271impl FollowableItem for ContextEditor {
4272 fn remote_id(&self) -> Option<workspace::ViewId> {
4273 self.remote_id
4274 }
4275
4276 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
4277 let context = self.context.read(cx);
4278 Some(proto::view::Variant::ContextEditor(
4279 proto::view::ContextEditor {
4280 context_id: context.id().to_proto(),
4281 editor: if let Some(proto::view::Variant::Editor(proto)) =
4282 self.editor.read(cx).to_state_proto(cx)
4283 {
4284 Some(proto)
4285 } else {
4286 None
4287 },
4288 },
4289 ))
4290 }
4291
4292 fn from_state_proto(
4293 workspace: View<Workspace>,
4294 id: workspace::ViewId,
4295 state: &mut Option<proto::view::Variant>,
4296 cx: &mut WindowContext,
4297 ) -> Option<Task<Result<View<Self>>>> {
4298 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
4299 return None;
4300 };
4301 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
4302 unreachable!()
4303 };
4304
4305 let context_id = ContextId::from_proto(state.context_id);
4306 let editor_state = state.editor?;
4307
4308 let (project, panel) = workspace.update(cx, |workspace, cx| {
4309 Some((
4310 workspace.project().clone(),
4311 workspace.panel::<AssistantPanel>(cx)?,
4312 ))
4313 })?;
4314
4315 let context_editor =
4316 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
4317
4318 Some(cx.spawn(|mut cx| async move {
4319 let context_editor = context_editor.await?;
4320 context_editor
4321 .update(&mut cx, |context_editor, cx| {
4322 context_editor.remote_id = Some(id);
4323 context_editor.editor.update(cx, |editor, cx| {
4324 editor.apply_update_proto(
4325 &project,
4326 proto::update_view::Variant::Editor(proto::update_view::Editor {
4327 selections: editor_state.selections,
4328 pending_selection: editor_state.pending_selection,
4329 scroll_top_anchor: editor_state.scroll_top_anchor,
4330 scroll_x: editor_state.scroll_y,
4331 scroll_y: editor_state.scroll_y,
4332 ..Default::default()
4333 }),
4334 cx,
4335 )
4336 })
4337 })?
4338 .await?;
4339 Ok(context_editor)
4340 }))
4341 }
4342
4343 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
4344 Editor::to_follow_event(event)
4345 }
4346
4347 fn add_event_to_update_proto(
4348 &self,
4349 event: &Self::Event,
4350 update: &mut Option<proto::update_view::Variant>,
4351 cx: &WindowContext,
4352 ) -> bool {
4353 self.editor
4354 .read(cx)
4355 .add_event_to_update_proto(event, update, cx)
4356 }
4357
4358 fn apply_update_proto(
4359 &mut self,
4360 project: &Model<Project>,
4361 message: proto::update_view::Variant,
4362 cx: &mut ViewContext<Self>,
4363 ) -> Task<Result<()>> {
4364 self.editor.update(cx, |editor, cx| {
4365 editor.apply_update_proto(project, message, cx)
4366 })
4367 }
4368
4369 fn is_project_item(&self, _cx: &WindowContext) -> bool {
4370 true
4371 }
4372
4373 fn set_leader_peer_id(
4374 &mut self,
4375 leader_peer_id: Option<proto::PeerId>,
4376 cx: &mut ViewContext<Self>,
4377 ) {
4378 self.editor.update(cx, |editor, cx| {
4379 editor.set_leader_peer_id(leader_peer_id, cx)
4380 })
4381 }
4382
4383 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
4384 if existing.context.read(cx).id() == self.context.read(cx).id() {
4385 Some(item::Dedup::KeepExisting)
4386 } else {
4387 None
4388 }
4389 }
4390}
4391
4392pub struct ContextEditorToolbarItem {
4393 fs: Arc<dyn Fs>,
4394 active_context_editor: Option<WeakView<ContextEditor>>,
4395 model_summary_editor: View<Editor>,
4396 model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
4397}
4398
4399fn render_inject_context_menu(
4400 active_context_editor: WeakView<ContextEditor>,
4401 cx: &mut WindowContext<'_>,
4402) -> impl IntoElement {
4403 let commands = SlashCommandRegistry::global(cx);
4404
4405 slash_command_picker::SlashCommandSelector::new(
4406 commands.clone(),
4407 active_context_editor,
4408 Button::new("trigger", "Add Context")
4409 .icon(IconName::Plus)
4410 .icon_size(IconSize::Small)
4411 .icon_color(Color::Muted)
4412 .icon_position(IconPosition::Start)
4413 .tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
4414 )
4415}
4416
4417impl ContextEditorToolbarItem {
4418 pub fn new(
4419 workspace: &Workspace,
4420 model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
4421 model_summary_editor: View<Editor>,
4422 ) -> Self {
4423 Self {
4424 fs: workspace.app_state().fs.clone(),
4425 active_context_editor: None,
4426 model_summary_editor,
4427 model_selector_menu_handle,
4428 }
4429 }
4430
4431 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
4432 let context = &self
4433 .active_context_editor
4434 .as_ref()?
4435 .upgrade()?
4436 .read(cx)
4437 .context;
4438 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
4439 TokenState::NoTokensLeft {
4440 max_token_count,
4441 token_count,
4442 } => (Color::Error, token_count, max_token_count),
4443 TokenState::HasMoreTokens {
4444 max_token_count,
4445 token_count,
4446 over_warn_threshold,
4447 } => {
4448 let color = if over_warn_threshold {
4449 Color::Warning
4450 } else {
4451 Color::Muted
4452 };
4453 (color, token_count, max_token_count)
4454 }
4455 };
4456 Some(
4457 h_flex()
4458 .gap_0p5()
4459 .child(
4460 Label::new(humanize_token_count(token_count))
4461 .size(LabelSize::Small)
4462 .color(token_count_color),
4463 )
4464 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
4465 .child(
4466 Label::new(humanize_token_count(max_token_count))
4467 .size(LabelSize::Small)
4468 .color(Color::Muted),
4469 ),
4470 )
4471 }
4472}
4473
4474impl Render for ContextEditorToolbarItem {
4475 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4476 let left_side = h_flex()
4477 .group("chat-title-group")
4478 .pl_0p5()
4479 .gap_1()
4480 .items_center()
4481 .flex_grow()
4482 .child(
4483 div()
4484 .w_full()
4485 .when(self.active_context_editor.is_some(), |left_side| {
4486 left_side.child(self.model_summary_editor.clone())
4487 }),
4488 )
4489 .child(
4490 div().visible_on_hover("chat-title-group").child(
4491 IconButton::new("regenerate-context", IconName::RefreshTitle)
4492 .shape(ui::IconButtonShape::Square)
4493 .tooltip(|cx| Tooltip::text("Regenerate Title", cx))
4494 .on_click(cx.listener(move |_, _, cx| {
4495 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
4496 })),
4497 ),
4498 );
4499 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
4500 let active_model = LanguageModelRegistry::read_global(cx).active_model();
4501 let right_side = h_flex()
4502 .gap_2()
4503 // TODO display this in a nicer way, once we have a design for it.
4504 // .children({
4505 // let project = self
4506 // .workspace
4507 // .upgrade()
4508 // .map(|workspace| workspace.read(cx).project().downgrade());
4509 //
4510 // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
4511 // project.and_then(|project| db.remaining_summaries(&project, cx))
4512 // });
4513 // scan_items_remaining
4514 // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
4515 // })
4516 .child(
4517 ModelSelector::new(
4518 self.fs.clone(),
4519 ButtonLike::new("active-model")
4520 .style(ButtonStyle::Subtle)
4521 .child(
4522 h_flex()
4523 .w_full()
4524 .gap_0p5()
4525 .child(
4526 div()
4527 .overflow_x_hidden()
4528 .flex_grow()
4529 .whitespace_nowrap()
4530 .child(match (active_provider, active_model) {
4531 (Some(provider), Some(model)) => h_flex()
4532 .gap_1()
4533 .child(
4534 Icon::new(
4535 model
4536 .icon()
4537 .unwrap_or_else(|| provider.icon()),
4538 )
4539 .color(Color::Muted)
4540 .size(IconSize::XSmall),
4541 )
4542 .child(
4543 Label::new(model.name().0)
4544 .size(LabelSize::Small)
4545 .color(Color::Muted),
4546 )
4547 .into_any_element(),
4548 _ => Label::new("No model selected")
4549 .size(LabelSize::Small)
4550 .color(Color::Muted)
4551 .into_any_element(),
4552 }),
4553 )
4554 .child(
4555 Icon::new(IconName::ChevronDown)
4556 .color(Color::Muted)
4557 .size(IconSize::XSmall),
4558 ),
4559 )
4560 .tooltip(move |cx| {
4561 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
4562 }),
4563 )
4564 .with_handle(self.model_selector_menu_handle.clone()),
4565 )
4566 .children(self.render_remaining_tokens(cx));
4567
4568 h_flex()
4569 .size_full()
4570 .gap_2()
4571 .justify_between()
4572 .child(left_side)
4573 .child(right_side)
4574 }
4575}
4576
4577impl ToolbarItemView for ContextEditorToolbarItem {
4578 fn set_active_pane_item(
4579 &mut self,
4580 active_pane_item: Option<&dyn ItemHandle>,
4581 cx: &mut ViewContext<Self>,
4582 ) -> ToolbarItemLocation {
4583 self.active_context_editor = active_pane_item
4584 .and_then(|item| item.act_as::<ContextEditor>(cx))
4585 .map(|editor| editor.downgrade());
4586 cx.notify();
4587 if self.active_context_editor.is_none() {
4588 ToolbarItemLocation::Hidden
4589 } else {
4590 ToolbarItemLocation::PrimaryRight
4591 }
4592 }
4593
4594 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
4595 cx.notify();
4596 }
4597}
4598
4599impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
4600
4601enum ContextEditorToolbarItemEvent {
4602 RegenerateSummary,
4603}
4604impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
4605
4606pub struct ContextHistory {
4607 picker: View<Picker<SavedContextPickerDelegate>>,
4608 _subscriptions: Vec<Subscription>,
4609 assistant_panel: WeakView<AssistantPanel>,
4610}
4611
4612impl ContextHistory {
4613 fn new(
4614 project: Model<Project>,
4615 context_store: Model<ContextStore>,
4616 assistant_panel: WeakView<AssistantPanel>,
4617 cx: &mut ViewContext<Self>,
4618 ) -> Self {
4619 let picker = cx.new_view(|cx| {
4620 Picker::uniform_list(
4621 SavedContextPickerDelegate::new(project, context_store.clone()),
4622 cx,
4623 )
4624 .modal(false)
4625 .max_height(None)
4626 });
4627
4628 let _subscriptions = vec![
4629 cx.observe(&context_store, |this, _, cx| {
4630 this.picker.update(cx, |picker, cx| picker.refresh(cx));
4631 }),
4632 cx.subscribe(&picker, Self::handle_picker_event),
4633 ];
4634
4635 Self {
4636 picker,
4637 _subscriptions,
4638 assistant_panel,
4639 }
4640 }
4641
4642 fn handle_picker_event(
4643 &mut self,
4644 _: View<Picker<SavedContextPickerDelegate>>,
4645 event: &SavedContextPickerEvent,
4646 cx: &mut ViewContext<Self>,
4647 ) {
4648 let SavedContextPickerEvent::Confirmed(context) = event;
4649 self.assistant_panel
4650 .update(cx, |assistant_panel, cx| match context {
4651 ContextMetadata::Remote(metadata) => {
4652 assistant_panel
4653 .open_remote_context(metadata.id.clone(), cx)
4654 .detach_and_log_err(cx);
4655 }
4656 ContextMetadata::Saved(metadata) => {
4657 assistant_panel
4658 .open_saved_context(metadata.path.clone(), cx)
4659 .detach_and_log_err(cx);
4660 }
4661 })
4662 .ok();
4663 }
4664}
4665
4666#[derive(Debug, PartialEq, Eq, Clone, Copy)]
4667pub enum WorkflowAssistStatus {
4668 Pending,
4669 Confirmed,
4670 Done,
4671 Idle,
4672}
4673
4674impl Render for ContextHistory {
4675 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
4676 div().size_full().child(self.picker.clone())
4677 }
4678}
4679
4680impl FocusableView for ContextHistory {
4681 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4682 self.picker.focus_handle(cx)
4683 }
4684}
4685
4686impl EventEmitter<()> for ContextHistory {}
4687
4688impl Item for ContextHistory {
4689 type Event = ();
4690
4691 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
4692 Some("History".into())
4693 }
4694}
4695
4696pub struct ConfigurationView {
4697 focus_handle: FocusHandle,
4698 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
4699 _registry_subscription: Subscription,
4700}
4701
4702impl ConfigurationView {
4703 fn new(cx: &mut ViewContext<Self>) -> Self {
4704 let focus_handle = cx.focus_handle();
4705
4706 let registry_subscription = cx.subscribe(
4707 &LanguageModelRegistry::global(cx),
4708 |this, _, event: &language_model::Event, cx| match event {
4709 language_model::Event::AddedProvider(provider_id) => {
4710 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
4711 if let Some(provider) = provider {
4712 this.add_configuration_view(&provider, cx);
4713 }
4714 }
4715 language_model::Event::RemovedProvider(provider_id) => {
4716 this.remove_configuration_view(provider_id);
4717 }
4718 _ => {}
4719 },
4720 );
4721
4722 let mut this = Self {
4723 focus_handle,
4724 configuration_views: HashMap::default(),
4725 _registry_subscription: registry_subscription,
4726 };
4727 this.build_configuration_views(cx);
4728 this
4729 }
4730
4731 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
4732 let providers = LanguageModelRegistry::read_global(cx).providers();
4733 for provider in providers {
4734 self.add_configuration_view(&provider, cx);
4735 }
4736 }
4737
4738 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
4739 self.configuration_views.remove(provider_id);
4740 }
4741
4742 fn add_configuration_view(
4743 &mut self,
4744 provider: &Arc<dyn LanguageModelProvider>,
4745 cx: &mut ViewContext<Self>,
4746 ) {
4747 let configuration_view = provider.configuration_view(cx);
4748 self.configuration_views
4749 .insert(provider.id(), configuration_view);
4750 }
4751
4752 fn render_provider_view(
4753 &mut self,
4754 provider: &Arc<dyn LanguageModelProvider>,
4755 cx: &mut ViewContext<Self>,
4756 ) -> Div {
4757 let provider_id = provider.id().0.clone();
4758 let provider_name = provider.name().0.clone();
4759 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
4760
4761 let open_new_context = cx.listener({
4762 let provider = provider.clone();
4763 move |_, _, cx| {
4764 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
4765 provider.clone(),
4766 ))
4767 }
4768 });
4769
4770 v_flex()
4771 .gap_2()
4772 .child(
4773 h_flex()
4774 .justify_between()
4775 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
4776 .when(provider.is_authenticated(cx), move |this| {
4777 this.child(
4778 h_flex().justify_end().child(
4779 Button::new(
4780 SharedString::from(format!("new-context-{provider_id}")),
4781 "Open new context",
4782 )
4783 .icon_position(IconPosition::Start)
4784 .icon(IconName::Plus)
4785 .style(ButtonStyle::Filled)
4786 .layer(ElevationIndex::ModalSurface)
4787 .on_click(open_new_context),
4788 ),
4789 )
4790 }),
4791 )
4792 .child(
4793 div()
4794 .p(Spacing::Large.rems(cx))
4795 .bg(cx.theme().colors().surface_background)
4796 .border_1()
4797 .border_color(cx.theme().colors().border_variant)
4798 .rounded_md()
4799 .when(configuration_view.is_none(), |this| {
4800 this.child(div().child(Label::new(format!(
4801 "No configuration view for {}",
4802 provider_name
4803 ))))
4804 })
4805 .when_some(configuration_view, |this, configuration_view| {
4806 this.child(configuration_view)
4807 }),
4808 )
4809 }
4810}
4811
4812impl Render for ConfigurationView {
4813 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4814 let providers = LanguageModelRegistry::read_global(cx).providers();
4815 let provider_views = providers
4816 .into_iter()
4817 .map(|provider| self.render_provider_view(&provider, cx))
4818 .collect::<Vec<_>>();
4819
4820 let mut element = v_flex()
4821 .id("assistant-configuration-view")
4822 .track_focus(&self.focus_handle(cx))
4823 .bg(cx.theme().colors().editor_background)
4824 .size_full()
4825 .overflow_y_scroll()
4826 .child(
4827 v_flex()
4828 .p(Spacing::XXLarge.rems(cx))
4829 .border_b_1()
4830 .border_color(cx.theme().colors().border)
4831 .gap_1()
4832 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
4833 .child(
4834 Label::new(
4835 "At least one LLM provider must be configured to use the Assistant.",
4836 )
4837 .color(Color::Muted),
4838 ),
4839 )
4840 .child(
4841 v_flex()
4842 .p(Spacing::XXLarge.rems(cx))
4843 .mt_1()
4844 .gap_6()
4845 .flex_1()
4846 .children(provider_views),
4847 )
4848 .into_any();
4849
4850 // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
4851 // because we couldn't the element to take up the size of the parent.
4852 canvas(
4853 move |bounds, cx| {
4854 element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
4855 element
4856 },
4857 |_, mut element, cx| {
4858 element.paint(cx);
4859 },
4860 )
4861 .flex_1()
4862 .w_full()
4863 }
4864}
4865
4866pub enum ConfigurationViewEvent {
4867 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
4868}
4869
4870impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
4871
4872impl FocusableView for ConfigurationView {
4873 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
4874 self.focus_handle.clone()
4875 }
4876}
4877
4878impl Item for ConfigurationView {
4879 type Event = ConfigurationViewEvent;
4880
4881 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
4882 Some("Configuration".into())
4883 }
4884}
4885
4886type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
4887
4888fn render_slash_command_output_toggle(
4889 row: MultiBufferRow,
4890 is_folded: bool,
4891 fold: ToggleFold,
4892 _cx: &mut WindowContext,
4893) -> AnyElement {
4894 Disclosure::new(
4895 ("slash-command-output-fold-indicator", row.0 as u64),
4896 !is_folded,
4897 )
4898 .selected(is_folded)
4899 .on_click(move |_e, cx| fold(!is_folded, cx))
4900 .into_any_element()
4901}
4902
4903fn fold_toggle(
4904 name: &'static str,
4905) -> impl Fn(
4906 MultiBufferRow,
4907 bool,
4908 Arc<dyn Fn(bool, &mut WindowContext<'_>) + Send + Sync>,
4909 &mut WindowContext<'_>,
4910) -> AnyElement {
4911 move |row, is_folded, fold, _cx| {
4912 Disclosure::new((name, row.0 as u64), !is_folded)
4913 .selected(is_folded)
4914 .on_click(move |_e, cx| fold(!is_folded, cx))
4915 .into_any_element()
4916 }
4917}
4918
4919fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) -> FoldPlaceholder {
4920 FoldPlaceholder {
4921 render: Arc::new({
4922 move |fold_id, fold_range, _cx| {
4923 let editor = editor.clone();
4924 ButtonLike::new(fold_id)
4925 .style(ButtonStyle::Filled)
4926 .layer(ElevationIndex::ElevatedSurface)
4927 .child(Icon::new(IconName::TextSnippet))
4928 .child(Label::new(title.clone()).single_line())
4929 .on_click(move |_, cx| {
4930 editor
4931 .update(cx, |editor, cx| {
4932 let buffer_start = fold_range
4933 .start
4934 .to_point(&editor.buffer().read(cx).read(cx));
4935 let buffer_row = MultiBufferRow(buffer_start.row);
4936 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
4937 })
4938 .ok();
4939 })
4940 .into_any_element()
4941 }
4942 }),
4943 constrain_width: false,
4944 merge_adjacent: false,
4945 }
4946}
4947
4948fn render_quote_selection_output_toggle(
4949 row: MultiBufferRow,
4950 is_folded: bool,
4951 fold: ToggleFold,
4952 _cx: &mut WindowContext,
4953) -> AnyElement {
4954 Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
4955 .selected(is_folded)
4956 .on_click(move |_e, cx| fold(!is_folded, cx))
4957 .into_any_element()
4958}
4959
4960fn render_pending_slash_command_gutter_decoration(
4961 row: MultiBufferRow,
4962 status: &PendingSlashCommandStatus,
4963 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
4964) -> AnyElement {
4965 let mut icon = IconButton::new(
4966 ("slash-command-gutter-decoration", row.0),
4967 ui::IconName::TriangleRight,
4968 )
4969 .on_click(move |_e, cx| confirm_command(cx))
4970 .icon_size(ui::IconSize::Small)
4971 .size(ui::ButtonSize::None);
4972
4973 match status {
4974 PendingSlashCommandStatus::Idle => {
4975 icon = icon.icon_color(Color::Muted);
4976 }
4977 PendingSlashCommandStatus::Running { .. } => {
4978 icon = icon.selected(true);
4979 }
4980 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
4981 }
4982
4983 icon.into_any_element()
4984}
4985
4986fn render_docs_slash_command_trailer(
4987 row: MultiBufferRow,
4988 command: PendingSlashCommand,
4989 cx: &mut WindowContext,
4990) -> AnyElement {
4991 if command.arguments.is_empty() {
4992 return Empty.into_any();
4993 }
4994 let args = DocsSlashCommandArgs::parse(&command.arguments);
4995
4996 let Some(store) = args
4997 .provider()
4998 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
4999 else {
5000 return Empty.into_any();
5001 };
5002
5003 let Some(package) = args.package() else {
5004 return Empty.into_any();
5005 };
5006
5007 let mut children = Vec::new();
5008
5009 if store.is_indexing(&package) {
5010 children.push(
5011 div()
5012 .id(("crates-being-indexed", row.0))
5013 .child(Icon::new(IconName::ArrowCircle).with_animation(
5014 "arrow-circle",
5015 Animation::new(Duration::from_secs(4)).repeat(),
5016 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
5017 ))
5018 .tooltip({
5019 let package = package.clone();
5020 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
5021 })
5022 .into_any_element(),
5023 );
5024 }
5025
5026 if let Some(latest_error) = store.latest_error_for_package(&package) {
5027 children.push(
5028 div()
5029 .id(("latest-error", row.0))
5030 .child(
5031 Icon::new(IconName::Warning)
5032 .size(IconSize::Small)
5033 .color(Color::Warning),
5034 )
5035 .tooltip(move |cx| Tooltip::text(format!("Failed to index: {latest_error}"), cx))
5036 .into_any_element(),
5037 )
5038 }
5039
5040 let is_indexing = store.is_indexing(&package);
5041 let latest_error = store.latest_error_for_package(&package);
5042
5043 if !is_indexing && latest_error.is_none() {
5044 return Empty.into_any();
5045 }
5046
5047 h_flex().gap_2().children(children).into_any_element()
5048}
5049
5050fn make_lsp_adapter_delegate(
5051 project: &Model<Project>,
5052 cx: &mut AppContext,
5053) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
5054 project.update(cx, |project, cx| {
5055 // TODO: Find the right worktree.
5056 let Some(worktree) = project.worktrees(cx).next() else {
5057 return Ok(None::<Arc<dyn LspAdapterDelegate>>);
5058 };
5059 let http_client = project.client().http_client().clone();
5060 project.lsp_store().update(cx, |lsp_store, cx| {
5061 Ok(Some(LocalLspAdapterDelegate::new(
5062 lsp_store,
5063 &worktree,
5064 http_client,
5065 project.fs().clone(),
5066 cx,
5067 ) as Arc<dyn LspAdapterDelegate>))
5068 })
5069 })
5070}
5071
5072fn slash_command_error_block_renderer(message: String) -> RenderBlock {
5073 Box::new(move |_| {
5074 div()
5075 .pl_6()
5076 .child(
5077 Label::new(format!("error: {}", message))
5078 .single_line()
5079 .color(Color::Error),
5080 )
5081 .into_any()
5082 })
5083}
5084
5085enum TokenState {
5086 NoTokensLeft {
5087 max_token_count: usize,
5088 token_count: usize,
5089 },
5090 HasMoreTokens {
5091 max_token_count: usize,
5092 token_count: usize,
5093 over_warn_threshold: bool,
5094 },
5095}
5096
5097fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
5098 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
5099
5100 let model = LanguageModelRegistry::read_global(cx).active_model()?;
5101 let token_count = context.read(cx).token_count()?;
5102 let max_token_count = model.max_token_count();
5103
5104 let remaining_tokens = max_token_count as isize - token_count as isize;
5105 let token_state = if remaining_tokens <= 0 {
5106 TokenState::NoTokensLeft {
5107 max_token_count,
5108 token_count,
5109 }
5110 } else {
5111 let over_warn_threshold =
5112 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
5113 TokenState::HasMoreTokens {
5114 max_token_count,
5115 token_count,
5116 over_warn_threshold,
5117 }
5118 };
5119 Some(token_state)
5120}
5121
5122fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
5123 let image_size = data
5124 .size(0)
5125 .map(|dimension| Pixels::from(u32::from(dimension)));
5126 let image_ratio = image_size.width / image_size.height;
5127 let bounds_ratio = max_size.width / max_size.height;
5128
5129 if image_size.width > max_size.width || image_size.height > max_size.height {
5130 if bounds_ratio > image_ratio {
5131 size(
5132 image_size.width * (max_size.height / image_size.height),
5133 max_size.height,
5134 )
5135 } else {
5136 size(
5137 max_size.width,
5138 image_size.height * (max_size.width / image_size.width),
5139 )
5140 }
5141 } else {
5142 size(image_size.width, image_size.height)
5143 }
5144}
5145
5146enum ConfigurationError {
5147 NoProvider,
5148 ProviderNotAuthenticated,
5149}
5150
5151fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
5152 let provider = LanguageModelRegistry::read_global(cx).active_provider();
5153 let is_authenticated = provider
5154 .as_ref()
5155 .map_or(false, |provider| provider.is_authenticated(cx));
5156
5157 if provider.is_some() && is_authenticated {
5158 return None;
5159 }
5160
5161 if provider.is_none() {
5162 return Some(ConfigurationError::NoProvider);
5163 }
5164
5165 if !is_authenticated {
5166 return Some(ConfigurationError::ProviderNotAuthenticated);
5167 }
5168
5169 None
5170}
5171
5172#[cfg(test)]
5173mod tests {
5174 use super::*;
5175 use gpui::{AppContext, Context};
5176 use language::Buffer;
5177 use unindent::Unindent;
5178
5179 #[gpui::test]
5180 fn test_find_code_blocks(cx: &mut AppContext) {
5181 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
5182
5183 let buffer = cx.new_model(|cx| {
5184 let text = r#"
5185 line 0
5186 line 1
5187 ```rust
5188 fn main() {}
5189 ```
5190 line 5
5191 line 6
5192 line 7
5193 ```go
5194 func main() {}
5195 ```
5196 line 11
5197 ```
5198 this is plain text code block
5199 ```
5200
5201 ```go
5202 func another() {}
5203 ```
5204 line 19
5205 "#
5206 .unindent();
5207 let mut buffer = Buffer::local(text, cx);
5208 buffer.set_language(Some(markdown.clone()), cx);
5209 buffer
5210 });
5211 let snapshot = buffer.read(cx).snapshot();
5212
5213 let code_blocks = vec![
5214 Point::new(3, 0)..Point::new(4, 0),
5215 Point::new(9, 0)..Point::new(10, 0),
5216 Point::new(13, 0)..Point::new(14, 0),
5217 Point::new(17, 0)..Point::new(18, 0),
5218 ]
5219 .into_iter()
5220 .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
5221 .collect::<Vec<_>>();
5222
5223 let expected_results = vec![
5224 (0, None),
5225 (1, None),
5226 (2, Some(code_blocks[0].clone())),
5227 (3, Some(code_blocks[0].clone())),
5228 (4, Some(code_blocks[0].clone())),
5229 (5, None),
5230 (6, None),
5231 (7, None),
5232 (8, Some(code_blocks[1].clone())),
5233 (9, Some(code_blocks[1].clone())),
5234 (10, Some(code_blocks[1].clone())),
5235 (11, None),
5236 (12, Some(code_blocks[2].clone())),
5237 (13, Some(code_blocks[2].clone())),
5238 (14, Some(code_blocks[2].clone())),
5239 (15, None),
5240 (16, Some(code_blocks[3].clone())),
5241 (17, Some(code_blocks[3].clone())),
5242 (18, Some(code_blocks[3].clone())),
5243 (19, None),
5244 ];
5245
5246 for (row, expected) in expected_results {
5247 let offset = snapshot.point_to_offset(Point::new(row, 0));
5248 let range = find_surrounding_code_block(&snapshot, offset);
5249 assert_eq!(range, expected, "unexpected result on row {:?}", row);
5250 }
5251 }
5252}