diff --git a/Cargo.lock b/Cargo.lock index 6639192f6c..896902cfc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4009,7 +4009,6 @@ dependencies = [ "db", "emojis", "env_logger 0.11.6", - "feature_flags", "file_icons", "fs", "futures 0.3.31", @@ -5307,12 +5306,15 @@ dependencies = [ "collections", "db", "editor", + "feature_flags", "futures 0.3.31", "git", "gpui", "language", "menu", + "multi_buffer", "picker", + "postage", "project", "rpc", "schemars", diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index a8d298e1c4..c3f21e88c5 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -201,9 +201,8 @@ impl UserStore { cx.update(|cx| { if let Some(info) = info { - let disable_staff = std::env::var("ZED_DISABLE_STAFF") - .map_or(false, |v| !v.is_empty() && v != "0"); - let staff = info.staff && !disable_staff; + let staff = + info.staff && !*feature_flags::ZED_DISABLE_STAFF; cx.update_flags(staff, info.flags); client.telemetry.set_authenticated_user_info( Some(info.metrics_id.clone()), diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 9a35c17ad1..4acc2768cf 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -39,7 +39,6 @@ collections.workspace = true convert_case.workspace = true db.workspace = true emojis.workspace = true -feature_flags.workspace = true file_icons.workspace = true futures.workspace = true fuzzy.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c288664337..034c7e9c1d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -339,7 +339,6 @@ pub fn init(cx: &mut App) { .detach(); } }); - git::project_diff::init(cx); } pub struct SearchWithinRange; @@ -4653,7 +4652,7 @@ impl Editor { let mut read_ranges = Vec::new(); for highlight in highlights { for (excerpt_id, excerpt_range) in - buffer.excerpts_for_buffer(&cursor_buffer, cx) + buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) { let start = highlight .range @@ -11747,10 +11746,7 @@ impl Editor { if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) { return; } - let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { - return; - }; - let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx); + let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); self.display_map .update(cx, |display_map, cx| display_map.fold_buffer(buffer_id, cx)); cx.emit(EditorEvent::BufferFoldToggled { @@ -11764,10 +11760,7 @@ impl Editor { if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) { return; } - let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { - return; - }; - let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(&buffer, cx); + let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); self.display_map.update(cx, |display_map, cx| { display_map.unfold_buffer(buffer_id, cx); }); diff --git a/crates/editor/src/git.rs b/crates/editor/src/git.rs index 97ca80ea29..080babe4c6 100644 --- a/crates/editor/src/git.rs +++ b/crates/editor/src/git.rs @@ -1,2 +1 @@ pub mod blame; -pub mod project_diff; diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 55fb1cb945..508bbeea6d 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -743,12 +743,12 @@ fn determine_query_ranges( excerpt_visible_range: Range, cx: &mut Context<'_, MultiBuffer>, ) -> Option { + let buffer = excerpt_buffer.read(cx); let full_excerpt_range = multi_buffer - .excerpts_for_buffer(excerpt_buffer, cx) + .excerpts_for_buffer(buffer.remote_id(), cx) .into_iter() .find(|(id, _)| id == &excerpt_id) .map(|(_, range)| range.context)?; - let buffer = excerpt_buffer.read(cx); let snapshot = buffer.snapshot(); let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index eb014be881..c92a8ada6c 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -1,6 +1,9 @@ use futures::channel::oneshot; use futures::{select_biased, FutureExt}; use gpui::{App, Context, Global, Subscription, Task, Window}; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::LazyLock; use std::time::Duration; use std::{future::Future, pin::Pin, task::Poll}; @@ -10,12 +13,21 @@ struct FeatureFlags { staff: bool, } +pub static ZED_DISABLE_STAFF: LazyLock = LazyLock::new(|| { + std::env::var("ZED_DISABLE_STAFF").map_or(false, |value| !value.is_empty() && value != "0") +}); + impl FeatureFlags { fn has_flag(&self) -> bool { if self.staff && T::enabled_for_staff() { return true; } + #[cfg(debug_assertions)] + if T::enabled_in_development() { + return true; + } + self.flags.iter().any(|f| f.as_str() == T::NAME) } } @@ -35,6 +47,10 @@ pub trait FeatureFlag { fn enabled_for_staff() -> bool { true } + + fn enabled_in_development() -> bool { + Self::enabled_for_staff() && !*ZED_DISABLE_STAFF + } } pub struct Assistant2FeatureFlag; @@ -97,6 +113,12 @@ pub trait FeatureFlagViewExt { fn observe_flag(&mut self, window: &Window, callback: F) -> Subscription where F: Fn(bool, &mut V, &mut Window, &mut Context) + Send + Sync + 'static; + + fn when_flag_enabled( + &mut self, + window: &mut Window, + callback: impl Fn(&mut V, &mut Window, &mut Context) + Send + Sync + 'static, + ); } impl FeatureFlagViewExt for Context<'_, V> @@ -112,6 +134,35 @@ where callback(feature_flags.has_flag::(), v, window, cx); }) } + + fn when_flag_enabled( + &mut self, + window: &mut Window, + callback: impl Fn(&mut V, &mut Window, &mut Context) + Send + Sync + 'static, + ) { + if self + .try_global::() + .is_some_and(|f| f.has_flag::()) + || cfg!(debug_assertions) && T::enabled_in_development() + { + self.defer_in(window, move |view, window, cx| { + callback(view, window, cx); + }); + return; + } + let subscription = Rc::new(RefCell::new(None)); + let inner = self.observe_global_in::(window, { + let subscription = subscription.clone(); + move |v, window, cx| { + let feature_flags = cx.global::(); + if feature_flags.has_flag::() { + callback(v, window, cx); + subscription.take(); + } + } + }); + subscription.borrow_mut().replace(inner); + } } pub trait FeatureFlagAppExt { diff --git a/crates/git/src/status.rs b/crates/git/src/status.rs index ef862e7e96..6d2a976933 100644 --- a/crates/git/src/status.rs +++ b/crates/git/src/status.rs @@ -133,6 +133,10 @@ impl FileStatus { } } + pub fn has_changes(&self) -> bool { + self.is_modified() || self.is_created() || self.is_deleted() || self.is_untracked() + } + pub fn is_modified(self) -> bool { match self { FileStatus::Tracked(tracked) => match (tracked.index_status, tracked.worktree_status) { diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 18cad73127..0554107604 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -17,11 +17,14 @@ anyhow.workspace = true collections.workspace = true db.workspace = true editor.workspace = true +feature_flags.workspace = true futures.workspace = true git.workspace = true gpui.workspace = true language.workspace = true +multi_buffer.workspace = true menu.workspace = true +postage.workspace = true project.workspace = true rpc.workspace = true schemars.workspace = true diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 3ff84e0ddc..ac622aaa42 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1,5 +1,6 @@ use crate::git_panel_settings::StatusStyle; use crate::repository_selector::RepositorySelectorPopoverMenu; +use crate::ProjectDiff; use crate::{ git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, }; @@ -207,31 +208,6 @@ fn commit_message_editor( } impl GitPanel { - pub fn load( - workspace: WeakEntity, - cx: AsyncWindowContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let commit_message_buffer = workspace.update(&mut cx, |workspace, cx| { - let project = workspace.project(); - let active_repository = project.read(cx).active_repository(cx); - active_repository - .map(|active_repository| commit_message_buffer(project, &active_repository, cx)) - })?; - let commit_message_buffer = match commit_message_buffer { - Some(commit_message_buffer) => Some( - commit_message_buffer - .await - .context("opening commit buffer")?, - ), - None => None, - }; - workspace.update_in(&mut cx, |workspace, window, cx| { - Self::new(workspace, window, commit_message_buffer, cx) - }) - }) - } - pub fn new( workspace: &mut Workspace, window: &mut Window, @@ -240,7 +216,7 @@ impl GitPanel { ) -> Entity { let fs = workspace.app_state().fs.clone(); let project = workspace.project().clone(); - let git_state = project.read(cx).git_state().cloned(); + let git_state = project.read(cx).git_state().clone(); let active_repository = project.read(cx).active_repository(cx); let (err_sender, mut err_receiver) = mpsc::channel(1); let workspace = cx.entity().downgrade(); @@ -261,19 +237,17 @@ impl GitPanel { let scroll_handle = UniformListScrollHandle::new(); - if let Some(git_state) = git_state { - cx.subscribe_in( - &git_state, - window, - move |this, git_state, event, window, cx| match event { - project::git::Event::RepositoriesUpdated => { - this.active_repository = git_state.read(cx).active_repository(); - this.schedule_update(window, cx); - } - }, - ) - .detach(); - } + cx.subscribe_in( + &git_state, + window, + move |this, git_state, event, window, cx| match event { + project::git::Event::RepositoriesUpdated => { + this.active_repository = git_state.read(cx).active_repository(); + this.schedule_update(window, cx); + } + }, + ) + .detach(); let repository_selector = cx.new(|cx| RepositorySelector::new(project.clone(), window, cx)); @@ -344,8 +318,24 @@ impl GitPanel { git_panel } + pub fn set_focused_path(&mut self, path: ProjectPath, _: &mut Window, cx: &mut Context) { + let Some(git_repo) = self.active_repository.as_ref() else { + return; + }; + let Some(repo_path) = git_repo.project_path_to_repo_path(&path) else { + return; + }; + let Ok(ix) = self + .visible_entries + .binary_search_by_key(&&repo_path, |entry| &entry.repo_path) + else { + return; + }; + self.selected_entry = Some(ix); + cx.notify(); + } + fn serialize(&mut self, cx: &mut Context) { - // TODO: we can store stage status here let width = self.width; self.pending_serialization = cx.background_executor().spawn( async move { @@ -623,7 +613,7 @@ impl GitPanel { let Some(active_repository) = self.active_repository.as_ref() else { return; }; - let Some(path) = active_repository.unrelativize(&entry.repo_path) else { + let Some(path) = active_repository.repo_path_to_project_path(&entry.repo_path) else { return; }; let path_exists = self.project.update(cx, |project, cx| { @@ -1021,8 +1011,8 @@ impl GitPanel { .project .read(cx) .git_state() - .map(|state| state.read(cx).all_repositories()) - .unwrap_or_default(); + .read(cx) + .all_repositories(); let entry_count = self .active_repository .as_ref() @@ -1408,17 +1398,26 @@ impl GitPanel { .toggle_state(selected) .disabled(!has_write_access) .on_click({ - let handle = cx.entity().downgrade(); - move |_, window, cx| { - let Some(this) = handle.upgrade() else { + let repo_path = entry_details.repo_path.clone(); + cx.listener(move |this, _, window, cx| { + this.selected_entry = Some(ix); + window.dispatch_action(Box::new(OpenSelected), cx); + cx.notify(); + let Some(workspace) = this.workspace.upgrade() else { return; }; - this.update(cx, |this, cx| { - this.selected_entry = Some(ix); - window.dispatch_action(Box::new(OpenSelected), cx); - cx.notify(); - }); - } + let Some(git_repo) = this.active_repository.as_ref() else { + return; + }; + let Some(path) = git_repo.repo_path_to_project_path(&repo_path).and_then( + |project_path| this.project.read(cx).absolute_path(&project_path, cx), + ) else { + return; + }; + workspace.update(cx, |workspace, cx| { + ProjectDiff::deploy_at(workspace, Some(path.into()), window, cx); + }) + }) }) .child( h_flex() diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index a88eb84cd0..3757daaf7e 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -2,14 +2,17 @@ use ::settings::Settings; use git::status::FileStatus; use git_panel_settings::GitPanelSettings; use gpui::App; +use project_diff::ProjectDiff; use ui::{ActiveTheme, Color, Icon, IconName, IntoElement}; pub mod git_panel; mod git_panel_settings; +pub mod project_diff; pub mod repository_selector; pub fn init(cx: &mut App) { GitPanelSettings::register(cx); + cx.observe_new(ProjectDiff::register).detach(); } // TODO: Add updated status colors to theme diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs new file mode 100644 index 0000000000..a9dd2bd388 --- /dev/null +++ b/crates/git_ui/src/project_diff.rs @@ -0,0 +1,495 @@ +use std::{ + any::{Any, TypeId}, + path::Path, + sync::Arc, +}; + +use anyhow::Result; +use collections::HashSet; +use editor::{scroll::Autoscroll, Editor, EditorEvent}; +use feature_flags::FeatureFlagViewExt; +use futures::StreamExt; +use gpui::{ + actions, AnyElement, AnyView, App, AppContext, AsyncWindowContext, Entity, EventEmitter, + FocusHandle, Focusable, Render, Subscription, Task, WeakEntity, +}; +use language::{Anchor, Buffer, Capability, OffsetRangeExt}; +use multi_buffer::MultiBuffer; +use project::{buffer_store::BufferChangeSet, git::GitState, Project, ProjectPath}; +use theme::ActiveTheme; +use ui::prelude::*; +use util::ResultExt as _; +use workspace::{ + item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, + searchable::SearchableItemHandle, + ItemNavHistory, ToolbarItemLocation, Workspace, +}; + +use crate::git_panel::GitPanel; + +actions!(git, [ShowUncommittedChanges]); + +pub(crate) struct ProjectDiff { + multibuffer: Entity, + editor: Entity, + project: Entity, + git_state: Entity, + workspace: WeakEntity, + focus_handle: FocusHandle, + update_needed: postage::watch::Sender<()>, + pending_scroll: Option>, + + _task: Task>, + _subscription: Subscription, +} + +struct DiffBuffer { + abs_path: Arc, + buffer: Entity, + change_set: Entity, +} + +impl ProjectDiff { + pub(crate) fn register( + _: &mut Workspace, + window: Option<&mut Window>, + cx: &mut Context, + ) { + let Some(window) = window else { return }; + cx.when_flag_enabled::(window, |workspace, _, _cx| { + workspace.register_action(Self::deploy); + }); + } + + fn deploy( + workspace: &mut Workspace, + _: &ShowUncommittedChanges, + window: &mut Window, + cx: &mut Context, + ) { + Self::deploy_at(workspace, None, window, cx) + } + + pub fn deploy_at( + workspace: &mut Workspace, + path: Option>, + window: &mut Window, + cx: &mut Context, + ) { + let project_diff = if let Some(existing) = workspace.item_of_type::(cx) { + workspace.activate_item(&existing, true, true, window, cx); + existing + } else { + let workspace_handle = cx.entity().downgrade(); + let project_diff = + cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx)); + workspace.add_item_to_active_pane( + Box::new(project_diff.clone()), + None, + true, + window, + cx, + ); + project_diff + }; + if let Some(path) = path { + project_diff.update(cx, |project_diff, cx| { + project_diff.scroll_to(path, window, cx); + }) + } + } + + fn new( + project: Entity, + workspace: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let focus_handle = cx.focus_handle(); + let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); + + let editor = cx.new(|cx| { + let mut diff_display_editor = Editor::for_multibuffer( + multibuffer.clone(), + Some(project.clone()), + true, + window, + cx, + ); + diff_display_editor.set_expand_all_diff_hunks(cx); + diff_display_editor + }); + cx.subscribe_in(&editor, window, Self::handle_editor_event) + .detach(); + + let git_state = project.read(cx).git_state().clone(); + let git_state_subscription = cx.subscribe_in( + &git_state, + window, + move |this, _git_state, event, _window, _cx| match event { + project::git::Event::RepositoriesUpdated => { + *this.update_needed.borrow_mut() = (); + } + }, + ); + + let (mut send, recv) = postage::watch::channel::<()>(); + let worker = window.spawn(cx, { + let this = cx.weak_entity(); + |cx| Self::handle_status_updates(this, recv, cx) + }); + // Kick of a refresh immediately + *send.borrow_mut() = (); + + Self { + project, + git_state: git_state.clone(), + workspace, + focus_handle, + editor, + multibuffer, + pending_scroll: None, + update_needed: send, + _task: worker, + _subscription: git_state_subscription, + } + } + + pub fn scroll_to(&mut self, path: Arc, window: &mut Window, cx: &mut Context) { + if let Some(position) = self.multibuffer.read(cx).location_for_path(&path, cx) { + self.editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| { + s.select_ranges([position..position]); + }) + }) + } else { + self.pending_scroll = Some(path); + } + } + + fn handle_editor_event( + &mut self, + editor: &Entity, + event: &EditorEvent, + window: &mut Window, + cx: &mut Context, + ) { + match event { + EditorEvent::ScrollPositionChanged { .. } => editor.update(cx, |editor, cx| { + let anchor = editor.scroll_manager.anchor().anchor; + let Some((_, buffer, _)) = self.multibuffer.read(cx).excerpt_containing(anchor, cx) + else { + return; + }; + let Some(project_path) = buffer + .read(cx) + .file() + .map(|file| (file.worktree_id(cx), file.path().clone())) + else { + return; + }; + self.workspace + .update(cx, |workspace, cx| { + if let Some(git_panel) = workspace.panel::(cx) { + git_panel.update(cx, |git_panel, cx| { + git_panel.set_focused_path(project_path.into(), window, cx) + }) + } + }) + .ok(); + }), + _ => {} + } + } + + fn load_buffers(&mut self, cx: &mut Context) -> Vec>> { + let Some(repo) = self.git_state.read(cx).active_repository() else { + self.multibuffer.update(cx, |multibuffer, cx| { + multibuffer.clear(cx); + }); + return vec![]; + }; + + let mut previous_paths = self.multibuffer.read(cx).paths().collect::>(); + + let mut result = vec![]; + for entry in repo.status() { + if !entry.status.has_changes() { + continue; + } + let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else { + continue; + }; + let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { + continue; + }; + let abs_path = Arc::from(abs_path); + + previous_paths.remove(&abs_path); + let load_buffer = self + .project + .update(cx, |project, cx| project.open_buffer(project_path, cx)); + + let project = self.project.clone(); + result.push(cx.spawn(|_, mut cx| async move { + let buffer = load_buffer.await?; + let changes = project + .update(&mut cx, |project, cx| { + project.open_unstaged_changes(buffer.clone(), cx) + })? + .await?; + Ok(DiffBuffer { + abs_path, + buffer, + change_set: changes, + }) + })); + } + self.multibuffer.update(cx, |multibuffer, cx| { + for path in previous_paths { + multibuffer.remove_excerpts_for_path(path, cx); + } + }); + result + } + + fn register_buffer( + &mut self, + diff_buffer: DiffBuffer, + window: &mut Window, + cx: &mut Context, + ) { + let abs_path = diff_buffer.abs_path; + let buffer = diff_buffer.buffer; + let change_set = diff_buffer.change_set; + + let snapshot = buffer.read(cx).snapshot(); + let diff_hunk_ranges = change_set + .read(cx) + .diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot) + .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) + .collect::>(); + + self.multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + abs_path.clone(), + buffer, + diff_hunk_ranges, + editor::DEFAULT_MULTIBUFFER_CONTEXT, + cx, + ); + }); + if self.pending_scroll.as_ref() == Some(&abs_path) { + self.scroll_to(abs_path, window, cx); + } + } + + pub async fn handle_status_updates( + this: WeakEntity, + mut recv: postage::watch::Receiver<()>, + mut cx: AsyncWindowContext, + ) -> Result<()> { + while let Some(_) = recv.next().await { + let buffers_to_load = this.update(&mut cx, |this, cx| this.load_buffers(cx))?; + for buffer_to_load in buffers_to_load { + if let Some(buffer) = buffer_to_load.await.log_err() { + cx.update(|window, cx| { + this.update(cx, |this, cx| this.register_buffer(buffer, window, cx)) + .ok(); + })?; + } + } + this.update(&mut cx, |this, _| this.pending_scroll.take())?; + } + + Ok(()) + } +} + +impl EventEmitter for ProjectDiff {} + +impl Focusable for ProjectDiff { + fn focus_handle(&self, _: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for ProjectDiff { + type Event = EditorEvent; + + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } + + fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { + self.editor + .update(cx, |editor, cx| editor.deactivated(window, cx)); + } + + fn navigate( + &mut self, + data: Box, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, window, cx)) + } + + fn tab_tooltip_text(&self, _: &App) -> Option { + Some("Project Diff".into()) + } + + fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement { + Label::new("Uncommitted Changes") + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("project diagnostics") + } + + fn as_searchable(&self, _: &Entity) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn for_each_project_item( + &self, + cx: &App, + f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), + ) { + self.editor.for_each_project_item(cx, f) + } + + fn is_singleton(&self, _: &App) -> bool { + false + } + + fn set_nav_history( + &mut self, + nav_history: ItemNavHistory, + _: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn clone_on_split( + &self, + _workspace_id: Option, + window: &mut Window, + cx: &mut Context, + ) -> Option> + where + Self: Sized, + { + Some( + cx.new(|cx| ProjectDiff::new(self.project.clone(), self.workspace.clone(), window, cx)), + ) + } + + fn is_dirty(&self, cx: &App) -> bool { + self.multibuffer.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &App) -> bool { + self.multibuffer.read(cx).has_conflict(cx) + } + + fn can_save(&self, _: &App) -> bool { + true + } + + fn save( + &mut self, + format: bool, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.editor.save(format, project, window, cx) + } + + fn save_as( + &mut self, + _: Entity, + _: ProjectPath, + _window: &mut Window, + _: &mut Context, + ) -> Task> { + unreachable!() + } + + fn reload( + &mut self, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + self.editor.reload(project, window, cx) + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a Entity, + _: &'a App, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } + + fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { + self.editor.breadcrumbs(theme, cx) + } + + fn added_to_workspace( + &mut self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + self.editor.update(cx, |editor, cx| { + editor.added_to_workspace(workspace, window, cx) + }); + } +} + +impl Render for ProjectDiff { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let is_empty = self.multibuffer.read(cx).is_empty(); + if is_empty { + div() + .bg(cx.theme().colors().editor_background) + .flex() + .items_center() + .justify_center() + .size_full() + .child(Label::new("No uncommitted changes")) + } else { + div() + .bg(cx.theme().colors().editor_background) + .flex() + .items_center() + .justify_center() + .size_full() + .child(self.editor.clone()) + } + } +} diff --git a/crates/git_ui/src/repository_selector.rs b/crates/git_ui/src/repository_selector.rs index a41c966845..6ec2dab6c6 100644 --- a/crates/git_ui/src/repository_selector.rs +++ b/crates/git_ui/src/repository_selector.rs @@ -20,10 +20,8 @@ pub struct RepositorySelector { impl RepositorySelector { pub fn new(project: Entity, window: &mut Window, cx: &mut Context) -> Self { - let git_state = project.read(cx).git_state().cloned(); - let all_repositories = git_state - .as_ref() - .map_or(vec![], |git_state| git_state.read(cx).all_repositories()); + let git_state = project.read(cx).git_state().clone(); + let all_repositories = git_state.read(cx).all_repositories(); let filtered_repositories = all_repositories.clone(); let delegate = RepositorySelectorDelegate { project: project.downgrade(), @@ -38,11 +36,8 @@ impl RepositorySelector { .max_height(Some(rems(20.).into())) }); - let _subscriptions = if let Some(git_state) = git_state { - vec![cx.subscribe_in(&git_state, window, Self::handle_project_git_event)] - } else { - Vec::new() - }; + let _subscriptions = + vec![cx.subscribe_in(&git_state, window, Self::handle_project_git_event)]; RepositorySelector { picker, diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 5143a02283..8d64682b8d 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -80,7 +80,7 @@ impl GoToLine { let last_line = editor .buffer() .read(cx) - .excerpts_for_buffer(&active_buffer, cx) + .excerpts_for_buffer(snapshot.remote_id(), cx) .into_iter() .map(move |(_, range)| text::ToPoint::to_point(&range.context.end, &snapshot).row) .max() diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index d218d28404..3eaa6cf955 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -35,6 +35,7 @@ use std::{ iter::{self, FromIterator}, mem, ops::{Range, RangeBounds, Sub}, + path::Path, str, sync::Arc, time::{Duration, Instant}, @@ -65,6 +66,8 @@ pub struct MultiBuffer { snapshot: RefCell, /// Contains the state of the buffers being edited buffers: RefCell>, + // only used by consumers using `set_excerpts_for_buffer` + buffers_by_path: BTreeMap, Vec>, diff_bases: HashMap, all_diff_hunks_expanded: bool, subscriptions: Topic, @@ -494,6 +497,7 @@ impl MultiBuffer { singleton: false, capability, title: None, + buffers_by_path: Default::default(), history: History { next_transaction_id: clock::Lamport::default(), undo_stack: Vec::new(), @@ -508,6 +512,7 @@ impl MultiBuffer { Self { snapshot: Default::default(), buffers: Default::default(), + buffers_by_path: Default::default(), diff_bases: HashMap::default(), all_diff_hunks_expanded: false, subscriptions: Default::default(), @@ -561,6 +566,7 @@ impl MultiBuffer { Self { snapshot: RefCell::new(self.snapshot.borrow().clone()), buffers: RefCell::new(buffers), + buffers_by_path: Default::default(), diff_bases, all_diff_hunks_expanded: self.all_diff_hunks_expanded, subscriptions: Default::default(), @@ -648,8 +654,8 @@ impl MultiBuffer { self.read(cx).len() } - pub fn is_empty(&self, cx: &App) -> bool { - self.len(cx) != 0 + pub fn is_empty(&self) -> bool { + self.buffers.borrow().is_empty() } pub fn symbols_containing( @@ -1388,6 +1394,138 @@ impl MultiBuffer { anchor_ranges } + pub fn location_for_path(&self, path: &Arc, cx: &App) -> Option { + let excerpt_id = self.buffers_by_path.get(path)?.first()?; + let snapshot = self.snapshot(cx); + let excerpt = snapshot.excerpt(*excerpt_id)?; + Some(Anchor::in_buffer( + *excerpt_id, + excerpt.buffer_id, + excerpt.range.context.start, + )) + } + + pub fn set_excerpts_for_path( + &mut self, + path: Arc, + buffer: Entity, + ranges: Vec>, + context_line_count: u32, + cx: &mut Context, + ) { + let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot()); + let (mut insert_after, excerpt_ids) = + if let Some(existing) = self.buffers_by_path.get(&path) { + (*existing.last().unwrap(), existing.clone()) + } else { + ( + self.buffers_by_path + .range(..path.clone()) + .next_back() + .map(|(_, value)| *value.last().unwrap()) + .unwrap_or(ExcerptId::min()), + Vec::default(), + ) + }; + + let (new, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); + + let mut new_iter = new.into_iter().peekable(); + let mut existing_iter = excerpt_ids.into_iter().peekable(); + + let mut new_excerpt_ids = Vec::new(); + let mut to_remove = Vec::new(); + let mut to_insert = Vec::new(); + let snapshot = self.snapshot(cx); + + let mut excerpts_cursor = snapshot.excerpts.cursor::>(&()); + excerpts_cursor.next(&()); + + loop { + let (new, existing) = match (new_iter.peek(), existing_iter.peek()) { + (Some(new), Some(existing)) => (new, existing), + (None, None) => break, + (None, Some(_)) => { + to_remove.push(existing_iter.next().unwrap()); + continue; + } + (Some(_), None) => { + to_insert.push(new_iter.next().unwrap()); + continue; + } + }; + let locator = snapshot.excerpt_locator_for_id(*existing); + excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &()); + let existing_excerpt = excerpts_cursor.item().unwrap(); + if existing_excerpt.buffer_id != buffer_snapshot.remote_id() { + to_remove.push(existing_iter.next().unwrap()); + to_insert.push(new_iter.next().unwrap()); + continue; + } + + let existing_start = existing_excerpt + .range + .context + .start + .to_point(&buffer_snapshot); + let existing_end = existing_excerpt + .range + .context + .end + .to_point(&buffer_snapshot); + + if existing_end < new.context.start { + to_remove.push(existing_iter.next().unwrap()); + continue; + } else if existing_start > new.context.end { + to_insert.push(new_iter.next().unwrap()); + continue; + } + + // maybe merge overlapping excerpts? + // it's hard to distinguish between a manually expanded excerpt, and one that + // got smaller because of a missing diff. + // + if existing_start == new.context.start && existing_end == new.context.end { + new_excerpt_ids.append(&mut self.insert_excerpts_after( + insert_after, + buffer.clone(), + mem::take(&mut to_insert), + cx, + )); + insert_after = existing_iter.next().unwrap(); + new_excerpt_ids.push(insert_after); + new_iter.next(); + } else { + to_remove.push(existing_iter.next().unwrap()); + to_insert.push(new_iter.next().unwrap()); + } + } + + new_excerpt_ids.append(&mut self.insert_excerpts_after( + insert_after, + buffer, + to_insert, + cx, + )); + self.remove_excerpts(to_remove, cx); + if new_excerpt_ids.is_empty() { + self.buffers_by_path.remove(&path); + } else { + self.buffers_by_path.insert(path, new_excerpt_ids); + } + } + + pub fn paths(&self) -> impl Iterator> + '_ { + self.buffers_by_path.keys().cloned() + } + + pub fn remove_excerpts_for_path(&mut self, path: Arc, cx: &mut Context) { + if let Some(to_remove) = self.buffers_by_path.remove(&path) { + self.remove_excerpts(to_remove, cx) + } + } + pub fn push_multiple_excerpts_with_context_lines( &self, buffers_with_ranges: Vec<(Entity, Vec>)>, @@ -1654,7 +1792,7 @@ impl MultiBuffer { pub fn excerpts_for_buffer( &self, - buffer: &Entity, + buffer_id: BufferId, cx: &App, ) -> Vec<(ExcerptId, ExcerptRange)> { let mut excerpts = Vec::new(); @@ -1662,7 +1800,7 @@ impl MultiBuffer { let buffers = self.buffers.borrow(); let mut cursor = snapshot.excerpts.cursor::>(&()); for locator in buffers - .get(&buffer.read(cx).remote_id()) + .get(&buffer_id) .map(|state| &state.excerpts) .into_iter() .flatten() @@ -1812,7 +1950,7 @@ impl MultiBuffer { ) -> Option { let mut found = None; let snapshot = buffer.read(cx).snapshot(); - for (excerpt_id, range) in self.excerpts_for_buffer(buffer, cx) { + for (excerpt_id, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) { let start = range.context.start.to_point(&snapshot); let end = range.context.end.to_point(&snapshot); if start <= point && point < end { @@ -4790,7 +4928,7 @@ impl MultiBufferSnapshot { cursor.next_excerpt(); let mut visited_end = false; - iter::from_fn(move || { + iter::from_fn(move || loop { if self.singleton { return None; } @@ -4800,7 +4938,8 @@ impl MultiBufferSnapshot { let next_region_start = if let Some(region) = &next_region { if !bounds.contains(®ion.range.start.key) { - return None; + prev_region = next_region; + continue; } region.range.start.value.unwrap() } else { @@ -4847,7 +4986,7 @@ impl MultiBufferSnapshot { prev_region = next_region; - Some(ExcerptBoundary { row, prev, next }) + return Some(ExcerptBoundary { row, prev, next }); }) } diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index 76cca4a830..b9a40e8555 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -6,7 +6,7 @@ use language::{Buffer, Rope}; use parking_lot::RwLock; use rand::prelude::*; use settings::SettingsStore; -use std::env; +use std::{env, path::PathBuf}; use util::test::sample_text; #[ctor::ctor] @@ -315,7 +315,8 @@ fn test_excerpt_boundaries_and_clipping(cx: &mut App) { ); let snapshot = multibuffer.update(cx, |multibuffer, cx| { - let (buffer_2_excerpt_id, _) = multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone(); + let (buffer_2_excerpt_id, _) = + multibuffer.excerpts_for_buffer(buffer_2.read(cx).remote_id(), cx)[0].clone(); multibuffer.remove_excerpts([buffer_2_excerpt_id], cx); multibuffer.snapshot(cx) }); @@ -1527,6 +1528,202 @@ fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) { ); } +#[gpui::test] +fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) { + let buf1 = cx.new(|cx| { + Buffer::local( + indoc! { + "zero + one + two + three + four + five + six + seven + ", + }, + cx, + ) + }); + let path1: Arc = Arc::from(PathBuf::from("path1")); + let buf2 = cx.new(|cx| { + Buffer::local( + indoc! { + "000 + 111 + 222 + 333 + 444 + 555 + 666 + 777 + 888 + 999 + " + }, + cx, + ) + }); + let path2: Arc = Arc::from(PathBuf::from("path2")); + + let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + path1.clone(), + buf1.clone(), + vec![Point::row_range(0..1)], + 2, + cx, + ); + }); + + assert_excerpts_match( + &multibuffer, + cx, + indoc! { + "----- + zero + one + two + three + " + }, + ); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx); + }); + + assert_excerpts_match(&multibuffer, cx, ""); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + path1.clone(), + buf1.clone(), + vec![Point::row_range(0..1), Point::row_range(7..8)], + 2, + cx, + ); + }); + + assert_excerpts_match( + &multibuffer, + cx, + indoc! {"----- + zero + one + two + three + ----- + five + six + seven + "}, + ); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + path1.clone(), + buf1.clone(), + vec![Point::row_range(0..1), Point::row_range(5..6)], + 2, + cx, + ); + }); + + assert_excerpts_match( + &multibuffer, + cx, + indoc! {"----- + zero + one + two + three + four + five + six + seven + "}, + ); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + path2.clone(), + buf2.clone(), + vec![Point::row_range(2..3)], + 2, + cx, + ); + }); + + assert_excerpts_match( + &multibuffer, + cx, + indoc! {"----- + zero + one + two + three + four + five + six + seven + ----- + 000 + 111 + 222 + 333 + 444 + 555 + "}, + ); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx); + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + path1.clone(), + buf1.clone(), + vec![Point::row_range(3..4)], + 2, + cx, + ); + }); + + assert_excerpts_match( + &multibuffer, + cx, + indoc! {"----- + one + two + three + four + five + six + ----- + 000 + 111 + 222 + 333 + 444 + 555 + "}, + ); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.set_excerpts_for_path( + path1.clone(), + buf1.clone(), + vec![Point::row_range(3..4)], + 2, + cx, + ); + }); +} + #[gpui::test] fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { let base_text_1 = indoc!( @@ -2700,6 +2897,25 @@ fn format_diff( .join("\n") } +#[track_caller] +fn assert_excerpts_match( + multibuffer: &Entity, + cx: &mut TestAppContext, + expected: &str, +) { + let mut output = String::new(); + multibuffer.read_with(cx, |multibuffer, cx| { + for (_, buffer, range) in multibuffer.snapshot(cx).excerpts() { + output.push_str("-----\n"); + output.extend(buffer.text_for_range(range.context)); + if !output.ends_with('\n') { + output.push('\n'); + } + } + }); + assert_eq!(output, expected); +} + #[track_caller] fn assert_new_snapshot( multibuffer: &Entity, diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 8f99fe198d..723f1e19f3 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1017,7 +1017,7 @@ impl OutlinePanel { .map(|buffer| { active_multi_buffer .read(cx) - .excerpts_for_buffer(&buffer, cx) + .excerpts_for_buffer(buffer.read(cx).remote_id(), cx) }) .and_then(|excerpts| { let (excerpt_id, excerpt_range) = excerpts.first()?; diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index 2a6b50a27f..325a363c10 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -12,7 +12,7 @@ use gpui::{App, Context, Entity, EventEmitter, SharedString, Subscription, WeakE use rpc::{proto, AnyProtoClient}; use settings::WorktreeId; use std::sync::Arc; -use util::maybe; +use util::{maybe, ResultExt}; use worktree::{ProjectEntryId, RepositoryEntry, StatusEntry}; pub struct GitState { @@ -332,7 +332,7 @@ impl GitState { impl RepositoryHandle { pub fn display_name(&self, project: &Project, cx: &App) -> SharedString { maybe!({ - let path = self.unrelativize(&"".into())?; + let path = self.repo_path_to_project_path(&"".into())?; Some( project .absolute_path(&path, cx)? @@ -367,11 +367,18 @@ impl RepositoryHandle { self.repository_entry.status() } - pub fn unrelativize(&self, path: &RepoPath) -> Option { + pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option { let path = self.repository_entry.unrelativize(path)?; Some((self.worktree_id, path).into()) } + pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option { + if path.worktree_id != self.worktree_id { + return None; + } + self.repository_entry.relativize(&path.path).log_err() + } + pub fn stage_entries( &self, entries: Vec, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4f6ae341ab..bb88982959 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -158,7 +158,7 @@ pub struct Project { fs: Arc, ssh_client: Option>, client_state: ProjectClientState, - git_state: Option>, + git_state: Entity, collaborators: HashMap, client_subscriptions: Vec, worktree_store: Entity, @@ -701,7 +701,7 @@ impl Project { ) }); - let git_state = Some(cx.new(|cx| GitState::new(&worktree_store, None, None, cx))); + let git_state = cx.new(|cx| GitState::new(&worktree_store, None, None, cx)); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); @@ -821,14 +821,14 @@ impl Project { }); cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - let git_state = Some(cx.new(|cx| { + let git_state = cx.new(|cx| { GitState::new( &worktree_store, Some(ssh_proto.clone()), Some(ProjectId(SSH_PROJECT_ID)), cx, ) - })); + }); cx.subscribe(&ssh, Self::on_ssh_event).detach(); cx.observe(&ssh, |_, _, cx| cx.notify()).detach(); @@ -1026,15 +1026,14 @@ impl Project { SettingsObserver::new_remote(worktree_store.clone(), task_store.clone(), cx) })?; - let git_state = Some(cx.new(|cx| { + let git_state = cx.new(|cx| { GitState::new( &worktree_store, Some(client.clone().into()), Some(ProjectId(remote_id)), cx, ) - })) - .transpose()?; + })?; let this = cx.new(|cx| { let replica_id = response.payload.replica_id as ReplicaId; @@ -4117,7 +4116,6 @@ impl Project { this.update(cx, |project, cx| { let repository_handle = project .git_state() - .context("missing git state")? .read(cx) .all_repositories() .into_iter() @@ -4332,19 +4330,16 @@ impl Project { &self.buffer_store } - pub fn git_state(&self) -> Option<&Entity> { - self.git_state.as_ref() + pub fn git_state(&self) -> &Entity { + &self.git_state } pub fn active_repository(&self, cx: &App) -> Option { - self.git_state() - .and_then(|git_state| git_state.read(cx).active_repository()) + self.git_state.read(cx).active_repository() } pub fn all_repositories(&self, cx: &App) -> Vec { - self.git_state() - .map(|git_state| git_state.read(cx).all_repositories()) - .unwrap_or_default() + self.git_state.read(cx).all_repositories() } } diff --git a/crates/rope/src/point.rs b/crates/rope/src/point.rs index 60e6fb3a39..a9d2e3a174 100644 --- a/crates/rope/src/point.rs +++ b/crates/rope/src/point.rs @@ -1,6 +1,6 @@ use std::{ cmp::Ordering, - ops::{Add, AddAssign, Sub}, + ops::{Add, AddAssign, Range, Sub}, }; /// A zero-indexed point in a text buffer consisting of a row and column. @@ -20,6 +20,16 @@ impl Point { Point { row, column } } + pub fn row_range(range: Range) -> Range { + Point { + row: range.start, + column: 0, + }..Point { + row: range.end, + column: 0, + } + } + pub fn zero() -> Self { Point::new(0, 0) } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 5e0bbef48e..9e0c92f189 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -19,7 +19,7 @@ use collections::VecDeque; use command_palette_hooks::CommandPaletteFilter; use editor::ProposedChangesEditorToolbar; use editor::{scroll::Autoscroll, Editor, MultiBuffer}; -use feature_flags::FeatureFlagAppExt; +use feature_flags::{FeatureFlagAppExt, FeatureFlagViewExt, GitUiFeatureFlag}; use futures::{channel::mpsc, select_biased, StreamExt}; use gpui::{ actions, point, px, Action, App, AppContext as _, AsyncApp, Context, DismissEvent, Element, @@ -364,8 +364,6 @@ fn initialize_panels( ) { let assistant2_feature_flag = cx.wait_for_flag_or_timeout::(Duration::from_secs(5)); - let git_ui_feature_flag = - cx.wait_for_flag_or_timeout::(Duration::from_secs(5)); let prompt_builder = prompt_builder.clone(); @@ -405,19 +403,10 @@ fn initialize_panels( workspace.add_panel(channels_panel, window, cx); workspace.add_panel(chat_panel, window, cx); workspace.add_panel(notification_panel, window, cx); - })?; - - let git_ui_enabled = git_ui_feature_flag.await; - - let git_panel = if git_ui_enabled { - Some(git_ui::git_panel::GitPanel::load(workspace_handle.clone(), cx.clone()).await?) - } else { - None - }; - workspace_handle.update_in(&mut cx, |workspace, window, cx| { - if let Some(git_panel) = git_panel { + cx.when_flag_enabled::(window, |workspace, window, cx| { + let git_panel = git_ui::git_panel::GitPanel::new(workspace, window, None, cx); workspace.add_panel(git_panel, window, cx); - } + }); })?; let is_assistant2_enabled = if cfg!(test) {