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::{LanguageModelPickerDelegate, LanguageModelSelector};
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(AssistantPanel::inline_assist)
112 .register_action(ContextEditor::quote_selection)
113 .register_action(ContextEditor::insert_selection)
114 .register_action(ContextEditor::copy_code)
115 .register_action(ContextEditor::insert_dragged_files)
116 .register_action(AssistantPanel::show_configuration)
117 .register_action(AssistantPanel::create_new_context)
118 .register_action(AssistantPanel::restart_context_servers);
119 },
120 )
121 .detach();
122
123 cx.observe_new_views(
124 |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
125 let settings = AssistantSettings::get_global(cx);
126 terminal_panel.asssistant_enabled(settings.enabled, cx);
127 },
128 )
129 .detach();
130}
131
132pub enum AssistantPanelEvent {
133 ContextEdited,
134}
135
136pub struct AssistantPanel {
137 pane: View<Pane>,
138 workspace: WeakView<Workspace>,
139 width: Option<Pixels>,
140 height: Option<Pixels>,
141 project: Model<Project>,
142 context_store: Model<ContextStore>,
143 languages: Arc<LanguageRegistry>,
144 fs: Arc<dyn Fs>,
145 subscriptions: Vec<Subscription>,
146 model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
147 model_summary_editor: View<Editor>,
148 authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
149 configuration_subscription: Option<Subscription>,
150 client_status: Option<client::Status>,
151 watch_client_status: Option<Task<()>>,
152 show_zed_ai_notice: bool,
153}
154
155#[derive(Clone)]
156enum ContextMetadata {
157 Remote(RemoteContextMetadata),
158 Saved(SavedContextMetadata),
159}
160
161struct SavedContextPickerDelegate {
162 store: Model<ContextStore>,
163 project: Model<Project>,
164 matches: Vec<ContextMetadata>,
165 selected_index: usize,
166}
167
168enum SavedContextPickerEvent {
169 Confirmed(ContextMetadata),
170}
171
172enum InlineAssistTarget {
173 Editor(View<Editor>, bool),
174 Terminal(View<TerminalView>),
175}
176
177impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
178
179impl SavedContextPickerDelegate {
180 fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
181 Self {
182 project,
183 store,
184 matches: Vec::new(),
185 selected_index: 0,
186 }
187 }
188}
189
190impl PickerDelegate for SavedContextPickerDelegate {
191 type ListItem = ListItem;
192
193 fn match_count(&self) -> usize {
194 self.matches.len()
195 }
196
197 fn selected_index(&self) -> usize {
198 self.selected_index
199 }
200
201 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
202 self.selected_index = ix;
203 }
204
205 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
206 "Search...".into()
207 }
208
209 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
210 let search = self.store.read(cx).search(query, cx);
211 cx.spawn(|this, mut cx| async move {
212 let matches = search.await;
213 this.update(&mut cx, |this, cx| {
214 let host_contexts = this.delegate.store.read(cx).host_contexts();
215 this.delegate.matches = host_contexts
216 .iter()
217 .cloned()
218 .map(ContextMetadata::Remote)
219 .chain(matches.into_iter().map(ContextMetadata::Saved))
220 .collect();
221 this.delegate.selected_index = 0;
222 cx.notify();
223 })
224 .ok();
225 })
226 }
227
228 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
229 if let Some(metadata) = self.matches.get(self.selected_index) {
230 cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
231 }
232 }
233
234 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
235
236 fn render_match(
237 &self,
238 ix: usize,
239 selected: bool,
240 cx: &mut ViewContext<Picker<Self>>,
241 ) -> Option<Self::ListItem> {
242 let context = self.matches.get(ix)?;
243 let item = match context {
244 ContextMetadata::Remote(context) => {
245 let host_user = self.project.read(cx).host().and_then(|collaborator| {
246 self.project
247 .read(cx)
248 .user_store()
249 .read(cx)
250 .get_cached_user(collaborator.user_id)
251 });
252 div()
253 .flex()
254 .w_full()
255 .justify_between()
256 .gap_2()
257 .child(
258 h_flex().flex_1().overflow_x_hidden().child(
259 Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
260 .size(LabelSize::Small),
261 ),
262 )
263 .child(
264 h_flex()
265 .gap_2()
266 .children(if let Some(host_user) = host_user {
267 vec![
268 Avatar::new(host_user.avatar_uri.clone()).into_any_element(),
269 Label::new(format!("Shared by @{}", host_user.github_login))
270 .color(Color::Muted)
271 .size(LabelSize::Small)
272 .into_any_element(),
273 ]
274 } else {
275 vec![Label::new("Shared by host")
276 .color(Color::Muted)
277 .size(LabelSize::Small)
278 .into_any_element()]
279 }),
280 )
281 }
282 ContextMetadata::Saved(context) => div()
283 .flex()
284 .w_full()
285 .justify_between()
286 .gap_2()
287 .child(
288 h_flex()
289 .flex_1()
290 .child(Label::new(context.title.clone()).size(LabelSize::Small))
291 .overflow_x_hidden(),
292 )
293 .child(
294 Label::new(format_distance_from_now(
295 DateTimeType::Local(context.mtime),
296 false,
297 true,
298 true,
299 ))
300 .color(Color::Muted)
301 .size(LabelSize::Small),
302 ),
303 };
304 Some(
305 ListItem::new(ix)
306 .inset(true)
307 .spacing(ListItemSpacing::Sparse)
308 .selected(selected)
309 .child(item),
310 )
311 }
312}
313
314impl AssistantPanel {
315 pub fn load(
316 workspace: WeakView<Workspace>,
317 prompt_builder: Arc<PromptBuilder>,
318 cx: AsyncWindowContext,
319 ) -> Task<Result<View<Self>>> {
320 cx.spawn(|mut cx| async move {
321 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
322 let tools = Arc::new(ToolWorkingSet::default());
323 let context_store = workspace
324 .update(&mut cx, |workspace, cx| {
325 let project = workspace.project().clone();
326 ContextStore::new(project, prompt_builder.clone(), slash_commands, tools, cx)
327 })?
328 .await?;
329
330 workspace.update(&mut cx, |workspace, cx| {
331 // TODO: deserialize state.
332 cx.new_view(|cx| Self::new(workspace, context_store, cx))
333 })
334 })
335 }
336
337 fn new(
338 workspace: &Workspace,
339 context_store: Model<ContextStore>,
340 cx: &mut ViewContext<Self>,
341 ) -> Self {
342 let model_selector_menu_handle = PopoverMenuHandle::default();
343 let model_summary_editor = cx.new_view(Editor::single_line);
344 let context_editor_toolbar = cx.new_view(|_| {
345 ContextEditorToolbarItem::new(
346 workspace,
347 model_selector_menu_handle.clone(),
348 model_summary_editor.clone(),
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 .selected(
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
1463impl EventEmitter<PanelEvent> for AssistantPanel {}
1464impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1465
1466impl FocusableView for AssistantPanel {
1467 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1468 self.pane.focus_handle(cx)
1469 }
1470}
1471
1472pub enum ContextEditorEvent {
1473 Edited,
1474 TabContentChanged,
1475}
1476
1477#[derive(Copy, Clone, Debug, PartialEq)]
1478struct ScrollPosition {
1479 offset_before_cursor: gpui::Point<f32>,
1480 cursor: Anchor,
1481}
1482
1483struct PatchViewState {
1484 crease_id: CreaseId,
1485 editor: Option<PatchEditorState>,
1486 update_task: Option<Task<()>>,
1487}
1488
1489struct PatchEditorState {
1490 editor: WeakView<ProposedChangesEditor>,
1491 opened_patch: AssistantPatch,
1492}
1493
1494type MessageHeader = MessageMetadata;
1495
1496#[derive(Clone)]
1497enum AssistError {
1498 FileRequired,
1499 PaymentRequired,
1500 MaxMonthlySpendReached,
1501 Message(SharedString),
1502}
1503
1504pub struct ContextEditor {
1505 context: Model<Context>,
1506 fs: Arc<dyn Fs>,
1507 slash_commands: Arc<SlashCommandWorkingSet>,
1508 tools: Arc<ToolWorkingSet>,
1509 workspace: WeakView<Workspace>,
1510 project: Model<Project>,
1511 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1512 editor: View<Editor>,
1513 blocks: HashMap<MessageId, (MessageHeader, CustomBlockId)>,
1514 image_blocks: HashSet<CustomBlockId>,
1515 scroll_position: Option<ScrollPosition>,
1516 remote_id: Option<workspace::ViewId>,
1517 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
1518 invoked_slash_command_creases: HashMap<InvokedSlashCommandId, CreaseId>,
1519 pending_tool_use_creases: HashMap<Range<language::Anchor>, CreaseId>,
1520 _subscriptions: Vec<Subscription>,
1521 patches: HashMap<Range<language::Anchor>, PatchViewState>,
1522 active_patch: Option<Range<language::Anchor>>,
1523 assistant_panel: WeakView<AssistantPanel>,
1524 last_error: Option<AssistError>,
1525 show_accept_terms: bool,
1526 pub(crate) slash_menu_handle:
1527 PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
1528 // dragged_file_worktrees is used to keep references to worktrees that were added
1529 // when the user drag/dropped an external file onto the context editor. Since
1530 // the worktree is not part of the project panel, it would be dropped as soon as
1531 // the file is opened. In order to keep the worktree alive for the duration of the
1532 // context editor, we keep a reference here.
1533 dragged_file_worktrees: Vec<Model<Worktree>>,
1534}
1535
1536const DEFAULT_TAB_TITLE: &str = "New Chat";
1537const MAX_TAB_TITLE_LEN: usize = 16;
1538
1539impl ContextEditor {
1540 fn for_context(
1541 context: Model<Context>,
1542 fs: Arc<dyn Fs>,
1543 workspace: WeakView<Workspace>,
1544 project: Model<Project>,
1545 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1546 assistant_panel: WeakView<AssistantPanel>,
1547 cx: &mut ViewContext<Self>,
1548 ) -> Self {
1549 let completion_provider = SlashCommandCompletionProvider::new(
1550 context.read(cx).slash_commands.clone(),
1551 Some(cx.view().downgrade()),
1552 Some(workspace.clone()),
1553 );
1554
1555 let editor = cx.new_view(|cx| {
1556 let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
1557 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1558 editor.set_show_line_numbers(false, cx);
1559 editor.set_show_git_diff_gutter(false, cx);
1560 editor.set_show_code_actions(false, cx);
1561 editor.set_show_runnables(false, cx);
1562 editor.set_show_wrap_guides(false, cx);
1563 editor.set_show_indent_guides(false, cx);
1564 editor.set_completion_provider(Some(Box::new(completion_provider)));
1565 editor.set_collaboration_hub(Box::new(project.clone()));
1566 editor
1567 });
1568
1569 let _subscriptions = vec![
1570 cx.observe(&context, |_, _, cx| cx.notify()),
1571 cx.subscribe(&context, Self::handle_context_event),
1572 cx.subscribe(&editor, Self::handle_editor_event),
1573 cx.subscribe(&editor, Self::handle_editor_search_event),
1574 ];
1575
1576 let sections = context.read(cx).slash_command_output_sections().to_vec();
1577 let patch_ranges = context.read(cx).patch_ranges().collect::<Vec<_>>();
1578 let slash_commands = context.read(cx).slash_commands.clone();
1579 let tools = context.read(cx).tools.clone();
1580 let mut this = Self {
1581 context,
1582 slash_commands,
1583 tools,
1584 editor,
1585 lsp_adapter_delegate,
1586 blocks: Default::default(),
1587 image_blocks: Default::default(),
1588 scroll_position: None,
1589 remote_id: None,
1590 fs,
1591 workspace,
1592 project,
1593 pending_slash_command_creases: HashMap::default(),
1594 invoked_slash_command_creases: HashMap::default(),
1595 pending_tool_use_creases: HashMap::default(),
1596 _subscriptions,
1597 patches: HashMap::default(),
1598 active_patch: None,
1599 assistant_panel,
1600 last_error: None,
1601 show_accept_terms: false,
1602 slash_menu_handle: Default::default(),
1603 dragged_file_worktrees: Vec::new(),
1604 };
1605 this.update_message_headers(cx);
1606 this.update_image_blocks(cx);
1607 this.insert_slash_command_output_sections(sections, false, cx);
1608 this.patches_updated(&Vec::new(), &patch_ranges, cx);
1609 this
1610 }
1611
1612 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
1613 let command_name = DefaultSlashCommand.name();
1614 self.editor.update(cx, |editor, cx| {
1615 editor.insert(&format!("/{command_name}\n\n"), cx)
1616 });
1617 let command = self.context.update(cx, |context, cx| {
1618 context.reparse(cx);
1619 context.parsed_slash_commands()[0].clone()
1620 });
1621 self.run_command(
1622 command.source_range,
1623 &command.name,
1624 &command.arguments,
1625 false,
1626 self.workspace.clone(),
1627 cx,
1628 );
1629 }
1630
1631 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1632 self.send_to_model(RequestType::Chat, cx);
1633 }
1634
1635 fn edit(&mut self, _: &Edit, cx: &mut ViewContext<Self>) {
1636 self.send_to_model(RequestType::SuggestEdits, cx);
1637 }
1638
1639 fn focus_active_patch(&mut self, cx: &mut ViewContext<Self>) -> bool {
1640 if let Some((_range, patch)) = self.active_patch() {
1641 if let Some(editor) = patch
1642 .editor
1643 .as_ref()
1644 .and_then(|state| state.editor.upgrade())
1645 {
1646 cx.focus_view(&editor);
1647 return true;
1648 }
1649 }
1650
1651 false
1652 }
1653
1654 fn send_to_model(&mut self, request_type: RequestType, cx: &mut ViewContext<Self>) {
1655 let provider = LanguageModelRegistry::read_global(cx).active_provider();
1656 if provider
1657 .as_ref()
1658 .map_or(false, |provider| provider.must_accept_terms(cx))
1659 {
1660 self.show_accept_terms = true;
1661 cx.notify();
1662 return;
1663 }
1664
1665 if self.focus_active_patch(cx) {
1666 return;
1667 }
1668
1669 self.last_error = None;
1670
1671 if request_type == RequestType::SuggestEdits && !self.context.read(cx).contains_files(cx) {
1672 self.last_error = Some(AssistError::FileRequired);
1673 cx.notify();
1674 } else if let Some(user_message) = self
1675 .context
1676 .update(cx, |context, cx| context.assist(request_type, cx))
1677 {
1678 let new_selection = {
1679 let cursor = user_message
1680 .start
1681 .to_offset(self.context.read(cx).buffer().read(cx));
1682 cursor..cursor
1683 };
1684 self.editor.update(cx, |editor, cx| {
1685 editor.change_selections(
1686 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1687 cx,
1688 |selections| selections.select_ranges([new_selection]),
1689 );
1690 });
1691 // Avoid scrolling to the new cursor position so the assistant's output is stable.
1692 cx.defer(|this, _| this.scroll_position = None);
1693 }
1694
1695 cx.notify();
1696 }
1697
1698 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1699 self.last_error = None;
1700
1701 if self
1702 .context
1703 .update(cx, |context, cx| context.cancel_last_assist(cx))
1704 {
1705 return;
1706 }
1707
1708 cx.propagate();
1709 }
1710
1711 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
1712 let cursors = self.cursors(cx);
1713 self.context.update(cx, |context, cx| {
1714 let messages = context
1715 .messages_for_offsets(cursors, cx)
1716 .into_iter()
1717 .map(|message| message.id)
1718 .collect();
1719 context.cycle_message_roles(messages, cx)
1720 });
1721 }
1722
1723 fn cursors(&self, cx: &mut WindowContext) -> Vec<usize> {
1724 let selections = self
1725 .editor
1726 .update(cx, |editor, cx| editor.selections.all::<usize>(cx));
1727 selections
1728 .into_iter()
1729 .map(|selection| selection.head())
1730 .collect()
1731 }
1732
1733 pub fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
1734 if let Some(command) = self.slash_commands.command(name, cx) {
1735 self.editor.update(cx, |editor, cx| {
1736 editor.transact(cx, |editor, cx| {
1737 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
1738 let snapshot = editor.buffer().read(cx).snapshot(cx);
1739 let newest_cursor = editor.selections.newest::<Point>(cx).head();
1740 if newest_cursor.column > 0
1741 || snapshot
1742 .chars_at(newest_cursor)
1743 .next()
1744 .map_or(false, |ch| ch != '\n')
1745 {
1746 editor.move_to_end_of_line(
1747 &MoveToEndOfLine {
1748 stop_at_soft_wraps: false,
1749 },
1750 cx,
1751 );
1752 editor.newline(&Newline, cx);
1753 }
1754
1755 editor.insert(&format!("/{name}"), cx);
1756 if command.accepts_arguments() {
1757 editor.insert(" ", cx);
1758 editor.show_completions(&ShowCompletions::default(), cx);
1759 }
1760 });
1761 });
1762 if !command.requires_argument() {
1763 self.confirm_command(&ConfirmCommand, cx);
1764 }
1765 }
1766 }
1767
1768 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
1769 if self.editor.read(cx).has_active_completions_menu() {
1770 return;
1771 }
1772
1773 let selections = self.editor.read(cx).selections.disjoint_anchors();
1774 let mut commands_by_range = HashMap::default();
1775 let workspace = self.workspace.clone();
1776 self.context.update(cx, |context, cx| {
1777 context.reparse(cx);
1778 for selection in selections.iter() {
1779 if let Some(command) =
1780 context.pending_command_for_position(selection.head().text_anchor, cx)
1781 {
1782 commands_by_range
1783 .entry(command.source_range.clone())
1784 .or_insert_with(|| command.clone());
1785 }
1786 }
1787 });
1788
1789 if commands_by_range.is_empty() {
1790 cx.propagate();
1791 } else {
1792 for command in commands_by_range.into_values() {
1793 self.run_command(
1794 command.source_range,
1795 &command.name,
1796 &command.arguments,
1797 true,
1798 workspace.clone(),
1799 cx,
1800 );
1801 }
1802 cx.stop_propagation();
1803 }
1804 }
1805
1806 #[allow(clippy::too_many_arguments)]
1807 pub fn run_command(
1808 &mut self,
1809 command_range: Range<language::Anchor>,
1810 name: &str,
1811 arguments: &[String],
1812 ensure_trailing_newline: bool,
1813 workspace: WeakView<Workspace>,
1814 cx: &mut ViewContext<Self>,
1815 ) {
1816 if let Some(command) = self.slash_commands.command(name, cx) {
1817 let context = self.context.read(cx);
1818 let sections = context
1819 .slash_command_output_sections()
1820 .into_iter()
1821 .filter(|section| section.is_valid(context.buffer().read(cx)))
1822 .cloned()
1823 .collect::<Vec<_>>();
1824 let snapshot = context.buffer().read(cx).snapshot();
1825 let output = command.run(
1826 arguments,
1827 §ions,
1828 snapshot,
1829 workspace,
1830 self.lsp_adapter_delegate.clone(),
1831 cx,
1832 );
1833 self.context.update(cx, |context, cx| {
1834 context.insert_command_output(
1835 command_range,
1836 name,
1837 output,
1838 ensure_trailing_newline,
1839 cx,
1840 )
1841 });
1842 }
1843 }
1844
1845 fn handle_context_event(
1846 &mut self,
1847 _: Model<Context>,
1848 event: &ContextEvent,
1849 cx: &mut ViewContext<Self>,
1850 ) {
1851 let context_editor = cx.view().downgrade();
1852
1853 match event {
1854 ContextEvent::MessagesEdited => {
1855 self.update_message_headers(cx);
1856 self.update_image_blocks(cx);
1857 self.context.update(cx, |context, cx| {
1858 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1859 });
1860 }
1861 ContextEvent::SummaryChanged => {
1862 cx.emit(EditorEvent::TitleChanged);
1863 self.context.update(cx, |context, cx| {
1864 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1865 });
1866 }
1867 ContextEvent::StreamedCompletion => {
1868 self.editor.update(cx, |editor, cx| {
1869 if let Some(scroll_position) = self.scroll_position {
1870 let snapshot = editor.snapshot(cx);
1871 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1872 let scroll_top =
1873 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
1874 editor.set_scroll_position(
1875 point(scroll_position.offset_before_cursor.x, scroll_top),
1876 cx,
1877 );
1878 }
1879
1880 let new_tool_uses = self
1881 .context
1882 .read(cx)
1883 .pending_tool_uses()
1884 .into_iter()
1885 .filter(|tool_use| {
1886 !self
1887 .pending_tool_use_creases
1888 .contains_key(&tool_use.source_range)
1889 })
1890 .cloned()
1891 .collect::<Vec<_>>();
1892
1893 let buffer = editor.buffer().read(cx).snapshot(cx);
1894 let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
1895 let excerpt_id = *excerpt_id;
1896
1897 let mut buffer_rows_to_fold = BTreeSet::new();
1898
1899 let creases = new_tool_uses
1900 .iter()
1901 .map(|tool_use| {
1902 let placeholder = FoldPlaceholder {
1903 render: render_fold_icon_button(
1904 cx.view().downgrade(),
1905 IconName::PocketKnife,
1906 tool_use.name.clone().into(),
1907 ),
1908 ..Default::default()
1909 };
1910 let render_trailer =
1911 move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
1912
1913 let start = buffer
1914 .anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
1915 .unwrap();
1916 let end = buffer
1917 .anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
1918 .unwrap();
1919
1920 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1921 buffer_rows_to_fold.insert(buffer_row);
1922
1923 self.context.update(cx, |context, cx| {
1924 context.insert_content(
1925 Content::ToolUse {
1926 range: tool_use.source_range.clone(),
1927 tool_use: LanguageModelToolUse {
1928 id: tool_use.id.clone(),
1929 name: tool_use.name.clone(),
1930 input: tool_use.input.clone(),
1931 },
1932 },
1933 cx,
1934 );
1935 });
1936
1937 Crease::inline(
1938 start..end,
1939 placeholder,
1940 fold_toggle("tool-use"),
1941 render_trailer,
1942 )
1943 })
1944 .collect::<Vec<_>>();
1945
1946 let crease_ids = editor.insert_creases(creases, cx);
1947
1948 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1949 editor.fold_at(&FoldAt { buffer_row }, cx);
1950 }
1951
1952 self.pending_tool_use_creases.extend(
1953 new_tool_uses
1954 .iter()
1955 .map(|tool_use| tool_use.source_range.clone())
1956 .zip(crease_ids),
1957 );
1958 });
1959 }
1960 ContextEvent::PatchesUpdated { removed, updated } => {
1961 self.patches_updated(removed, updated, cx);
1962 }
1963 ContextEvent::ParsedSlashCommandsUpdated { removed, updated } => {
1964 self.editor.update(cx, |editor, cx| {
1965 let buffer = editor.buffer().read(cx).snapshot(cx);
1966 let (&excerpt_id, _, _) = buffer.as_singleton().unwrap();
1967
1968 editor.remove_creases(
1969 removed
1970 .iter()
1971 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
1972 cx,
1973 );
1974
1975 let crease_ids = editor.insert_creases(
1976 updated.iter().map(|command| {
1977 let workspace = self.workspace.clone();
1978 let confirm_command = Arc::new({
1979 let context_editor = context_editor.clone();
1980 let command = command.clone();
1981 move |cx: &mut WindowContext| {
1982 context_editor
1983 .update(cx, |context_editor, cx| {
1984 context_editor.run_command(
1985 command.source_range.clone(),
1986 &command.name,
1987 &command.arguments,
1988 false,
1989 workspace.clone(),
1990 cx,
1991 );
1992 })
1993 .ok();
1994 }
1995 });
1996 let placeholder = FoldPlaceholder {
1997 render: Arc::new(move |_, _, _| Empty.into_any()),
1998 ..Default::default()
1999 };
2000 let render_toggle = {
2001 let confirm_command = confirm_command.clone();
2002 let command = command.clone();
2003 move |row, _, _, _cx: &mut WindowContext| {
2004 render_pending_slash_command_gutter_decoration(
2005 row,
2006 &command.status,
2007 confirm_command.clone(),
2008 )
2009 }
2010 };
2011 let render_trailer = {
2012 let command = command.clone();
2013 move |row, _unfold, cx: &mut WindowContext| {
2014 // TODO: In the future we should investigate how we can expose
2015 // this as a hook on the `SlashCommand` trait so that we don't
2016 // need to special-case it here.
2017 if command.name == DocsSlashCommand::NAME {
2018 return render_docs_slash_command_trailer(
2019 row,
2020 command.clone(),
2021 cx,
2022 );
2023 }
2024
2025 Empty.into_any()
2026 }
2027 };
2028
2029 let start = buffer
2030 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2031 .unwrap();
2032 let end = buffer
2033 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2034 .unwrap();
2035 Crease::inline(start..end, placeholder, render_toggle, render_trailer)
2036 }),
2037 cx,
2038 );
2039
2040 self.pending_slash_command_creases.extend(
2041 updated
2042 .iter()
2043 .map(|command| command.source_range.clone())
2044 .zip(crease_ids),
2045 );
2046 })
2047 }
2048 ContextEvent::InvokedSlashCommandChanged { command_id } => {
2049 self.update_invoked_slash_command(*command_id, cx);
2050 }
2051 ContextEvent::SlashCommandOutputSectionAdded { section } => {
2052 self.insert_slash_command_output_sections([section.clone()], false, cx);
2053 }
2054 ContextEvent::UsePendingTools => {
2055 let pending_tool_uses = self
2056 .context
2057 .read(cx)
2058 .pending_tool_uses()
2059 .into_iter()
2060 .filter(|tool_use| tool_use.status.is_idle())
2061 .cloned()
2062 .collect::<Vec<_>>();
2063
2064 for tool_use in pending_tool_uses {
2065 if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
2066 let task = tool.run(tool_use.input, self.workspace.clone(), cx);
2067
2068 self.context.update(cx, |context, cx| {
2069 context.insert_tool_output(tool_use.id.clone(), task, cx);
2070 });
2071 }
2072 }
2073 }
2074 ContextEvent::ToolFinished {
2075 tool_use_id,
2076 output_range,
2077 } => {
2078 self.editor.update(cx, |editor, cx| {
2079 let buffer = editor.buffer().read(cx).snapshot(cx);
2080 let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
2081 let excerpt_id = *excerpt_id;
2082
2083 let placeholder = FoldPlaceholder {
2084 render: render_fold_icon_button(
2085 cx.view().downgrade(),
2086 IconName::PocketKnife,
2087 format!("Tool Result: {tool_use_id}").into(),
2088 ),
2089 ..Default::default()
2090 };
2091 let render_trailer =
2092 move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
2093
2094 let start = buffer
2095 .anchor_in_excerpt(excerpt_id, output_range.start)
2096 .unwrap();
2097 let end = buffer
2098 .anchor_in_excerpt(excerpt_id, output_range.end)
2099 .unwrap();
2100
2101 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2102
2103 let crease = Crease::inline(
2104 start..end,
2105 placeholder,
2106 fold_toggle("tool-use"),
2107 render_trailer,
2108 );
2109
2110 editor.insert_creases([crease], cx);
2111 editor.fold_at(&FoldAt { buffer_row }, cx);
2112 });
2113 }
2114 ContextEvent::Operation(_) => {}
2115 ContextEvent::ShowAssistError(error_message) => {
2116 self.last_error = Some(AssistError::Message(error_message.clone()));
2117 }
2118 ContextEvent::ShowPaymentRequiredError => {
2119 self.last_error = Some(AssistError::PaymentRequired);
2120 }
2121 ContextEvent::ShowMaxMonthlySpendReachedError => {
2122 self.last_error = Some(AssistError::MaxMonthlySpendReached);
2123 }
2124 }
2125 }
2126
2127 fn update_invoked_slash_command(
2128 &mut self,
2129 command_id: InvokedSlashCommandId,
2130 cx: &mut ViewContext<Self>,
2131 ) {
2132 if let Some(invoked_slash_command) =
2133 self.context.read(cx).invoked_slash_command(&command_id)
2134 {
2135 if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
2136 let run_commands_in_ranges = invoked_slash_command
2137 .run_commands_in_ranges
2138 .iter()
2139 .cloned()
2140 .collect::<Vec<_>>();
2141 for range in run_commands_in_ranges {
2142 let commands = self.context.update(cx, |context, cx| {
2143 context.reparse(cx);
2144 context
2145 .pending_commands_for_range(range.clone(), cx)
2146 .to_vec()
2147 });
2148
2149 for command in commands {
2150 self.run_command(
2151 command.source_range,
2152 &command.name,
2153 &command.arguments,
2154 false,
2155 self.workspace.clone(),
2156 cx,
2157 );
2158 }
2159 }
2160 }
2161 }
2162
2163 self.editor.update(cx, |editor, cx| {
2164 if let Some(invoked_slash_command) =
2165 self.context.read(cx).invoked_slash_command(&command_id)
2166 {
2167 if let InvokedSlashCommandStatus::Finished = invoked_slash_command.status {
2168 let buffer = editor.buffer().read(cx).snapshot(cx);
2169 let (&excerpt_id, _buffer_id, _buffer_snapshot) =
2170 buffer.as_singleton().unwrap();
2171
2172 let start = buffer
2173 .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
2174 .unwrap();
2175 let end = buffer
2176 .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
2177 .unwrap();
2178 editor.remove_folds_with_type(
2179 &[start..end],
2180 TypeId::of::<PendingSlashCommand>(),
2181 false,
2182 cx,
2183 );
2184
2185 editor.remove_creases(
2186 HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
2187 cx,
2188 );
2189 } else if let hash_map::Entry::Vacant(entry) =
2190 self.invoked_slash_command_creases.entry(command_id)
2191 {
2192 let buffer = editor.buffer().read(cx).snapshot(cx);
2193 let (&excerpt_id, _buffer_id, _buffer_snapshot) =
2194 buffer.as_singleton().unwrap();
2195 let context = self.context.downgrade();
2196 let crease_start = buffer
2197 .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.start)
2198 .unwrap();
2199 let crease_end = buffer
2200 .anchor_in_excerpt(excerpt_id, invoked_slash_command.range.end)
2201 .unwrap();
2202 let crease = Crease::inline(
2203 crease_start..crease_end,
2204 invoked_slash_command_fold_placeholder(command_id, context),
2205 fold_toggle("invoked-slash-command"),
2206 |_row, _folded, _cx| Empty.into_any(),
2207 );
2208 let crease_ids = editor.insert_creases([crease.clone()], cx);
2209 editor.fold_creases(vec![crease], false, cx);
2210 entry.insert(crease_ids[0]);
2211 } else {
2212 cx.notify()
2213 }
2214 } else {
2215 editor.remove_creases(
2216 HashSet::from_iter(self.invoked_slash_command_creases.remove(&command_id)),
2217 cx,
2218 );
2219 cx.notify();
2220 };
2221 });
2222 }
2223
2224 fn patches_updated(
2225 &mut self,
2226 removed: &Vec<Range<text::Anchor>>,
2227 updated: &Vec<Range<text::Anchor>>,
2228 cx: &mut ViewContext<ContextEditor>,
2229 ) {
2230 let this = cx.view().downgrade();
2231 let mut editors_to_close = Vec::new();
2232
2233 self.editor.update(cx, |editor, cx| {
2234 let snapshot = editor.snapshot(cx);
2235 let multibuffer = &snapshot.buffer_snapshot;
2236 let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
2237
2238 let mut removed_crease_ids = Vec::new();
2239 let mut ranges_to_unfold: Vec<Range<Anchor>> = Vec::new();
2240 for range in removed {
2241 if let Some(state) = self.patches.remove(range) {
2242 let patch_start = multibuffer
2243 .anchor_in_excerpt(excerpt_id, range.start)
2244 .unwrap();
2245 let patch_end = multibuffer
2246 .anchor_in_excerpt(excerpt_id, range.end)
2247 .unwrap();
2248
2249 editors_to_close.extend(state.editor.and_then(|state| state.editor.upgrade()));
2250 ranges_to_unfold.push(patch_start..patch_end);
2251 removed_crease_ids.push(state.crease_id);
2252 }
2253 }
2254 editor.unfold_ranges(&ranges_to_unfold, true, false, cx);
2255 editor.remove_creases(removed_crease_ids, cx);
2256
2257 for range in updated {
2258 let Some(patch) = self.context.read(cx).patch_for_range(&range, cx).cloned() else {
2259 continue;
2260 };
2261
2262 let path_count = patch.path_count();
2263 let patch_start = multibuffer
2264 .anchor_in_excerpt(excerpt_id, patch.range.start)
2265 .unwrap();
2266 let patch_end = multibuffer
2267 .anchor_in_excerpt(excerpt_id, patch.range.end)
2268 .unwrap();
2269 let render_block: RenderBlock = Arc::new({
2270 let this = this.clone();
2271 let patch_range = range.clone();
2272 move |cx: &mut BlockContext<'_, '_>| {
2273 let max_width = cx.max_width;
2274 let gutter_width = cx.gutter_dimensions.full_width();
2275 let block_id = cx.block_id;
2276 let selected = cx.selected;
2277 this.update(&mut **cx, |this, cx| {
2278 this.render_patch_block(
2279 patch_range.clone(),
2280 max_width,
2281 gutter_width,
2282 block_id,
2283 selected,
2284 cx,
2285 )
2286 })
2287 .ok()
2288 .flatten()
2289 .unwrap_or_else(|| Empty.into_any())
2290 }
2291 });
2292
2293 let height = path_count as u32 + 1;
2294 let crease = Crease::block(
2295 patch_start..patch_end,
2296 height,
2297 BlockStyle::Flex,
2298 render_block.clone(),
2299 );
2300
2301 let should_refold;
2302 if let Some(state) = self.patches.get_mut(&range) {
2303 if let Some(editor_state) = &state.editor {
2304 if editor_state.opened_patch != patch {
2305 state.update_task = Some({
2306 let this = this.clone();
2307 cx.spawn(|_, cx| async move {
2308 Self::update_patch_editor(this.clone(), patch, cx)
2309 .await
2310 .log_err();
2311 })
2312 });
2313 }
2314 }
2315
2316 should_refold =
2317 snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
2318 } else {
2319 let crease_id = editor.insert_creases([crease.clone()], cx)[0];
2320 self.patches.insert(
2321 range.clone(),
2322 PatchViewState {
2323 crease_id,
2324 editor: None,
2325 update_task: None,
2326 },
2327 );
2328
2329 should_refold = true;
2330 }
2331
2332 if should_refold {
2333 editor.unfold_ranges(&[patch_start..patch_end], true, false, cx);
2334 editor.fold_creases(vec![crease], false, cx);
2335 }
2336 }
2337 });
2338
2339 for editor in editors_to_close {
2340 self.close_patch_editor(editor, cx);
2341 }
2342
2343 self.update_active_patch(cx);
2344 }
2345
2346 fn insert_slash_command_output_sections(
2347 &mut self,
2348 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2349 expand_result: bool,
2350 cx: &mut ViewContext<Self>,
2351 ) {
2352 self.editor.update(cx, |editor, cx| {
2353 let buffer = editor.buffer().read(cx).snapshot(cx);
2354 let excerpt_id = *buffer.as_singleton().unwrap().0;
2355 let mut buffer_rows_to_fold = BTreeSet::new();
2356 let mut creases = Vec::new();
2357 for section in sections {
2358 let start = buffer
2359 .anchor_in_excerpt(excerpt_id, section.range.start)
2360 .unwrap();
2361 let end = buffer
2362 .anchor_in_excerpt(excerpt_id, section.range.end)
2363 .unwrap();
2364 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2365 buffer_rows_to_fold.insert(buffer_row);
2366 creases.push(
2367 Crease::inline(
2368 start..end,
2369 FoldPlaceholder {
2370 render: render_fold_icon_button(
2371 cx.view().downgrade(),
2372 section.icon,
2373 section.label.clone(),
2374 ),
2375 merge_adjacent: false,
2376 ..Default::default()
2377 },
2378 render_slash_command_output_toggle,
2379 |_, _, _| Empty.into_any_element(),
2380 )
2381 .with_metadata(CreaseMetadata {
2382 icon: section.icon,
2383 label: section.label,
2384 }),
2385 );
2386 }
2387
2388 editor.insert_creases(creases, cx);
2389
2390 if expand_result {
2391 buffer_rows_to_fold.clear();
2392 }
2393 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2394 editor.fold_at(&FoldAt { buffer_row }, cx);
2395 }
2396 });
2397 }
2398
2399 fn handle_editor_event(
2400 &mut self,
2401 _: View<Editor>,
2402 event: &EditorEvent,
2403 cx: &mut ViewContext<Self>,
2404 ) {
2405 match event {
2406 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2407 let cursor_scroll_position = self.cursor_scroll_position(cx);
2408 if *autoscroll {
2409 self.scroll_position = cursor_scroll_position;
2410 } else if self.scroll_position != cursor_scroll_position {
2411 self.scroll_position = None;
2412 }
2413 }
2414 EditorEvent::SelectionsChanged { .. } => {
2415 self.scroll_position = self.cursor_scroll_position(cx);
2416 self.update_active_patch(cx);
2417 }
2418 _ => {}
2419 }
2420 cx.emit(event.clone());
2421 }
2422
2423 fn active_patch(&self) -> Option<(Range<text::Anchor>, &PatchViewState)> {
2424 let patch = self.active_patch.as_ref()?;
2425 Some((patch.clone(), self.patches.get(&patch)?))
2426 }
2427
2428 fn update_active_patch(&mut self, cx: &mut ViewContext<Self>) {
2429 let newest_cursor = self.editor.update(cx, |editor, cx| {
2430 editor.selections.newest::<Point>(cx).head()
2431 });
2432 let context = self.context.read(cx);
2433
2434 let new_patch = context.patch_containing(newest_cursor, cx).cloned();
2435
2436 if new_patch.as_ref().map(|p| &p.range) == self.active_patch.as_ref() {
2437 return;
2438 }
2439
2440 if let Some(old_patch_range) = self.active_patch.take() {
2441 if let Some(patch_state) = self.patches.get_mut(&old_patch_range) {
2442 if let Some(state) = patch_state.editor.take() {
2443 if let Some(editor) = state.editor.upgrade() {
2444 self.close_patch_editor(editor, cx);
2445 }
2446 }
2447 }
2448 }
2449
2450 if let Some(new_patch) = new_patch {
2451 self.active_patch = Some(new_patch.range.clone());
2452
2453 if let Some(patch_state) = self.patches.get_mut(&new_patch.range) {
2454 let mut editor = None;
2455 if let Some(state) = &patch_state.editor {
2456 if let Some(opened_editor) = state.editor.upgrade() {
2457 editor = Some(opened_editor);
2458 }
2459 }
2460
2461 if let Some(editor) = editor {
2462 self.workspace
2463 .update(cx, |workspace, cx| {
2464 workspace.activate_item(&editor, true, false, cx);
2465 })
2466 .ok();
2467 } else {
2468 patch_state.update_task = Some(cx.spawn(move |this, cx| async move {
2469 Self::open_patch_editor(this, new_patch, cx).await.log_err();
2470 }));
2471 }
2472 }
2473 }
2474 }
2475
2476 fn close_patch_editor(
2477 &mut self,
2478 editor: View<ProposedChangesEditor>,
2479 cx: &mut ViewContext<ContextEditor>,
2480 ) {
2481 self.workspace
2482 .update(cx, |workspace, cx| {
2483 if let Some(pane) = workspace.pane_for(&editor) {
2484 pane.update(cx, |pane, cx| {
2485 let item_id = editor.entity_id();
2486 if !editor.read(cx).focus_handle(cx).is_focused(cx) {
2487 pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
2488 .detach_and_log_err(cx);
2489 }
2490 });
2491 }
2492 })
2493 .ok();
2494 }
2495
2496 async fn open_patch_editor(
2497 this: WeakView<Self>,
2498 patch: AssistantPatch,
2499 mut cx: AsyncWindowContext,
2500 ) -> Result<()> {
2501 let project = this.update(&mut cx, |this, _| this.project.clone())?;
2502 let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
2503
2504 let editor = cx.new_view(|cx| {
2505 let editor = ProposedChangesEditor::new(
2506 patch.title.clone(),
2507 resolved_patch
2508 .edit_groups
2509 .iter()
2510 .map(|(buffer, groups)| ProposedChangeLocation {
2511 buffer: buffer.clone(),
2512 ranges: groups
2513 .iter()
2514 .map(|group| group.context_range.clone())
2515 .collect(),
2516 })
2517 .collect(),
2518 Some(project.clone()),
2519 cx,
2520 );
2521 resolved_patch.apply(&editor, cx);
2522 editor
2523 })?;
2524
2525 this.update(&mut cx, |this, cx| {
2526 if let Some(patch_state) = this.patches.get_mut(&patch.range) {
2527 patch_state.editor = Some(PatchEditorState {
2528 editor: editor.downgrade(),
2529 opened_patch: patch,
2530 });
2531 patch_state.update_task.take();
2532 }
2533
2534 this.workspace
2535 .update(cx, |workspace, cx| {
2536 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2537 })
2538 .log_err();
2539 })?;
2540
2541 Ok(())
2542 }
2543
2544 async fn update_patch_editor(
2545 this: WeakView<Self>,
2546 patch: AssistantPatch,
2547 mut cx: AsyncWindowContext,
2548 ) -> Result<()> {
2549 let project = this.update(&mut cx, |this, _| this.project.clone())?;
2550 let resolved_patch = patch.resolve(project.clone(), &mut cx).await;
2551 this.update(&mut cx, |this, cx| {
2552 let patch_state = this.patches.get_mut(&patch.range)?;
2553
2554 let locations = resolved_patch
2555 .edit_groups
2556 .iter()
2557 .map(|(buffer, groups)| ProposedChangeLocation {
2558 buffer: buffer.clone(),
2559 ranges: groups
2560 .iter()
2561 .map(|group| group.context_range.clone())
2562 .collect(),
2563 })
2564 .collect();
2565
2566 if let Some(state) = &mut patch_state.editor {
2567 if let Some(editor) = state.editor.upgrade() {
2568 editor.update(cx, |editor, cx| {
2569 editor.set_title(patch.title.clone(), cx);
2570 editor.reset_locations(locations, cx);
2571 resolved_patch.apply(editor, cx);
2572 });
2573
2574 state.opened_patch = patch;
2575 } else {
2576 patch_state.editor.take();
2577 }
2578 }
2579 patch_state.update_task.take();
2580
2581 Some(())
2582 })?;
2583 Ok(())
2584 }
2585
2586 fn handle_editor_search_event(
2587 &mut self,
2588 _: View<Editor>,
2589 event: &SearchEvent,
2590 cx: &mut ViewContext<Self>,
2591 ) {
2592 cx.emit(event.clone());
2593 }
2594
2595 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2596 self.editor.update(cx, |editor, cx| {
2597 let snapshot = editor.snapshot(cx);
2598 let cursor = editor.selections.newest_anchor().head();
2599 let cursor_row = cursor
2600 .to_display_point(&snapshot.display_snapshot)
2601 .row()
2602 .as_f32();
2603 let scroll_position = editor
2604 .scroll_manager
2605 .anchor()
2606 .scroll_position(&snapshot.display_snapshot);
2607
2608 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2609 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2610 Some(ScrollPosition {
2611 cursor,
2612 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2613 })
2614 } else {
2615 None
2616 }
2617 })
2618 }
2619
2620 fn esc_kbd(cx: &WindowContext) -> Div {
2621 let colors = cx.theme().colors().clone();
2622
2623 h_flex()
2624 .items_center()
2625 .gap_1()
2626 .font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
2627 .text_size(TextSize::XSmall.rems(cx))
2628 .text_color(colors.text_muted)
2629 .child("Press")
2630 .child(
2631 h_flex()
2632 .rounded_md()
2633 .px_1()
2634 .mr_0p5()
2635 .border_1()
2636 .border_color(theme::color_alpha(colors.border_variant, 0.6))
2637 .bg(theme::color_alpha(colors.element_background, 0.6))
2638 .child("esc"),
2639 )
2640 .child("to cancel")
2641 }
2642
2643 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2644 self.editor.update(cx, |editor, cx| {
2645 let buffer = editor.buffer().read(cx).snapshot(cx);
2646
2647 let excerpt_id = *buffer.as_singleton().unwrap().0;
2648 let mut old_blocks = std::mem::take(&mut self.blocks);
2649 let mut blocks_to_remove: HashMap<_, _> = old_blocks
2650 .iter()
2651 .map(|(message_id, (_, block_id))| (*message_id, *block_id))
2652 .collect();
2653 let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
2654
2655 let render_block = |message: MessageMetadata| -> RenderBlock {
2656 Arc::new({
2657 let context = self.context.clone();
2658
2659 move |cx| {
2660 let message_id = MessageId(message.timestamp);
2661 let llm_loading = message.role == Role::Assistant
2662 && message.status == MessageStatus::Pending;
2663
2664 let (label, spinner, note) = match message.role {
2665 Role::User => (
2666 Label::new("You").color(Color::Default).into_any_element(),
2667 None,
2668 None,
2669 ),
2670 Role::Assistant => {
2671 let base_label = Label::new("Assistant").color(Color::Info);
2672 let mut spinner = None;
2673 let mut note = None;
2674 let animated_label = if llm_loading {
2675 base_label
2676 .with_animation(
2677 "pulsating-label",
2678 Animation::new(Duration::from_secs(2))
2679 .repeat()
2680 .with_easing(pulsating_between(0.4, 0.8)),
2681 |label, delta| label.alpha(delta),
2682 )
2683 .into_any_element()
2684 } else {
2685 base_label.into_any_element()
2686 };
2687 if llm_loading {
2688 spinner = Some(
2689 Icon::new(IconName::ArrowCircle)
2690 .size(IconSize::XSmall)
2691 .color(Color::Info)
2692 .with_animation(
2693 "arrow-circle",
2694 Animation::new(Duration::from_secs(2)).repeat(),
2695 |icon, delta| {
2696 icon.transform(Transformation::rotate(
2697 percentage(delta),
2698 ))
2699 },
2700 )
2701 .into_any_element(),
2702 );
2703 note = Some(Self::esc_kbd(cx).into_any_element());
2704 }
2705 (animated_label, spinner, note)
2706 }
2707 Role::System => (
2708 Label::new("System")
2709 .color(Color::Warning)
2710 .into_any_element(),
2711 None,
2712 None,
2713 ),
2714 };
2715
2716 let sender = h_flex()
2717 .items_center()
2718 .gap_2p5()
2719 .child(
2720 ButtonLike::new("role")
2721 .style(ButtonStyle::Filled)
2722 .child(
2723 h_flex()
2724 .items_center()
2725 .gap_1p5()
2726 .child(label)
2727 .children(spinner),
2728 )
2729 .tooltip(|cx| {
2730 Tooltip::with_meta(
2731 "Toggle message role",
2732 None,
2733 "Available roles: You (User), Assistant, System",
2734 cx,
2735 )
2736 })
2737 .on_click({
2738 let context = context.clone();
2739 move |_, cx| {
2740 context.update(cx, |context, cx| {
2741 context.cycle_message_roles(
2742 HashSet::from_iter(Some(message_id)),
2743 cx,
2744 )
2745 })
2746 }
2747 }),
2748 )
2749 .children(note);
2750
2751 h_flex()
2752 .id(("message_header", message_id.as_u64()))
2753 .pl(cx.gutter_dimensions.full_width())
2754 .h_11()
2755 .w_full()
2756 .relative()
2757 .gap_1p5()
2758 .child(sender)
2759 .children(match &message.cache {
2760 Some(cache) if cache.is_final_anchor => match cache.status {
2761 CacheStatus::Cached => Some(
2762 div()
2763 .id("cached")
2764 .child(
2765 Icon::new(IconName::DatabaseZap)
2766 .size(IconSize::XSmall)
2767 .color(Color::Hint),
2768 )
2769 .tooltip(|cx| {
2770 Tooltip::with_meta(
2771 "Context Cached",
2772 None,
2773 "Large messages cached to optimize performance",
2774 cx,
2775 )
2776 })
2777 .into_any_element(),
2778 ),
2779 CacheStatus::Pending => Some(
2780 div()
2781 .child(
2782 Icon::new(IconName::Ellipsis)
2783 .size(IconSize::XSmall)
2784 .color(Color::Hint),
2785 )
2786 .into_any_element(),
2787 ),
2788 },
2789 _ => None,
2790 })
2791 .children(match &message.status {
2792 MessageStatus::Error(error) => Some(
2793 Button::new("show-error", "Error")
2794 .color(Color::Error)
2795 .selected_label_color(Color::Error)
2796 .selected_icon_color(Color::Error)
2797 .icon(IconName::XCircle)
2798 .icon_color(Color::Error)
2799 .icon_size(IconSize::XSmall)
2800 .icon_position(IconPosition::Start)
2801 .tooltip(move |cx| Tooltip::text("View Details", cx))
2802 .on_click({
2803 let context = context.clone();
2804 let error = error.clone();
2805 move |_, cx| {
2806 context.update(cx, |_, cx| {
2807 cx.emit(ContextEvent::ShowAssistError(
2808 error.clone(),
2809 ));
2810 });
2811 }
2812 })
2813 .into_any_element(),
2814 ),
2815 MessageStatus::Canceled => Some(
2816 h_flex()
2817 .gap_1()
2818 .items_center()
2819 .child(
2820 Icon::new(IconName::XCircle)
2821 .color(Color::Disabled)
2822 .size(IconSize::XSmall),
2823 )
2824 .child(
2825 Label::new("Canceled")
2826 .size(LabelSize::Small)
2827 .color(Color::Disabled),
2828 )
2829 .into_any_element(),
2830 ),
2831 _ => None,
2832 })
2833 .into_any_element()
2834 }
2835 })
2836 };
2837 let create_block_properties = |message: &Message| BlockProperties {
2838 height: 2,
2839 style: BlockStyle::Sticky,
2840 placement: BlockPlacement::Above(
2841 buffer
2842 .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
2843 .unwrap(),
2844 ),
2845 priority: usize::MAX,
2846 render: render_block(MessageMetadata::from(message)),
2847 };
2848 let mut new_blocks = vec![];
2849 let mut block_index_to_message = vec![];
2850 for message in self.context.read(cx).messages(cx) {
2851 if let Some(_) = blocks_to_remove.remove(&message.id) {
2852 // This is an old message that we might modify.
2853 let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
2854 debug_assert!(
2855 false,
2856 "old_blocks should contain a message_id we've just removed."
2857 );
2858 continue;
2859 };
2860 // Should we modify it?
2861 let message_meta = MessageMetadata::from(&message);
2862 if meta != &message_meta {
2863 blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
2864 *meta = message_meta;
2865 }
2866 } else {
2867 // This is a new message.
2868 new_blocks.push(create_block_properties(&message));
2869 block_index_to_message.push((message.id, MessageMetadata::from(&message)));
2870 }
2871 }
2872 editor.replace_blocks(blocks_to_replace, None, cx);
2873 editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
2874
2875 let ids = editor.insert_blocks(new_blocks, None, cx);
2876 old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
2877 |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
2878 ));
2879 self.blocks = old_blocks;
2880 });
2881 }
2882
2883 /// Returns either the selected text, or the content of the Markdown code
2884 /// block surrounding the cursor.
2885 fn get_selection_or_code_block(
2886 context_editor_view: &View<ContextEditor>,
2887 cx: &mut ViewContext<Workspace>,
2888 ) -> Option<(String, bool)> {
2889 const CODE_FENCE_DELIMITER: &'static str = "```";
2890
2891 let context_editor = context_editor_view.read(cx).editor.clone();
2892 context_editor.update(cx, |context_editor, cx| {
2893 if context_editor.selections.newest::<Point>(cx).is_empty() {
2894 let snapshot = context_editor.buffer().read(cx).snapshot(cx);
2895 let (_, _, snapshot) = snapshot.as_singleton()?;
2896
2897 let head = context_editor.selections.newest::<Point>(cx).head();
2898 let offset = snapshot.point_to_offset(head);
2899
2900 let surrounding_code_block_range = find_surrounding_code_block(snapshot, offset)?;
2901 let mut text = snapshot
2902 .text_for_range(surrounding_code_block_range)
2903 .collect::<String>();
2904
2905 // If there is no newline trailing the closing three-backticks, then
2906 // tree-sitter-md extends the range of the content node to include
2907 // the backticks.
2908 if text.ends_with(CODE_FENCE_DELIMITER) {
2909 text.drain((text.len() - CODE_FENCE_DELIMITER.len())..);
2910 }
2911
2912 (!text.is_empty()).then_some((text, true))
2913 } else {
2914 let anchor = context_editor.selections.newest_anchor();
2915 let text = context_editor
2916 .buffer()
2917 .read(cx)
2918 .read(cx)
2919 .text_for_range(anchor.range())
2920 .collect::<String>();
2921
2922 (!text.is_empty()).then_some((text, false))
2923 }
2924 })
2925 }
2926
2927 fn insert_selection(
2928 workspace: &mut Workspace,
2929 _: &InsertIntoEditor,
2930 cx: &mut ViewContext<Workspace>,
2931 ) {
2932 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2933 return;
2934 };
2935 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2936 return;
2937 };
2938 let Some(active_editor_view) = workspace
2939 .active_item(cx)
2940 .and_then(|item| item.act_as::<Editor>(cx))
2941 else {
2942 return;
2943 };
2944
2945 if let Some((text, _)) = Self::get_selection_or_code_block(&context_editor_view, cx) {
2946 active_editor_view.update(cx, |editor, cx| {
2947 editor.insert(&text, cx);
2948 editor.focus(cx);
2949 })
2950 }
2951 }
2952
2953 fn copy_code(workspace: &mut Workspace, _: &CopyCode, cx: &mut ViewContext<Workspace>) {
2954 let result = maybe!({
2955 let panel = workspace.panel::<AssistantPanel>(cx)?;
2956 let context_editor_view = panel.read(cx).active_context_editor(cx)?;
2957 Self::get_selection_or_code_block(&context_editor_view, cx)
2958 });
2959 let Some((text, is_code_block)) = result else {
2960 return;
2961 };
2962
2963 cx.write_to_clipboard(ClipboardItem::new_string(text));
2964
2965 struct CopyToClipboardToast;
2966 workspace.show_toast(
2967 Toast::new(
2968 NotificationId::unique::<CopyToClipboardToast>(),
2969 format!(
2970 "{} copied to clipboard.",
2971 if is_code_block {
2972 "Code block"
2973 } else {
2974 "Selection"
2975 }
2976 ),
2977 )
2978 .autohide(),
2979 cx,
2980 );
2981 }
2982
2983 fn insert_dragged_files(
2984 workspace: &mut Workspace,
2985 action: &InsertDraggedFiles,
2986 cx: &mut ViewContext<Workspace>,
2987 ) {
2988 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2989 return;
2990 };
2991 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2992 return;
2993 };
2994
2995 let project = workspace.project().clone();
2996
2997 let paths = match action {
2998 InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
2999 InsertDraggedFiles::ExternalFiles(paths) => {
3000 let tasks = paths
3001 .clone()
3002 .into_iter()
3003 .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
3004 .collect::<Vec<_>>();
3005
3006 cx.spawn(move |_, cx| async move {
3007 let mut paths = vec![];
3008 let mut worktrees = vec![];
3009
3010 let opened_paths = futures::future::join_all(tasks).await;
3011 for (worktree, project_path) in opened_paths.into_iter().flatten() {
3012 let Ok(worktree_root_name) =
3013 worktree.read_with(&cx, |worktree, _| worktree.root_name().to_string())
3014 else {
3015 continue;
3016 };
3017
3018 let mut full_path = PathBuf::from(worktree_root_name.clone());
3019 full_path.push(&project_path.path);
3020 paths.push(full_path);
3021 worktrees.push(worktree);
3022 }
3023
3024 (paths, worktrees)
3025 })
3026 }
3027 };
3028
3029 cx.spawn(|_, mut cx| async move {
3030 let (paths, dragged_file_worktrees) = paths.await;
3031 let cmd_name = file_command::FileSlashCommand.name();
3032
3033 context_editor_view
3034 .update(&mut cx, |context_editor, cx| {
3035 let file_argument = paths
3036 .into_iter()
3037 .map(|path| path.to_string_lossy().to_string())
3038 .collect::<Vec<_>>()
3039 .join(" ");
3040
3041 context_editor.editor.update(cx, |editor, cx| {
3042 editor.insert("\n", cx);
3043 editor.insert(&format!("/{} {}", cmd_name, file_argument), cx);
3044 });
3045
3046 context_editor.confirm_command(&ConfirmCommand, cx);
3047
3048 context_editor
3049 .dragged_file_worktrees
3050 .extend(dragged_file_worktrees);
3051 })
3052 .log_err();
3053 })
3054 .detach();
3055 }
3056
3057 fn quote_selection(
3058 workspace: &mut Workspace,
3059 _: &QuoteSelection,
3060 cx: &mut ViewContext<Workspace>,
3061 ) {
3062 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
3063 return;
3064 };
3065
3066 let Some(creases) = selections_creases(workspace, cx) else {
3067 return;
3068 };
3069
3070 if creases.is_empty() {
3071 return;
3072 }
3073 // Activate the panel
3074 if !panel.focus_handle(cx).contains_focused(cx) {
3075 workspace.toggle_panel_focus::<AssistantPanel>(cx);
3076 }
3077
3078 panel.update(cx, |_, cx| {
3079 // Wait to create a new context until the workspace is no longer
3080 // being updated.
3081 cx.defer(move |panel, cx| {
3082 if let Some(context) = panel
3083 .active_context_editor(cx)
3084 .or_else(|| panel.new_context(cx))
3085 {
3086 context.update(cx, |context, cx| {
3087 context.editor.update(cx, |editor, cx| {
3088 editor.insert("\n", cx);
3089 for (text, crease_title) in creases {
3090 let point = editor.selections.newest::<Point>(cx).head();
3091 let start_row = MultiBufferRow(point.row);
3092
3093 editor.insert(&text, cx);
3094
3095 let snapshot = editor.buffer().read(cx).snapshot(cx);
3096 let anchor_before = snapshot.anchor_after(point);
3097 let anchor_after = editor
3098 .selections
3099 .newest_anchor()
3100 .head()
3101 .bias_left(&snapshot);
3102
3103 editor.insert("\n", cx);
3104
3105 let fold_placeholder = quote_selection_fold_placeholder(
3106 crease_title,
3107 cx.view().downgrade(),
3108 );
3109 let crease = Crease::inline(
3110 anchor_before..anchor_after,
3111 fold_placeholder,
3112 render_quote_selection_output_toggle,
3113 |_, _, _| Empty.into_any(),
3114 );
3115 editor.insert_creases(vec![crease], cx);
3116 editor.fold_at(
3117 &FoldAt {
3118 buffer_row: start_row,
3119 },
3120 cx,
3121 );
3122 }
3123 })
3124 });
3125 };
3126 });
3127 });
3128 }
3129
3130 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
3131 if self.editor.read(cx).selections.count() == 1 {
3132 let (copied_text, metadata, _) = self.get_clipboard_contents(cx);
3133 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
3134 copied_text,
3135 metadata,
3136 ));
3137 cx.stop_propagation();
3138 return;
3139 }
3140
3141 cx.propagate();
3142 }
3143
3144 fn cut(&mut self, _: &editor::actions::Cut, cx: &mut ViewContext<Self>) {
3145 if self.editor.read(cx).selections.count() == 1 {
3146 let (copied_text, metadata, selections) = self.get_clipboard_contents(cx);
3147
3148 self.editor.update(cx, |editor, cx| {
3149 editor.transact(cx, |this, cx| {
3150 this.change_selections(Some(Autoscroll::fit()), cx, |s| {
3151 s.select(selections);
3152 });
3153 this.insert("", cx);
3154 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
3155 copied_text,
3156 metadata,
3157 ));
3158 });
3159 });
3160
3161 cx.stop_propagation();
3162 return;
3163 }
3164
3165 cx.propagate();
3166 }
3167
3168 fn get_clipboard_contents(
3169 &mut self,
3170 cx: &mut ViewContext<Self>,
3171 ) -> (String, CopyMetadata, Vec<text::Selection<usize>>) {
3172 let (snapshot, selection, creases) = self.editor.update(cx, |editor, cx| {
3173 let mut selection = editor.selections.newest::<Point>(cx);
3174 let snapshot = editor.buffer().read(cx).snapshot(cx);
3175
3176 let is_entire_line = selection.is_empty() || editor.selections.line_mode;
3177 if is_entire_line {
3178 selection.start = Point::new(selection.start.row, 0);
3179 selection.end =
3180 cmp::min(snapshot.max_point(), Point::new(selection.start.row + 1, 0));
3181 selection.goal = SelectionGoal::None;
3182 }
3183
3184 let selection_start = snapshot.point_to_offset(selection.start);
3185
3186 (
3187 snapshot.clone(),
3188 selection.clone(),
3189 editor.display_map.update(cx, |display_map, cx| {
3190 display_map
3191 .snapshot(cx)
3192 .crease_snapshot
3193 .creases_in_range(
3194 MultiBufferRow(selection.start.row)
3195 ..MultiBufferRow(selection.end.row + 1),
3196 &snapshot,
3197 )
3198 .filter_map(|crease| {
3199 if let Crease::Inline {
3200 range, metadata, ..
3201 } = &crease
3202 {
3203 let metadata = metadata.as_ref()?;
3204 let start = range
3205 .start
3206 .to_offset(&snapshot)
3207 .saturating_sub(selection_start);
3208 let end = range
3209 .end
3210 .to_offset(&snapshot)
3211 .saturating_sub(selection_start);
3212
3213 let range_relative_to_selection = start..end;
3214 if !range_relative_to_selection.is_empty() {
3215 return Some(SelectedCreaseMetadata {
3216 range_relative_to_selection,
3217 crease: metadata.clone(),
3218 });
3219 }
3220 }
3221 None
3222 })
3223 .collect::<Vec<_>>()
3224 }),
3225 )
3226 });
3227
3228 let selection = selection.map(|point| snapshot.point_to_offset(point));
3229 let context = self.context.read(cx);
3230
3231 let mut text = String::new();
3232 for message in context.messages(cx) {
3233 if message.offset_range.start >= selection.range().end {
3234 break;
3235 } else if message.offset_range.end >= selection.range().start {
3236 let range = cmp::max(message.offset_range.start, selection.range().start)
3237 ..cmp::min(message.offset_range.end, selection.range().end);
3238 if !range.is_empty() {
3239 for chunk in context.buffer().read(cx).text_for_range(range) {
3240 text.push_str(chunk);
3241 }
3242 if message.offset_range.end < selection.range().end {
3243 text.push('\n');
3244 }
3245 }
3246 }
3247 }
3248
3249 (text, CopyMetadata { creases }, vec![selection])
3250 }
3251
3252 fn paste(&mut self, action: &editor::actions::Paste, cx: &mut ViewContext<Self>) {
3253 cx.stop_propagation();
3254
3255 let images = if let Some(item) = cx.read_from_clipboard() {
3256 item.into_entries()
3257 .filter_map(|entry| {
3258 if let ClipboardEntry::Image(image) = entry {
3259 Some(image)
3260 } else {
3261 None
3262 }
3263 })
3264 .collect()
3265 } else {
3266 Vec::new()
3267 };
3268
3269 let metadata = if let Some(item) = cx.read_from_clipboard() {
3270 item.entries().first().and_then(|entry| {
3271 if let ClipboardEntry::String(text) = entry {
3272 text.metadata_json::<CopyMetadata>()
3273 } else {
3274 None
3275 }
3276 })
3277 } else {
3278 None
3279 };
3280
3281 if images.is_empty() {
3282 self.editor.update(cx, |editor, cx| {
3283 let paste_position = editor.selections.newest::<usize>(cx).head();
3284 editor.paste(action, cx);
3285
3286 if let Some(metadata) = metadata {
3287 let buffer = editor.buffer().read(cx).snapshot(cx);
3288
3289 let mut buffer_rows_to_fold = BTreeSet::new();
3290 let weak_editor = cx.view().downgrade();
3291 editor.insert_creases(
3292 metadata.creases.into_iter().map(|metadata| {
3293 let start = buffer.anchor_after(
3294 paste_position + metadata.range_relative_to_selection.start,
3295 );
3296 let end = buffer.anchor_before(
3297 paste_position + metadata.range_relative_to_selection.end,
3298 );
3299
3300 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
3301 buffer_rows_to_fold.insert(buffer_row);
3302 Crease::inline(
3303 start..end,
3304 FoldPlaceholder {
3305 render: render_fold_icon_button(
3306 weak_editor.clone(),
3307 metadata.crease.icon,
3308 metadata.crease.label.clone(),
3309 ),
3310 ..Default::default()
3311 },
3312 render_slash_command_output_toggle,
3313 |_, _, _| Empty.into_any(),
3314 )
3315 .with_metadata(metadata.crease.clone())
3316 }),
3317 cx,
3318 );
3319 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
3320 editor.fold_at(&FoldAt { buffer_row }, cx);
3321 }
3322 }
3323 });
3324 } else {
3325 let mut image_positions = Vec::new();
3326 self.editor.update(cx, |editor, cx| {
3327 editor.transact(cx, |editor, cx| {
3328 let edits = editor
3329 .selections
3330 .all::<usize>(cx)
3331 .into_iter()
3332 .map(|selection| (selection.start..selection.end, "\n"));
3333 editor.edit(edits, cx);
3334
3335 let snapshot = editor.buffer().read(cx).snapshot(cx);
3336 for selection in editor.selections.all::<usize>(cx) {
3337 image_positions.push(snapshot.anchor_before(selection.end));
3338 }
3339 });
3340 });
3341
3342 self.context.update(cx, |context, cx| {
3343 for image in images {
3344 let Some(render_image) = image.to_image_data(cx.svg_renderer()).log_err()
3345 else {
3346 continue;
3347 };
3348 let image_id = image.id();
3349 let image_task = LanguageModelImage::from_image(image, cx).shared();
3350
3351 for image_position in image_positions.iter() {
3352 context.insert_content(
3353 Content::Image {
3354 anchor: image_position.text_anchor,
3355 image_id,
3356 image: image_task.clone(),
3357 render_image: render_image.clone(),
3358 },
3359 cx,
3360 );
3361 }
3362 }
3363 });
3364 }
3365 }
3366
3367 fn update_image_blocks(&mut self, cx: &mut ViewContext<Self>) {
3368 self.editor.update(cx, |editor, cx| {
3369 let buffer = editor.buffer().read(cx).snapshot(cx);
3370 let excerpt_id = *buffer.as_singleton().unwrap().0;
3371 let old_blocks = std::mem::take(&mut self.image_blocks);
3372 let new_blocks = self
3373 .context
3374 .read(cx)
3375 .contents(cx)
3376 .filter_map(|content| {
3377 if let Content::Image {
3378 anchor,
3379 render_image,
3380 ..
3381 } = content
3382 {
3383 Some((anchor, render_image))
3384 } else {
3385 None
3386 }
3387 })
3388 .filter_map(|(anchor, render_image)| {
3389 const MAX_HEIGHT_IN_LINES: u32 = 8;
3390 let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
3391 let image = render_image.clone();
3392 anchor.is_valid(&buffer).then(|| BlockProperties {
3393 placement: BlockPlacement::Above(anchor),
3394 height: MAX_HEIGHT_IN_LINES,
3395 style: BlockStyle::Sticky,
3396 render: Arc::new(move |cx| {
3397 let image_size = size_for_image(
3398 &image,
3399 size(
3400 cx.max_width - cx.gutter_dimensions.full_width(),
3401 MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
3402 ),
3403 );
3404 h_flex()
3405 .pl(cx.gutter_dimensions.full_width())
3406 .child(
3407 img(image.clone())
3408 .object_fit(gpui::ObjectFit::ScaleDown)
3409 .w(image_size.width)
3410 .h(image_size.height),
3411 )
3412 .into_any_element()
3413 }),
3414 priority: 0,
3415 })
3416 })
3417 .collect::<Vec<_>>();
3418
3419 editor.remove_blocks(old_blocks, None, cx);
3420 let ids = editor.insert_blocks(new_blocks, None, cx);
3421 self.image_blocks = HashSet::from_iter(ids);
3422 });
3423 }
3424
3425 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
3426 self.context.update(cx, |context, cx| {
3427 let selections = self.editor.read(cx).selections.disjoint_anchors();
3428 for selection in selections.as_ref() {
3429 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
3430 let range = selection
3431 .map(|endpoint| endpoint.to_offset(&buffer))
3432 .range();
3433 context.split_message(range, cx);
3434 }
3435 });
3436 }
3437
3438 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3439 self.context.update(cx, |context, cx| {
3440 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
3441 });
3442 }
3443
3444 fn title(&self, cx: &AppContext) -> Cow<str> {
3445 self.context
3446 .read(cx)
3447 .summary()
3448 .map(|summary| summary.text.clone())
3449 .map(Cow::Owned)
3450 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
3451 }
3452
3453 fn render_patch_block(
3454 &mut self,
3455 range: Range<text::Anchor>,
3456 max_width: Pixels,
3457 gutter_width: Pixels,
3458 id: BlockId,
3459 selected: bool,
3460 cx: &mut ViewContext<Self>,
3461 ) -> Option<AnyElement> {
3462 let snapshot = self.editor.update(cx, |editor, cx| editor.snapshot(cx));
3463 let (excerpt_id, _buffer_id, _) = snapshot.buffer_snapshot.as_singleton().unwrap();
3464 let excerpt_id = *excerpt_id;
3465 let anchor = snapshot
3466 .buffer_snapshot
3467 .anchor_in_excerpt(excerpt_id, range.start)
3468 .unwrap();
3469
3470 let theme = cx.theme().clone();
3471 let patch = self.context.read(cx).patch_for_range(&range, cx)?;
3472 let paths = patch
3473 .paths()
3474 .map(|p| SharedString::from(p.to_string()))
3475 .collect::<BTreeSet<_>>();
3476
3477 Some(
3478 v_flex()
3479 .id(id)
3480 .bg(theme.colors().editor_background)
3481 .ml(gutter_width)
3482 .pb_1()
3483 .w(max_width - gutter_width)
3484 .rounded_md()
3485 .border_1()
3486 .border_color(theme.colors().border_variant)
3487 .overflow_hidden()
3488 .hover(|style| style.border_color(theme.colors().text_accent))
3489 .when(selected, |this| {
3490 this.border_color(theme.colors().text_accent)
3491 })
3492 .cursor(CursorStyle::PointingHand)
3493 .on_click(cx.listener(move |this, _, cx| {
3494 this.editor.update(cx, |editor, cx| {
3495 editor.change_selections(None, cx, |selections| {
3496 selections.select_ranges(vec![anchor..anchor]);
3497 });
3498 });
3499 this.focus_active_patch(cx);
3500 }))
3501 .child(
3502 div()
3503 .px_2()
3504 .py_1()
3505 .overflow_hidden()
3506 .text_ellipsis()
3507 .border_b_1()
3508 .border_color(theme.colors().border_variant)
3509 .bg(theme.colors().element_background)
3510 .child(
3511 Label::new(patch.title.clone())
3512 .size(LabelSize::Small)
3513 .color(Color::Muted),
3514 ),
3515 )
3516 .children(paths.into_iter().map(|path| {
3517 h_flex()
3518 .px_2()
3519 .pt_1()
3520 .gap_1p5()
3521 .child(Icon::new(IconName::File).size(IconSize::Small))
3522 .child(Label::new(path).size(LabelSize::Small))
3523 }))
3524 .when(patch.status == AssistantPatchStatus::Pending, |div| {
3525 div.child(
3526 h_flex()
3527 .pt_1()
3528 .px_2()
3529 .gap_1()
3530 .child(
3531 Icon::new(IconName::ArrowCircle)
3532 .size(IconSize::XSmall)
3533 .color(Color::Muted)
3534 .with_animation(
3535 "arrow-circle",
3536 Animation::new(Duration::from_secs(2)).repeat(),
3537 |icon, delta| {
3538 icon.transform(Transformation::rotate(percentage(
3539 delta,
3540 )))
3541 },
3542 ),
3543 )
3544 .child(
3545 Label::new("Generating…")
3546 .color(Color::Muted)
3547 .size(LabelSize::Small)
3548 .with_animation(
3549 "pulsating-label",
3550 Animation::new(Duration::from_secs(2))
3551 .repeat()
3552 .with_easing(pulsating_between(0.4, 0.8)),
3553 |label, delta| label.alpha(delta),
3554 ),
3555 ),
3556 )
3557 })
3558 .into_any(),
3559 )
3560 }
3561
3562 fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
3563 use feature_flags::FeatureFlagAppExt;
3564 let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
3565 assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
3566 });
3567
3568 if nudge.map_or(false, |value| value) {
3569 Some(
3570 h_flex()
3571 .p_3()
3572 .border_b_1()
3573 .border_color(cx.theme().colors().border_variant)
3574 .bg(cx.theme().colors().editor_background)
3575 .justify_between()
3576 .child(
3577 h_flex()
3578 .gap_3()
3579 .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
3580 .child(Label::new("Zed AI is here! Get started by signing in →")),
3581 )
3582 .child(
3583 Button::new("sign-in", "Sign in")
3584 .size(ButtonSize::Compact)
3585 .style(ButtonStyle::Filled)
3586 .on_click(cx.listener(|this, _event, cx| {
3587 let client = this
3588 .workspace
3589 .update(cx, |workspace, _| workspace.client().clone())
3590 .log_err();
3591
3592 if let Some(client) = client {
3593 cx.spawn(|this, mut cx| async move {
3594 client.authenticate_and_connect(true, &mut cx).await?;
3595 this.update(&mut cx, |_, cx| cx.notify())
3596 })
3597 .detach_and_log_err(cx)
3598 }
3599 })),
3600 )
3601 .into_any_element(),
3602 )
3603 } else if let Some(configuration_error) = configuration_error(cx) {
3604 let label = match configuration_error {
3605 ConfigurationError::NoProvider => "No LLM provider selected.",
3606 ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
3607 };
3608 Some(
3609 h_flex()
3610 .px_3()
3611 .py_2()
3612 .border_b_1()
3613 .border_color(cx.theme().colors().border_variant)
3614 .bg(cx.theme().colors().editor_background)
3615 .justify_between()
3616 .child(
3617 h_flex()
3618 .gap_3()
3619 .child(
3620 Icon::new(IconName::Warning)
3621 .size(IconSize::Small)
3622 .color(Color::Warning),
3623 )
3624 .child(Label::new(label)),
3625 )
3626 .child(
3627 Button::new("open-configuration", "Configure Providers")
3628 .size(ButtonSize::Compact)
3629 .icon(Some(IconName::SlidersVertical))
3630 .icon_size(IconSize::Small)
3631 .icon_position(IconPosition::Start)
3632 .style(ButtonStyle::Filled)
3633 .on_click({
3634 let focus_handle = self.focus_handle(cx).clone();
3635 move |_event, cx| {
3636 focus_handle.dispatch_action(&ShowConfiguration, cx);
3637 }
3638 }),
3639 )
3640 .into_any_element(),
3641 )
3642 } else {
3643 None
3644 }
3645 }
3646
3647 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3648 let focus_handle = self.focus_handle(cx).clone();
3649
3650 let (style, tooltip) = match token_state(&self.context, cx) {
3651 Some(TokenState::NoTokensLeft { .. }) => (
3652 ButtonStyle::Tinted(TintColor::Negative),
3653 Some(Tooltip::text("Token limit reached", cx)),
3654 ),
3655 Some(TokenState::HasMoreTokens {
3656 over_warn_threshold,
3657 ..
3658 }) => {
3659 let (style, tooltip) = if over_warn_threshold {
3660 (
3661 ButtonStyle::Tinted(TintColor::Warning),
3662 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
3663 )
3664 } else {
3665 (ButtonStyle::Filled, None)
3666 };
3667 (style, tooltip)
3668 }
3669 None => (ButtonStyle::Filled, None),
3670 };
3671
3672 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3673
3674 let has_configuration_error = configuration_error(cx).is_some();
3675 let needs_to_accept_terms = self.show_accept_terms
3676 && provider
3677 .as_ref()
3678 .map_or(false, |provider| provider.must_accept_terms(cx));
3679 let disabled = has_configuration_error || needs_to_accept_terms;
3680
3681 ButtonLike::new("send_button")
3682 .disabled(disabled)
3683 .style(style)
3684 .when_some(tooltip, |button, tooltip| {
3685 button.tooltip(move |_| tooltip.clone())
3686 })
3687 .layer(ElevationIndex::ModalSurface)
3688 .child(Label::new(
3689 if AssistantSettings::get_global(cx).are_live_diffs_enabled(cx) {
3690 "Chat"
3691 } else {
3692 "Send"
3693 },
3694 ))
3695 .children(
3696 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
3697 .map(|binding| binding.into_any_element()),
3698 )
3699 .on_click(move |_event, cx| {
3700 focus_handle.dispatch_action(&Assist, cx);
3701 })
3702 }
3703
3704 fn render_edit_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3705 let focus_handle = self.focus_handle(cx).clone();
3706
3707 let (style, tooltip) = match token_state(&self.context, cx) {
3708 Some(TokenState::NoTokensLeft { .. }) => (
3709 ButtonStyle::Tinted(TintColor::Negative),
3710 Some(Tooltip::text("Token limit reached", cx)),
3711 ),
3712 Some(TokenState::HasMoreTokens {
3713 over_warn_threshold,
3714 ..
3715 }) => {
3716 let (style, tooltip) = if over_warn_threshold {
3717 (
3718 ButtonStyle::Tinted(TintColor::Warning),
3719 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
3720 )
3721 } else {
3722 (ButtonStyle::Filled, None)
3723 };
3724 (style, tooltip)
3725 }
3726 None => (ButtonStyle::Filled, None),
3727 };
3728
3729 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3730
3731 let has_configuration_error = configuration_error(cx).is_some();
3732 let needs_to_accept_terms = self.show_accept_terms
3733 && provider
3734 .as_ref()
3735 .map_or(false, |provider| provider.must_accept_terms(cx));
3736 let disabled = has_configuration_error || needs_to_accept_terms;
3737
3738 ButtonLike::new("edit_button")
3739 .disabled(disabled)
3740 .style(style)
3741 .when_some(tooltip, |button, tooltip| {
3742 button.tooltip(move |_| tooltip.clone())
3743 })
3744 .layer(ElevationIndex::ModalSurface)
3745 .child(Label::new("Suggest Edits"))
3746 .children(
3747 KeyBinding::for_action_in(&Edit, &focus_handle, cx)
3748 .map(|binding| binding.into_any_element()),
3749 )
3750 .on_click(move |_event, cx| {
3751 focus_handle.dispatch_action(&Edit, cx);
3752 })
3753 }
3754
3755 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3756 slash_command_picker::SlashCommandSelector::new(
3757 self.slash_commands.clone(),
3758 cx.view().downgrade(),
3759 Button::new("trigger", "Add Context")
3760 .icon(IconName::Plus)
3761 .icon_size(IconSize::Small)
3762 .icon_color(Color::Muted)
3763 .icon_position(IconPosition::Start)
3764 .tooltip(|cx| Tooltip::text("Type / to insert via keyboard", cx)),
3765 )
3766 }
3767
3768 fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
3769 let last_error = self.last_error.as_ref()?;
3770
3771 Some(
3772 div()
3773 .absolute()
3774 .right_3()
3775 .bottom_12()
3776 .max_w_96()
3777 .py_2()
3778 .px_3()
3779 .elevation_2(cx)
3780 .occlude()
3781 .child(match last_error {
3782 AssistError::FileRequired => self.render_file_required_error(cx),
3783 AssistError::PaymentRequired => self.render_payment_required_error(cx),
3784 AssistError::MaxMonthlySpendReached => {
3785 self.render_max_monthly_spend_reached_error(cx)
3786 }
3787 AssistError::Message(error_message) => {
3788 self.render_assist_error(error_message, cx)
3789 }
3790 })
3791 .into_any(),
3792 )
3793 }
3794
3795 fn render_file_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
3796 v_flex()
3797 .gap_0p5()
3798 .child(
3799 h_flex()
3800 .gap_1p5()
3801 .items_center()
3802 .child(Icon::new(IconName::Warning).color(Color::Warning))
3803 .child(
3804 Label::new("Suggest Edits needs a file to edit").weight(FontWeight::MEDIUM),
3805 ),
3806 )
3807 .child(
3808 div()
3809 .id("error-message")
3810 .max_h_24()
3811 .overflow_y_scroll()
3812 .child(Label::new(
3813 "To include files, type /file or /tab in your prompt.",
3814 )),
3815 )
3816 .child(
3817 h_flex()
3818 .justify_end()
3819 .mt_1()
3820 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
3821 |this, _, cx| {
3822 this.last_error = None;
3823 cx.notify();
3824 },
3825 ))),
3826 )
3827 .into_any()
3828 }
3829
3830 fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
3831 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.";
3832
3833 v_flex()
3834 .gap_0p5()
3835 .child(
3836 h_flex()
3837 .gap_1p5()
3838 .items_center()
3839 .child(Icon::new(IconName::XCircle).color(Color::Error))
3840 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
3841 )
3842 .child(
3843 div()
3844 .id("error-message")
3845 .max_h_24()
3846 .overflow_y_scroll()
3847 .child(Label::new(ERROR_MESSAGE)),
3848 )
3849 .child(
3850 h_flex()
3851 .justify_end()
3852 .mt_1()
3853 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
3854 |this, _, cx| {
3855 this.last_error = None;
3856 cx.open_url(&zed_urls::account_url(cx));
3857 cx.notify();
3858 },
3859 )))
3860 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
3861 |this, _, cx| {
3862 this.last_error = None;
3863 cx.notify();
3864 },
3865 ))),
3866 )
3867 .into_any()
3868 }
3869
3870 fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
3871 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
3872
3873 v_flex()
3874 .gap_0p5()
3875 .child(
3876 h_flex()
3877 .gap_1p5()
3878 .items_center()
3879 .child(Icon::new(IconName::XCircle).color(Color::Error))
3880 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
3881 )
3882 .child(
3883 div()
3884 .id("error-message")
3885 .max_h_24()
3886 .overflow_y_scroll()
3887 .child(Label::new(ERROR_MESSAGE)),
3888 )
3889 .child(
3890 h_flex()
3891 .justify_end()
3892 .mt_1()
3893 .child(
3894 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
3895 cx.listener(|this, _, cx| {
3896 this.last_error = None;
3897 cx.open_url(&zed_urls::account_url(cx));
3898 cx.notify();
3899 }),
3900 ),
3901 )
3902 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
3903 |this, _, cx| {
3904 this.last_error = None;
3905 cx.notify();
3906 },
3907 ))),
3908 )
3909 .into_any()
3910 }
3911
3912 fn render_assist_error(
3913 &self,
3914 error_message: &SharedString,
3915 cx: &mut ViewContext<Self>,
3916 ) -> AnyElement {
3917 v_flex()
3918 .gap_0p5()
3919 .child(
3920 h_flex()
3921 .gap_1p5()
3922 .items_center()
3923 .child(Icon::new(IconName::XCircle).color(Color::Error))
3924 .child(
3925 Label::new("Error interacting with language model")
3926 .weight(FontWeight::MEDIUM),
3927 ),
3928 )
3929 .child(
3930 div()
3931 .id("error-message")
3932 .max_h_32()
3933 .overflow_y_scroll()
3934 .child(Label::new(error_message.clone())),
3935 )
3936 .child(
3937 h_flex()
3938 .justify_end()
3939 .mt_1()
3940 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
3941 |this, _, cx| {
3942 this.last_error = None;
3943 cx.notify();
3944 },
3945 ))),
3946 )
3947 .into_any()
3948 }
3949}
3950
3951/// Returns the contents of the *outermost* fenced code block that contains the given offset.
3952fn find_surrounding_code_block(snapshot: &BufferSnapshot, offset: usize) -> Option<Range<usize>> {
3953 const CODE_BLOCK_NODE: &'static str = "fenced_code_block";
3954 const CODE_BLOCK_CONTENT: &'static str = "code_fence_content";
3955
3956 let layer = snapshot.syntax_layers().next()?;
3957
3958 let root_node = layer.node();
3959 let mut cursor = root_node.walk();
3960
3961 // Go to the first child for the given offset
3962 while cursor.goto_first_child_for_byte(offset).is_some() {
3963 // If we're at the end of the node, go to the next one.
3964 // Example: if you have a fenced-code-block, and you're on the start of the line
3965 // right after the closing ```, you want to skip the fenced-code-block and
3966 // go to the next sibling.
3967 if cursor.node().end_byte() == offset {
3968 cursor.goto_next_sibling();
3969 }
3970
3971 if cursor.node().start_byte() > offset {
3972 break;
3973 }
3974
3975 // We found the fenced code block.
3976 if cursor.node().kind() == CODE_BLOCK_NODE {
3977 // Now we need to find the child node that contains the code.
3978 cursor.goto_first_child();
3979 loop {
3980 if cursor.node().kind() == CODE_BLOCK_CONTENT {
3981 return Some(cursor.node().byte_range());
3982 }
3983 if !cursor.goto_next_sibling() {
3984 break;
3985 }
3986 }
3987 }
3988 }
3989
3990 None
3991}
3992
3993pub fn selections_creases(
3994 workspace: &mut workspace::Workspace,
3995 cx: &mut ViewContext<Workspace>,
3996) -> Option<Vec<(String, String)>> {
3997 let editor = workspace
3998 .active_item(cx)
3999 .and_then(|item| item.act_as::<Editor>(cx))?;
4000
4001 let mut creases = vec![];
4002 editor.update(cx, |editor, cx| {
4003 let selections = editor.selections.all_adjusted(cx);
4004 let buffer = editor.buffer().read(cx).snapshot(cx);
4005 for selection in selections {
4006 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
4007 ..editor::ToOffset::to_offset(&selection.end, &buffer);
4008 let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
4009 if selected_text.is_empty() {
4010 continue;
4011 }
4012 let start_language = buffer.language_at(range.start);
4013 let end_language = buffer.language_at(range.end);
4014 let language_name = if start_language == end_language {
4015 start_language.map(|language| language.code_fence_block_name())
4016 } else {
4017 None
4018 };
4019 let language_name = language_name.as_deref().unwrap_or("");
4020 let filename = buffer
4021 .file_at(selection.start)
4022 .map(|file| file.full_path(cx));
4023 let text = if language_name == "markdown" {
4024 selected_text
4025 .lines()
4026 .map(|line| format!("> {}", line))
4027 .collect::<Vec<_>>()
4028 .join("\n")
4029 } else {
4030 let start_symbols = buffer
4031 .symbols_containing(selection.start, None)
4032 .map(|(_, symbols)| symbols);
4033 let end_symbols = buffer
4034 .symbols_containing(selection.end, None)
4035 .map(|(_, symbols)| symbols);
4036
4037 let outline_text =
4038 if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
4039 Some(
4040 start_symbols
4041 .into_iter()
4042 .zip(end_symbols)
4043 .take_while(|(a, b)| a == b)
4044 .map(|(a, _)| a.text)
4045 .collect::<Vec<_>>()
4046 .join(" > "),
4047 )
4048 } else {
4049 None
4050 };
4051
4052 let line_comment_prefix = start_language
4053 .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
4054
4055 let fence = codeblock_fence_for_path(
4056 filename.as_deref(),
4057 Some(selection.start.row..=selection.end.row),
4058 );
4059
4060 if let Some((line_comment_prefix, outline_text)) =
4061 line_comment_prefix.zip(outline_text)
4062 {
4063 let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
4064 format!("{fence}{breadcrumb}{selected_text}\n```")
4065 } else {
4066 format!("{fence}{selected_text}\n```")
4067 }
4068 };
4069 let crease_title = if let Some(path) = filename {
4070 let start_line = selection.start.row + 1;
4071 let end_line = selection.end.row + 1;
4072 if start_line == end_line {
4073 format!("{}, Line {}", path.display(), start_line)
4074 } else {
4075 format!("{}, Lines {} to {}", path.display(), start_line, end_line)
4076 }
4077 } else {
4078 "Quoted selection".to_string()
4079 };
4080 creases.push((text, crease_title));
4081 }
4082 });
4083 Some(creases)
4084}
4085
4086fn render_fold_icon_button(
4087 editor: WeakView<Editor>,
4088 icon: IconName,
4089 label: SharedString,
4090) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement> {
4091 Arc::new(move |fold_id, fold_range, _cx| {
4092 let editor = editor.clone();
4093 ButtonLike::new(fold_id)
4094 .style(ButtonStyle::Filled)
4095 .layer(ElevationIndex::ElevatedSurface)
4096 .child(Icon::new(icon))
4097 .child(Label::new(label.clone()).single_line())
4098 .on_click(move |_, cx| {
4099 editor
4100 .update(cx, |editor, cx| {
4101 let buffer_start = fold_range
4102 .start
4103 .to_point(&editor.buffer().read(cx).read(cx));
4104 let buffer_row = MultiBufferRow(buffer_start.row);
4105 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
4106 })
4107 .ok();
4108 })
4109 .into_any_element()
4110 })
4111}
4112
4113#[derive(Debug, Clone, Serialize, Deserialize)]
4114struct CopyMetadata {
4115 creases: Vec<SelectedCreaseMetadata>,
4116}
4117
4118#[derive(Debug, Clone, Serialize, Deserialize)]
4119struct SelectedCreaseMetadata {
4120 range_relative_to_selection: Range<usize>,
4121 crease: CreaseMetadata,
4122}
4123
4124impl EventEmitter<EditorEvent> for ContextEditor {}
4125impl EventEmitter<SearchEvent> for ContextEditor {}
4126
4127impl Render for ContextEditor {
4128 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4129 let provider = LanguageModelRegistry::read_global(cx).active_provider();
4130 let accept_terms = if self.show_accept_terms {
4131 provider
4132 .as_ref()
4133 .and_then(|provider| provider.render_accept_terms(cx))
4134 } else {
4135 None
4136 };
4137
4138 v_flex()
4139 .key_context("ContextEditor")
4140 .capture_action(cx.listener(ContextEditor::cancel))
4141 .capture_action(cx.listener(ContextEditor::save))
4142 .capture_action(cx.listener(ContextEditor::copy))
4143 .capture_action(cx.listener(ContextEditor::cut))
4144 .capture_action(cx.listener(ContextEditor::paste))
4145 .capture_action(cx.listener(ContextEditor::cycle_message_role))
4146 .capture_action(cx.listener(ContextEditor::confirm_command))
4147 .on_action(cx.listener(ContextEditor::edit))
4148 .on_action(cx.listener(ContextEditor::assist))
4149 .on_action(cx.listener(ContextEditor::split))
4150 .size_full()
4151 .children(self.render_notice(cx))
4152 .child(
4153 div()
4154 .flex_grow()
4155 .bg(cx.theme().colors().editor_background)
4156 .child(self.editor.clone()),
4157 )
4158 .when_some(accept_terms, |this, element| {
4159 this.child(
4160 div()
4161 .absolute()
4162 .right_3()
4163 .bottom_12()
4164 .max_w_96()
4165 .py_2()
4166 .px_3()
4167 .elevation_2(cx)
4168 .bg(cx.theme().colors().surface_background)
4169 .occlude()
4170 .child(element),
4171 )
4172 })
4173 .children(self.render_last_error(cx))
4174 .child(
4175 h_flex().w_full().relative().child(
4176 h_flex()
4177 .p_2()
4178 .w_full()
4179 .border_t_1()
4180 .border_color(cx.theme().colors().border_variant)
4181 .bg(cx.theme().colors().editor_background)
4182 .child(h_flex().gap_1().child(self.render_inject_context_menu(cx)))
4183 .child(
4184 h_flex()
4185 .w_full()
4186 .justify_end()
4187 .when(
4188 AssistantSettings::get_global(cx).are_live_diffs_enabled(cx),
4189 |buttons| {
4190 buttons
4191 .items_center()
4192 .gap_1p5()
4193 .child(self.render_edit_button(cx))
4194 .child(
4195 Label::new("or")
4196 .size(LabelSize::Small)
4197 .color(Color::Muted),
4198 )
4199 },
4200 )
4201 .child(self.render_send_button(cx)),
4202 ),
4203 ),
4204 )
4205 }
4206}
4207
4208impl FocusableView for ContextEditor {
4209 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4210 self.editor.focus_handle(cx)
4211 }
4212}
4213
4214impl Item for ContextEditor {
4215 type Event = editor::EditorEvent;
4216
4217 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
4218 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
4219 }
4220
4221 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
4222 match event {
4223 EditorEvent::Edited { .. } => {
4224 f(item::ItemEvent::Edit);
4225 }
4226 EditorEvent::TitleChanged => {
4227 f(item::ItemEvent::UpdateTab);
4228 }
4229 _ => {}
4230 }
4231 }
4232
4233 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
4234 Some(self.title(cx).to_string().into())
4235 }
4236
4237 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
4238 Some(Box::new(handle.clone()))
4239 }
4240
4241 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
4242 self.editor.update(cx, |editor, cx| {
4243 Item::set_nav_history(editor, nav_history, cx)
4244 })
4245 }
4246
4247 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
4248 self.editor
4249 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
4250 }
4251
4252 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
4253 self.editor.update(cx, Item::deactivated)
4254 }
4255
4256 fn act_as_type<'a>(
4257 &'a self,
4258 type_id: TypeId,
4259 self_handle: &'a View<Self>,
4260 _: &'a AppContext,
4261 ) -> Option<AnyView> {
4262 if type_id == TypeId::of::<Self>() {
4263 Some(self_handle.to_any())
4264 } else if type_id == TypeId::of::<Editor>() {
4265 Some(self.editor.to_any())
4266 } else {
4267 None
4268 }
4269 }
4270}
4271
4272impl SearchableItem for ContextEditor {
4273 type Match = <Editor as SearchableItem>::Match;
4274
4275 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
4276 self.editor.update(cx, |editor, cx| {
4277 editor.clear_matches(cx);
4278 });
4279 }
4280
4281 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
4282 self.editor
4283 .update(cx, |editor, cx| editor.update_matches(matches, cx));
4284 }
4285
4286 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
4287 self.editor
4288 .update(cx, |editor, cx| editor.query_suggestion(cx))
4289 }
4290
4291 fn activate_match(
4292 &mut self,
4293 index: usize,
4294 matches: &[Self::Match],
4295 cx: &mut ViewContext<Self>,
4296 ) {
4297 self.editor.update(cx, |editor, cx| {
4298 editor.activate_match(index, matches, cx);
4299 });
4300 }
4301
4302 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
4303 self.editor
4304 .update(cx, |editor, cx| editor.select_matches(matches, cx));
4305 }
4306
4307 fn replace(
4308 &mut self,
4309 identifier: &Self::Match,
4310 query: &project::search::SearchQuery,
4311 cx: &mut ViewContext<Self>,
4312 ) {
4313 self.editor
4314 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
4315 }
4316
4317 fn find_matches(
4318 &mut self,
4319 query: Arc<project::search::SearchQuery>,
4320 cx: &mut ViewContext<Self>,
4321 ) -> Task<Vec<Self::Match>> {
4322 self.editor
4323 .update(cx, |editor, cx| editor.find_matches(query, cx))
4324 }
4325
4326 fn active_match_index(
4327 &mut self,
4328 matches: &[Self::Match],
4329 cx: &mut ViewContext<Self>,
4330 ) -> Option<usize> {
4331 self.editor
4332 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
4333 }
4334}
4335
4336impl FollowableItem for ContextEditor {
4337 fn remote_id(&self) -> Option<workspace::ViewId> {
4338 self.remote_id
4339 }
4340
4341 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
4342 let context = self.context.read(cx);
4343 Some(proto::view::Variant::ContextEditor(
4344 proto::view::ContextEditor {
4345 context_id: context.id().to_proto(),
4346 editor: if let Some(proto::view::Variant::Editor(proto)) =
4347 self.editor.read(cx).to_state_proto(cx)
4348 {
4349 Some(proto)
4350 } else {
4351 None
4352 },
4353 },
4354 ))
4355 }
4356
4357 fn from_state_proto(
4358 workspace: View<Workspace>,
4359 id: workspace::ViewId,
4360 state: &mut Option<proto::view::Variant>,
4361 cx: &mut WindowContext,
4362 ) -> Option<Task<Result<View<Self>>>> {
4363 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
4364 return None;
4365 };
4366 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
4367 unreachable!()
4368 };
4369
4370 let context_id = ContextId::from_proto(state.context_id);
4371 let editor_state = state.editor?;
4372
4373 let (project, panel) = workspace.update(cx, |workspace, cx| {
4374 Some((
4375 workspace.project().clone(),
4376 workspace.panel::<AssistantPanel>(cx)?,
4377 ))
4378 })?;
4379
4380 let context_editor =
4381 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
4382
4383 Some(cx.spawn(|mut cx| async move {
4384 let context_editor = context_editor.await?;
4385 context_editor
4386 .update(&mut cx, |context_editor, cx| {
4387 context_editor.remote_id = Some(id);
4388 context_editor.editor.update(cx, |editor, cx| {
4389 editor.apply_update_proto(
4390 &project,
4391 proto::update_view::Variant::Editor(proto::update_view::Editor {
4392 selections: editor_state.selections,
4393 pending_selection: editor_state.pending_selection,
4394 scroll_top_anchor: editor_state.scroll_top_anchor,
4395 scroll_x: editor_state.scroll_y,
4396 scroll_y: editor_state.scroll_y,
4397 ..Default::default()
4398 }),
4399 cx,
4400 )
4401 })
4402 })?
4403 .await?;
4404 Ok(context_editor)
4405 }))
4406 }
4407
4408 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
4409 Editor::to_follow_event(event)
4410 }
4411
4412 fn add_event_to_update_proto(
4413 &self,
4414 event: &Self::Event,
4415 update: &mut Option<proto::update_view::Variant>,
4416 cx: &WindowContext,
4417 ) -> bool {
4418 self.editor
4419 .read(cx)
4420 .add_event_to_update_proto(event, update, cx)
4421 }
4422
4423 fn apply_update_proto(
4424 &mut self,
4425 project: &Model<Project>,
4426 message: proto::update_view::Variant,
4427 cx: &mut ViewContext<Self>,
4428 ) -> Task<Result<()>> {
4429 self.editor.update(cx, |editor, cx| {
4430 editor.apply_update_proto(project, message, cx)
4431 })
4432 }
4433
4434 fn is_project_item(&self, _cx: &WindowContext) -> bool {
4435 true
4436 }
4437
4438 fn set_leader_peer_id(
4439 &mut self,
4440 leader_peer_id: Option<proto::PeerId>,
4441 cx: &mut ViewContext<Self>,
4442 ) {
4443 self.editor.update(cx, |editor, cx| {
4444 editor.set_leader_peer_id(leader_peer_id, cx)
4445 })
4446 }
4447
4448 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
4449 if existing.context.read(cx).id() == self.context.read(cx).id() {
4450 Some(item::Dedup::KeepExisting)
4451 } else {
4452 None
4453 }
4454 }
4455}
4456
4457pub struct ContextEditorToolbarItem {
4458 fs: Arc<dyn Fs>,
4459 active_context_editor: Option<WeakView<ContextEditor>>,
4460 model_summary_editor: View<Editor>,
4461 model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
4462}
4463
4464impl ContextEditorToolbarItem {
4465 pub fn new(
4466 workspace: &Workspace,
4467 model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
4468 model_summary_editor: View<Editor>,
4469 ) -> Self {
4470 Self {
4471 fs: workspace.app_state().fs.clone(),
4472 active_context_editor: None,
4473 model_summary_editor,
4474 model_selector_menu_handle,
4475 }
4476 }
4477
4478 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
4479 let context = &self
4480 .active_context_editor
4481 .as_ref()?
4482 .upgrade()?
4483 .read(cx)
4484 .context;
4485 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
4486 TokenState::NoTokensLeft {
4487 max_token_count,
4488 token_count,
4489 } => (Color::Error, token_count, max_token_count),
4490 TokenState::HasMoreTokens {
4491 max_token_count,
4492 token_count,
4493 over_warn_threshold,
4494 } => {
4495 let color = if over_warn_threshold {
4496 Color::Warning
4497 } else {
4498 Color::Muted
4499 };
4500 (color, token_count, max_token_count)
4501 }
4502 };
4503 Some(
4504 h_flex()
4505 .gap_0p5()
4506 .child(
4507 Label::new(humanize_token_count(token_count))
4508 .size(LabelSize::Small)
4509 .color(token_count_color),
4510 )
4511 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
4512 .child(
4513 Label::new(humanize_token_count(max_token_count))
4514 .size(LabelSize::Small)
4515 .color(Color::Muted),
4516 ),
4517 )
4518 }
4519}
4520
4521impl Render for ContextEditorToolbarItem {
4522 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4523 let left_side = h_flex()
4524 .group("chat-title-group")
4525 .gap_1()
4526 .items_center()
4527 .flex_grow()
4528 .child(
4529 div()
4530 .w_full()
4531 .when(self.active_context_editor.is_some(), |left_side| {
4532 left_side.child(self.model_summary_editor.clone())
4533 }),
4534 )
4535 .child(
4536 div().visible_on_hover("chat-title-group").child(
4537 IconButton::new("regenerate-context", IconName::RefreshTitle)
4538 .shape(ui::IconButtonShape::Square)
4539 .tooltip(|cx| Tooltip::text("Regenerate Title", cx))
4540 .on_click(cx.listener(move |_, _, cx| {
4541 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
4542 })),
4543 ),
4544 );
4545 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
4546 let active_model = LanguageModelRegistry::read_global(cx).active_model();
4547 let right_side = h_flex()
4548 .gap_2()
4549 // TODO display this in a nicer way, once we have a design for it.
4550 // .children({
4551 // let project = self
4552 // .workspace
4553 // .upgrade()
4554 // .map(|workspace| workspace.read(cx).project().downgrade());
4555 //
4556 // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
4557 // project.and_then(|project| db.remaining_summaries(&project, cx))
4558 // });
4559 // scan_items_remaining
4560 // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
4561 // })
4562 .child(
4563 LanguageModelSelector::new(
4564 {
4565 let fs = self.fs.clone();
4566 move |model, cx| {
4567 update_settings_file::<AssistantSettings>(
4568 fs.clone(),
4569 cx,
4570 move |settings, _| settings.set_model(model.clone()),
4571 );
4572 }
4573 },
4574 ButtonLike::new("active-model")
4575 .style(ButtonStyle::Subtle)
4576 .child(
4577 h_flex()
4578 .w_full()
4579 .gap_0p5()
4580 .child(
4581 div()
4582 .overflow_x_hidden()
4583 .flex_grow()
4584 .whitespace_nowrap()
4585 .child(match (active_provider, active_model) {
4586 (Some(provider), Some(model)) => h_flex()
4587 .gap_1()
4588 .child(
4589 Icon::new(
4590 model
4591 .icon()
4592 .unwrap_or_else(|| provider.icon()),
4593 )
4594 .color(Color::Muted)
4595 .size(IconSize::XSmall),
4596 )
4597 .child(
4598 Label::new(model.name().0)
4599 .size(LabelSize::Small)
4600 .color(Color::Muted),
4601 )
4602 .into_any_element(),
4603 _ => Label::new("No model selected")
4604 .size(LabelSize::Small)
4605 .color(Color::Muted)
4606 .into_any_element(),
4607 }),
4608 )
4609 .child(
4610 Icon::new(IconName::ChevronDown)
4611 .color(Color::Muted)
4612 .size(IconSize::XSmall),
4613 ),
4614 )
4615 .tooltip(move |cx| {
4616 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
4617 }),
4618 )
4619 .with_handle(self.model_selector_menu_handle.clone()),
4620 )
4621 .children(self.render_remaining_tokens(cx));
4622
4623 h_flex()
4624 .px_0p5()
4625 .size_full()
4626 .gap_2()
4627 .justify_between()
4628 .child(left_side)
4629 .child(right_side)
4630 }
4631}
4632
4633impl ToolbarItemView for ContextEditorToolbarItem {
4634 fn set_active_pane_item(
4635 &mut self,
4636 active_pane_item: Option<&dyn ItemHandle>,
4637 cx: &mut ViewContext<Self>,
4638 ) -> ToolbarItemLocation {
4639 self.active_context_editor = active_pane_item
4640 .and_then(|item| item.act_as::<ContextEditor>(cx))
4641 .map(|editor| editor.downgrade());
4642 cx.notify();
4643 if self.active_context_editor.is_none() {
4644 ToolbarItemLocation::Hidden
4645 } else {
4646 ToolbarItemLocation::PrimaryRight
4647 }
4648 }
4649
4650 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
4651 cx.notify();
4652 }
4653}
4654
4655impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
4656
4657enum ContextEditorToolbarItemEvent {
4658 RegenerateSummary,
4659}
4660impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
4661
4662pub struct ContextHistory {
4663 picker: View<Picker<SavedContextPickerDelegate>>,
4664 _subscriptions: Vec<Subscription>,
4665 assistant_panel: WeakView<AssistantPanel>,
4666}
4667
4668impl ContextHistory {
4669 fn new(
4670 project: Model<Project>,
4671 context_store: Model<ContextStore>,
4672 assistant_panel: WeakView<AssistantPanel>,
4673 cx: &mut ViewContext<Self>,
4674 ) -> Self {
4675 let picker = cx.new_view(|cx| {
4676 Picker::uniform_list(
4677 SavedContextPickerDelegate::new(project, context_store.clone()),
4678 cx,
4679 )
4680 .modal(false)
4681 .max_height(None)
4682 });
4683
4684 let _subscriptions = vec![
4685 cx.observe(&context_store, |this, _, cx| {
4686 this.picker.update(cx, |picker, cx| picker.refresh(cx));
4687 }),
4688 cx.subscribe(&picker, Self::handle_picker_event),
4689 ];
4690
4691 Self {
4692 picker,
4693 _subscriptions,
4694 assistant_panel,
4695 }
4696 }
4697
4698 fn handle_picker_event(
4699 &mut self,
4700 _: View<Picker<SavedContextPickerDelegate>>,
4701 event: &SavedContextPickerEvent,
4702 cx: &mut ViewContext<Self>,
4703 ) {
4704 let SavedContextPickerEvent::Confirmed(context) = event;
4705 self.assistant_panel
4706 .update(cx, |assistant_panel, cx| match context {
4707 ContextMetadata::Remote(metadata) => {
4708 assistant_panel
4709 .open_remote_context(metadata.id.clone(), cx)
4710 .detach_and_log_err(cx);
4711 }
4712 ContextMetadata::Saved(metadata) => {
4713 assistant_panel
4714 .open_saved_context(metadata.path.clone(), cx)
4715 .detach_and_log_err(cx);
4716 }
4717 })
4718 .ok();
4719 }
4720}
4721
4722#[derive(Debug, PartialEq, Eq, Clone, Copy)]
4723pub enum WorkflowAssistStatus {
4724 Pending,
4725 Confirmed,
4726 Done,
4727 Idle,
4728}
4729
4730impl Render for ContextHistory {
4731 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
4732 div().size_full().child(self.picker.clone())
4733 }
4734}
4735
4736impl FocusableView for ContextHistory {
4737 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4738 self.picker.focus_handle(cx)
4739 }
4740}
4741
4742impl EventEmitter<()> for ContextHistory {}
4743
4744impl Item for ContextHistory {
4745 type Event = ();
4746
4747 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
4748 Some("History".into())
4749 }
4750}
4751
4752pub struct ConfigurationView {
4753 focus_handle: FocusHandle,
4754 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
4755 _registry_subscription: Subscription,
4756}
4757
4758impl ConfigurationView {
4759 fn new(cx: &mut ViewContext<Self>) -> Self {
4760 let focus_handle = cx.focus_handle();
4761
4762 let registry_subscription = cx.subscribe(
4763 &LanguageModelRegistry::global(cx),
4764 |this, _, event: &language_model::Event, cx| match event {
4765 language_model::Event::AddedProvider(provider_id) => {
4766 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
4767 if let Some(provider) = provider {
4768 this.add_configuration_view(&provider, cx);
4769 }
4770 }
4771 language_model::Event::RemovedProvider(provider_id) => {
4772 this.remove_configuration_view(provider_id);
4773 }
4774 _ => {}
4775 },
4776 );
4777
4778 let mut this = Self {
4779 focus_handle,
4780 configuration_views: HashMap::default(),
4781 _registry_subscription: registry_subscription,
4782 };
4783 this.build_configuration_views(cx);
4784 this
4785 }
4786
4787 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
4788 let providers = LanguageModelRegistry::read_global(cx).providers();
4789 for provider in providers {
4790 self.add_configuration_view(&provider, cx);
4791 }
4792 }
4793
4794 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
4795 self.configuration_views.remove(provider_id);
4796 }
4797
4798 fn add_configuration_view(
4799 &mut self,
4800 provider: &Arc<dyn LanguageModelProvider>,
4801 cx: &mut ViewContext<Self>,
4802 ) {
4803 let configuration_view = provider.configuration_view(cx);
4804 self.configuration_views
4805 .insert(provider.id(), configuration_view);
4806 }
4807
4808 fn render_provider_view(
4809 &mut self,
4810 provider: &Arc<dyn LanguageModelProvider>,
4811 cx: &mut ViewContext<Self>,
4812 ) -> Div {
4813 let provider_id = provider.id().0.clone();
4814 let provider_name = provider.name().0.clone();
4815 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
4816
4817 let open_new_context = cx.listener({
4818 let provider = provider.clone();
4819 move |_, _, cx| {
4820 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
4821 provider.clone(),
4822 ))
4823 }
4824 });
4825
4826 v_flex()
4827 .gap_2()
4828 .child(
4829 h_flex()
4830 .justify_between()
4831 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
4832 .when(provider.is_authenticated(cx), move |this| {
4833 this.child(
4834 h_flex().justify_end().child(
4835 Button::new(
4836 SharedString::from(format!("new-context-{provider_id}")),
4837 "Open New Chat",
4838 )
4839 .icon_position(IconPosition::Start)
4840 .icon(IconName::Plus)
4841 .style(ButtonStyle::Filled)
4842 .layer(ElevationIndex::ModalSurface)
4843 .on_click(open_new_context),
4844 ),
4845 )
4846 }),
4847 )
4848 .child(
4849 div()
4850 .p(DynamicSpacing::Base08.rems(cx))
4851 .bg(cx.theme().colors().surface_background)
4852 .border_1()
4853 .border_color(cx.theme().colors().border_variant)
4854 .rounded_md()
4855 .when(configuration_view.is_none(), |this| {
4856 this.child(div().child(Label::new(format!(
4857 "No configuration view for {}",
4858 provider_name
4859 ))))
4860 })
4861 .when_some(configuration_view, |this, configuration_view| {
4862 this.child(configuration_view)
4863 }),
4864 )
4865 }
4866}
4867
4868impl Render for ConfigurationView {
4869 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4870 let providers = LanguageModelRegistry::read_global(cx).providers();
4871 let provider_views = providers
4872 .into_iter()
4873 .map(|provider| self.render_provider_view(&provider, cx))
4874 .collect::<Vec<_>>();
4875
4876 let mut element = v_flex()
4877 .id("assistant-configuration-view")
4878 .track_focus(&self.focus_handle(cx))
4879 .bg(cx.theme().colors().editor_background)
4880 .size_full()
4881 .overflow_y_scroll()
4882 .child(
4883 v_flex()
4884 .p(DynamicSpacing::Base16.rems(cx))
4885 .border_b_1()
4886 .border_color(cx.theme().colors().border)
4887 .gap_1()
4888 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
4889 .child(
4890 Label::new(
4891 "At least one LLM provider must be configured to use the Assistant.",
4892 )
4893 .color(Color::Muted),
4894 ),
4895 )
4896 .child(
4897 v_flex()
4898 .p(DynamicSpacing::Base16.rems(cx))
4899 .mt_1()
4900 .gap_6()
4901 .flex_1()
4902 .children(provider_views),
4903 )
4904 .into_any();
4905
4906 // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
4907 // because we couldn't the element to take up the size of the parent.
4908 canvas(
4909 move |bounds, cx| {
4910 element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
4911 element
4912 },
4913 |_, mut element, cx| {
4914 element.paint(cx);
4915 },
4916 )
4917 .flex_1()
4918 .w_full()
4919 }
4920}
4921
4922pub enum ConfigurationViewEvent {
4923 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
4924}
4925
4926impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
4927
4928impl FocusableView for ConfigurationView {
4929 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
4930 self.focus_handle.clone()
4931 }
4932}
4933
4934impl Item for ConfigurationView {
4935 type Event = ConfigurationViewEvent;
4936
4937 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
4938 Some("Configuration".into())
4939 }
4940}
4941
4942type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
4943
4944fn render_slash_command_output_toggle(
4945 row: MultiBufferRow,
4946 is_folded: bool,
4947 fold: ToggleFold,
4948 _cx: &mut WindowContext,
4949) -> AnyElement {
4950 Disclosure::new(
4951 ("slash-command-output-fold-indicator", row.0 as u64),
4952 !is_folded,
4953 )
4954 .selected(is_folded)
4955 .on_click(move |_e, cx| fold(!is_folded, cx))
4956 .into_any_element()
4957}
4958
4959fn fold_toggle(
4960 name: &'static str,
4961) -> impl Fn(
4962 MultiBufferRow,
4963 bool,
4964 Arc<dyn Fn(bool, &mut WindowContext<'_>) + Send + Sync>,
4965 &mut WindowContext<'_>,
4966) -> AnyElement {
4967 move |row, is_folded, fold, _cx| {
4968 Disclosure::new((name, row.0 as u64), !is_folded)
4969 .selected(is_folded)
4970 .on_click(move |_e, cx| fold(!is_folded, cx))
4971 .into_any_element()
4972 }
4973}
4974
4975fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) -> FoldPlaceholder {
4976 FoldPlaceholder {
4977 render: Arc::new({
4978 move |fold_id, fold_range, _cx| {
4979 let editor = editor.clone();
4980 ButtonLike::new(fold_id)
4981 .style(ButtonStyle::Filled)
4982 .layer(ElevationIndex::ElevatedSurface)
4983 .child(Icon::new(IconName::TextSnippet))
4984 .child(Label::new(title.clone()).single_line())
4985 .on_click(move |_, cx| {
4986 editor
4987 .update(cx, |editor, cx| {
4988 let buffer_start = fold_range
4989 .start
4990 .to_point(&editor.buffer().read(cx).read(cx));
4991 let buffer_row = MultiBufferRow(buffer_start.row);
4992 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
4993 })
4994 .ok();
4995 })
4996 .into_any_element()
4997 }
4998 }),
4999 merge_adjacent: false,
5000 ..Default::default()
5001 }
5002}
5003
5004fn render_quote_selection_output_toggle(
5005 row: MultiBufferRow,
5006 is_folded: bool,
5007 fold: ToggleFold,
5008 _cx: &mut WindowContext,
5009) -> AnyElement {
5010 Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
5011 .selected(is_folded)
5012 .on_click(move |_e, cx| fold(!is_folded, cx))
5013 .into_any_element()
5014}
5015
5016fn render_pending_slash_command_gutter_decoration(
5017 row: MultiBufferRow,
5018 status: &PendingSlashCommandStatus,
5019 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
5020) -> AnyElement {
5021 let mut icon = IconButton::new(
5022 ("slash-command-gutter-decoration", row.0),
5023 ui::IconName::TriangleRight,
5024 )
5025 .on_click(move |_e, cx| confirm_command(cx))
5026 .icon_size(ui::IconSize::Small)
5027 .size(ui::ButtonSize::None);
5028
5029 match status {
5030 PendingSlashCommandStatus::Idle => {
5031 icon = icon.icon_color(Color::Muted);
5032 }
5033 PendingSlashCommandStatus::Running { .. } => {
5034 icon = icon.selected(true);
5035 }
5036 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
5037 }
5038
5039 icon.into_any_element()
5040}
5041
5042fn render_docs_slash_command_trailer(
5043 row: MultiBufferRow,
5044 command: ParsedSlashCommand,
5045 cx: &mut WindowContext,
5046) -> AnyElement {
5047 if command.arguments.is_empty() {
5048 return Empty.into_any();
5049 }
5050 let args = DocsSlashCommandArgs::parse(&command.arguments);
5051
5052 let Some(store) = args
5053 .provider()
5054 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
5055 else {
5056 return Empty.into_any();
5057 };
5058
5059 let Some(package) = args.package() else {
5060 return Empty.into_any();
5061 };
5062
5063 let mut children = Vec::new();
5064
5065 if store.is_indexing(&package) {
5066 children.push(
5067 div()
5068 .id(("crates-being-indexed", row.0))
5069 .child(Icon::new(IconName::ArrowCircle).with_animation(
5070 "arrow-circle",
5071 Animation::new(Duration::from_secs(4)).repeat(),
5072 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
5073 ))
5074 .tooltip({
5075 let package = package.clone();
5076 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
5077 })
5078 .into_any_element(),
5079 );
5080 }
5081
5082 if let Some(latest_error) = store.latest_error_for_package(&package) {
5083 children.push(
5084 div()
5085 .id(("latest-error", row.0))
5086 .child(
5087 Icon::new(IconName::Warning)
5088 .size(IconSize::Small)
5089 .color(Color::Warning),
5090 )
5091 .tooltip(move |cx| Tooltip::text(format!("Failed to index: {latest_error}"), cx))
5092 .into_any_element(),
5093 )
5094 }
5095
5096 let is_indexing = store.is_indexing(&package);
5097 let latest_error = store.latest_error_for_package(&package);
5098
5099 if !is_indexing && latest_error.is_none() {
5100 return Empty.into_any();
5101 }
5102
5103 h_flex().gap_2().children(children).into_any_element()
5104}
5105
5106fn make_lsp_adapter_delegate(
5107 project: &Model<Project>,
5108 cx: &mut AppContext,
5109) -> Result<Option<Arc<dyn LspAdapterDelegate>>> {
5110 project.update(cx, |project, cx| {
5111 // TODO: Find the right worktree.
5112 let Some(worktree) = project.worktrees(cx).next() else {
5113 return Ok(None::<Arc<dyn LspAdapterDelegate>>);
5114 };
5115 let http_client = project.client().http_client().clone();
5116 project.lsp_store().update(cx, |lsp_store, cx| {
5117 Ok(Some(LocalLspAdapterDelegate::new(
5118 lsp_store,
5119 &worktree,
5120 http_client,
5121 project.fs().clone(),
5122 cx,
5123 ) as Arc<dyn LspAdapterDelegate>))
5124 })
5125 })
5126}
5127
5128enum PendingSlashCommand {}
5129
5130fn invoked_slash_command_fold_placeholder(
5131 command_id: InvokedSlashCommandId,
5132 context: WeakModel<Context>,
5133) -> FoldPlaceholder {
5134 FoldPlaceholder {
5135 constrain_width: false,
5136 merge_adjacent: false,
5137 render: Arc::new(move |fold_id, _, cx| {
5138 let Some(context) = context.upgrade() else {
5139 return Empty.into_any();
5140 };
5141
5142 let Some(command) = context.read(cx).invoked_slash_command(&command_id) else {
5143 return Empty.into_any();
5144 };
5145
5146 h_flex()
5147 .id(fold_id)
5148 .px_1()
5149 .ml_6()
5150 .gap_2()
5151 .bg(cx.theme().colors().surface_background)
5152 .rounded_md()
5153 .child(Label::new(format!("/{}", command.name.clone())))
5154 .map(|parent| match &command.status {
5155 InvokedSlashCommandStatus::Running(_) => {
5156 parent.child(Icon::new(IconName::ArrowCircle).with_animation(
5157 "arrow-circle",
5158 Animation::new(Duration::from_secs(4)).repeat(),
5159 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
5160 ))
5161 }
5162 InvokedSlashCommandStatus::Error(message) => parent.child(
5163 Label::new(format!("error: {message}"))
5164 .single_line()
5165 .color(Color::Error),
5166 ),
5167 InvokedSlashCommandStatus::Finished => parent,
5168 })
5169 .into_any_element()
5170 }),
5171 type_tag: Some(TypeId::of::<PendingSlashCommand>()),
5172 }
5173}
5174
5175enum TokenState {
5176 NoTokensLeft {
5177 max_token_count: usize,
5178 token_count: usize,
5179 },
5180 HasMoreTokens {
5181 max_token_count: usize,
5182 token_count: usize,
5183 over_warn_threshold: bool,
5184 },
5185}
5186
5187fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
5188 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
5189
5190 let model = LanguageModelRegistry::read_global(cx).active_model()?;
5191 let token_count = context.read(cx).token_count()?;
5192 let max_token_count = model.max_token_count();
5193
5194 let remaining_tokens = max_token_count as isize - token_count as isize;
5195 let token_state = if remaining_tokens <= 0 {
5196 TokenState::NoTokensLeft {
5197 max_token_count,
5198 token_count,
5199 }
5200 } else {
5201 let over_warn_threshold =
5202 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
5203 TokenState::HasMoreTokens {
5204 max_token_count,
5205 token_count,
5206 over_warn_threshold,
5207 }
5208 };
5209 Some(token_state)
5210}
5211
5212fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
5213 let image_size = data
5214 .size(0)
5215 .map(|dimension| Pixels::from(u32::from(dimension)));
5216 let image_ratio = image_size.width / image_size.height;
5217 let bounds_ratio = max_size.width / max_size.height;
5218
5219 if image_size.width > max_size.width || image_size.height > max_size.height {
5220 if bounds_ratio > image_ratio {
5221 size(
5222 image_size.width * (max_size.height / image_size.height),
5223 max_size.height,
5224 )
5225 } else {
5226 size(
5227 max_size.width,
5228 image_size.height * (max_size.width / image_size.width),
5229 )
5230 }
5231 } else {
5232 size(image_size.width, image_size.height)
5233 }
5234}
5235
5236enum ConfigurationError {
5237 NoProvider,
5238 ProviderNotAuthenticated,
5239}
5240
5241fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
5242 let provider = LanguageModelRegistry::read_global(cx).active_provider();
5243 let is_authenticated = provider
5244 .as_ref()
5245 .map_or(false, |provider| provider.is_authenticated(cx));
5246
5247 if provider.is_some() && is_authenticated {
5248 return None;
5249 }
5250
5251 if provider.is_none() {
5252 return Some(ConfigurationError::NoProvider);
5253 }
5254
5255 if !is_authenticated {
5256 return Some(ConfigurationError::ProviderNotAuthenticated);
5257 }
5258
5259 None
5260}
5261
5262#[cfg(test)]
5263mod tests {
5264 use super::*;
5265 use gpui::{AppContext, Context};
5266 use language::Buffer;
5267 use unindent::Unindent;
5268
5269 #[gpui::test]
5270 fn test_find_code_blocks(cx: &mut AppContext) {
5271 let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
5272
5273 let buffer = cx.new_model(|cx| {
5274 let text = r#"
5275 line 0
5276 line 1
5277 ```rust
5278 fn main() {}
5279 ```
5280 line 5
5281 line 6
5282 line 7
5283 ```go
5284 func main() {}
5285 ```
5286 line 11
5287 ```
5288 this is plain text code block
5289 ```
5290
5291 ```go
5292 func another() {}
5293 ```
5294 line 19
5295 "#
5296 .unindent();
5297 let mut buffer = Buffer::local(text, cx);
5298 buffer.set_language(Some(markdown.clone()), cx);
5299 buffer
5300 });
5301 let snapshot = buffer.read(cx).snapshot();
5302
5303 let code_blocks = vec![
5304 Point::new(3, 0)..Point::new(4, 0),
5305 Point::new(9, 0)..Point::new(10, 0),
5306 Point::new(13, 0)..Point::new(14, 0),
5307 Point::new(17, 0)..Point::new(18, 0),
5308 ]
5309 .into_iter()
5310 .map(|range| snapshot.point_to_offset(range.start)..snapshot.point_to_offset(range.end))
5311 .collect::<Vec<_>>();
5312
5313 let expected_results = vec![
5314 (0, None),
5315 (1, None),
5316 (2, Some(code_blocks[0].clone())),
5317 (3, Some(code_blocks[0].clone())),
5318 (4, Some(code_blocks[0].clone())),
5319 (5, None),
5320 (6, None),
5321 (7, None),
5322 (8, Some(code_blocks[1].clone())),
5323 (9, Some(code_blocks[1].clone())),
5324 (10, Some(code_blocks[1].clone())),
5325 (11, None),
5326 (12, Some(code_blocks[2].clone())),
5327 (13, Some(code_blocks[2].clone())),
5328 (14, Some(code_blocks[2].clone())),
5329 (15, None),
5330 (16, Some(code_blocks[3].clone())),
5331 (17, Some(code_blocks[3].clone())),
5332 (18, Some(code_blocks[3].clone())),
5333 (19, None),
5334 ];
5335
5336 for (row, expected) in expected_results {
5337 let offset = snapshot.point_to_offset(Point::new(row, 0));
5338 let range = find_surrounding_code_block(&snapshot, offset);
5339 assert_eq!(range, expected, "unexpected result on row {:?}", row);
5340 }
5341 }
5342}