Project Diff 2 (#23891)
This adds a new version of the project diff editor to go alongside the new git panel. The basics seem to be working, but still todo: * [ ] Fix untracked files * [ ] Fix deleted files * [ ] Show commit message editor at top * [x] Handle empty state * [x] Fix panic where locator sometimes seeks to wrong excerpt Release Notes: - N/A
This commit is contained in:
parent
27a413a5e3
commit
45708d2680
21 changed files with 1023 additions and 125 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -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
|
||||
|
|
495
crates/git_ui/src/project_diff.rs
Normal file
495
crates/git_ui/src/project_diff.rs
Normal file
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue