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