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