Detailed changes
@@ -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",
@@ -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()),
@@ -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
@@ -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);
});
@@ -1,2 +1 @@
pub mod blame;
-pub mod project_diff;
@@ -743,12 +743,12 @@ fn determine_query_ranges(
excerpt_visible_range: Range<usize>,
cx: &mut Context<'_, MultiBuffer>,
) -> Option<QueryRanges> {
+ 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;
@@ -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<bool> = LazyLock::new(|| {
+ std::env::var("ZED_DISABLE_STAFF").map_or(false, |value| !value.is_empty() && value != "0")
+});
+
impl FeatureFlags {
fn has_flag<T: FeatureFlag>(&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<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
where
F: Fn(bool, &mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static;
+
+ fn when_flag_enabled<T: FeatureFlag>(
+ &mut self,
+ window: &mut Window,
+ callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
+ );
}
impl<V> FeatureFlagViewExt<V> for Context<'_, V>
@@ -112,6 +134,35 @@ where
callback(feature_flags.has_flag::<T>(), v, window, cx);
})
}
+
+ fn when_flag_enabled<T: FeatureFlag>(
+ &mut self,
+ window: &mut Window,
+ callback: impl Fn(&mut V, &mut Window, &mut Context<V>) + Send + Sync + 'static,
+ ) {
+ if self
+ .try_global::<FeatureFlags>()
+ .is_some_and(|f| f.has_flag::<T>())
+ || 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::<FeatureFlags>(window, {
+ let subscription = subscription.clone();
+ move |v, window, cx| {
+ let feature_flags = cx.global::<FeatureFlags>();
+ if feature_flags.has_flag::<T>() {
+ callback(v, window, cx);
+ subscription.take();
+ }
+ }
+ });
+ subscription.borrow_mut().replace(inner);
+ }
}
pub trait FeatureFlagAppExt {
@@ -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) {
@@ -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
@@ -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<Workspace>,
- cx: AsyncWindowContext,
- ) -> Task<Result<Entity<Self>>> {
- 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<Self> {
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<Self>) {
+ 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<Self>) {
- // 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()
@@ -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
@@ -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<MultiBuffer>,
+ editor: Entity<Editor>,
+ project: Entity<Project>,
+ git_state: Entity<GitState>,
+ workspace: WeakEntity<Workspace>,
+ focus_handle: FocusHandle,
+ update_needed: postage::watch::Sender<()>,
+ pending_scroll: Option<Arc<Path>>,
+
+ _task: Task<Result<()>>,
+ _subscription: Subscription,
+}
+
+struct DiffBuffer {
+ abs_path: Arc<Path>,
+ buffer: Entity<Buffer>,
+ change_set: Entity<BufferChangeSet>,
+}
+
+impl ProjectDiff {
+ pub(crate) fn register(
+ _: &mut Workspace,
+ window: Option<&mut Window>,
+ cx: &mut Context<Workspace>,
+ ) {
+ let Some(window) = window else { return };
+ cx.when_flag_enabled::<feature_flags::GitUiFeatureFlag>(window, |workspace, _, _cx| {
+ workspace.register_action(Self::deploy);
+ });
+ }
+
+ fn deploy(
+ workspace: &mut Workspace,
+ _: &ShowUncommittedChanges,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) {
+ Self::deploy_at(workspace, None, window, cx)
+ }
+
+ pub fn deploy_at(
+ workspace: &mut Workspace,
+ path: Option<Arc<Path>>,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) {
+ let project_diff = if let Some(existing) = workspace.item_of_type::<Self>(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<Project>,
+ workspace: WeakEntity<Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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<Path>, window: &mut Window, cx: &mut Context<Self>) {
+ 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<Editor>,
+ event: &EditorEvent,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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::<GitPanel>(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<Self>) -> Vec<Task<Result<DiffBuffer>>> {
+ 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::<HashSet<_>>();
+
+ 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<Self>,
+ ) {
+ 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::<Vec<_>>();
+
+ 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<Self>,
+ 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<EditorEvent> 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>) {
+ self.editor
+ .update(cx, |editor, cx| editor.deactivated(window, cx));
+ }
+
+ fn navigate(
+ &mut self,
+ data: Box<dyn Any>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ self.editor
+ .update(cx, |editor, cx| editor.navigate(data, window, cx))
+ }
+
+ fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
+ 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<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ 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>,
+ ) {
+ self.editor.update(cx, |editor, _| {
+ editor.set_nav_history(Some(nav_history));
+ });
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: Option<workspace::WorkspaceId>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<Entity<Self>>
+ 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<Project>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ self.editor.save(format, project, window, cx)
+ }
+
+ fn save_as(
+ &mut self,
+ _: Entity<Project>,
+ _: ProjectPath,
+ _window: &mut Window,
+ _: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ unreachable!()
+ }
+
+ fn reload(
+ &mut self,
+ project: Entity<Project>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
+ self.editor.reload(project, window, cx)
+ }
+
+ fn act_as_type<'a>(
+ &'a self,
+ type_id: TypeId,
+ self_handle: &'a Entity<Self>,
+ _: &'a App,
+ ) -> Option<AnyView> {
+ if type_id == TypeId::of::<Self>() {
+ Some(self_handle.to_any())
+ } else if type_id == TypeId::of::<Editor>() {
+ Some(self.editor.to_any())
+ } else {
+ None
+ }
+ }
+
+ fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
+ ToolbarItemLocation::PrimaryLeft
+ }
+
+ fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
+ self.editor.breadcrumbs(theme, cx)
+ }
+
+ fn added_to_workspace(
+ &mut self,
+ workspace: &mut Workspace,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ 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<Self>) -> 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())
+ }
+ }
+}
@@ -20,10 +20,8 @@ pub struct RepositorySelector {
impl RepositorySelector {
pub fn new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> 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,
@@ -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()
@@ -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<MultiBufferSnapshot>,
/// Contains the state of the buffers being edited
buffers: RefCell<HashMap<BufferId, BufferState>>,
+ // only used by consumers using `set_excerpts_for_buffer`
+ buffers_by_path: BTreeMap<Arc<Path>, Vec<ExcerptId>>,
diff_bases: HashMap<BufferId, ChangeSetState>,
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<T: ToOffset>(
@@ -1388,6 +1394,138 @@ impl MultiBuffer {
anchor_ranges
}
+ pub fn location_for_path(&self, path: &Arc<Path>, cx: &App) -> Option<Anchor> {
+ 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<Path>,
+ buffer: Entity<Buffer>,
+ ranges: Vec<Range<Point>>,
+ context_line_count: u32,
+ cx: &mut Context<Self>,
+ ) {
+ 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::<Option<&Locator>>(&());
+ 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<Item = Arc<Path>> + '_ {
+ self.buffers_by_path.keys().cloned()
+ }
+
+ pub fn remove_excerpts_for_path(&mut self, path: Arc<Path>, cx: &mut Context<Self>) {
+ 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<Buffer>, Vec<Range<text::Anchor>>)>,
@@ -1654,7 +1792,7 @@ impl MultiBuffer {
pub fn excerpts_for_buffer(
&self,
- buffer: &Entity<Buffer>,
+ buffer_id: BufferId,
cx: &App,
) -> Vec<(ExcerptId, ExcerptRange<text::Anchor>)> {
let mut excerpts = Vec::new();
@@ -1662,7 +1800,7 @@ impl MultiBuffer {
let buffers = self.buffers.borrow();
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>(&());
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<Anchor> {
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 });
})
}
@@ -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<Path> = 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<Path> = 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<MultiBuffer>,
+ 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<MultiBuffer>,
@@ -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()?;
@@ -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<ProjectPath> {
+ pub fn repo_path_to_project_path(&self, path: &RepoPath) -> Option<ProjectPath> {
let path = self.repository_entry.unrelativize(path)?;
Some((self.worktree_id, path).into())
}
+ pub fn project_path_to_repo_path(&self, path: &ProjectPath) -> Option<RepoPath> {
+ if path.worktree_id != self.worktree_id {
+ return None;
+ }
+ self.repository_entry.relativize(&path.path).log_err()
+ }
+
pub fn stage_entries(
&self,
entries: Vec<RepoPath>,
@@ -158,7 +158,7 @@ pub struct Project {
fs: Arc<dyn Fs>,
ssh_client: Option<Entity<SshRemoteClient>>,
client_state: ProjectClientState,
- git_state: Option<Entity<GitState>>,
+ git_state: Entity<GitState>,
collaborators: HashMap<proto::PeerId, Collaborator>,
client_subscriptions: Vec<client::Subscription>,
worktree_store: Entity<WorktreeStore>,
@@ -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<GitState>> {
- self.git_state.as_ref()
+ pub fn git_state(&self) -> &Entity<GitState> {
+ &self.git_state
}
pub fn active_repository(&self, cx: &App) -> Option<RepositoryHandle> {
- 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<RepositoryHandle> {
- self.git_state()
- .map(|git_state| git_state.read(cx).all_repositories())
- .unwrap_or_default()
+ self.git_state.read(cx).all_repositories()
}
}
@@ -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<u32>) -> Range<Self> {
+ Point {
+ row: range.start,
+ column: 0,
+ }..Point {
+ row: range.end,
+ column: 0,
+ }
+ }
+
pub fn zero() -> Self {
Point::new(0, 0)
}
@@ -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::<feature_flags::Assistant2FeatureFlag>(Duration::from_secs(5));
- let git_ui_feature_flag =
- cx.wait_for_flag_or_timeout::<feature_flags::GitUiFeatureFlag>(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::<GitUiFeatureFlag>(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) {