Allow viewing past commits in Zed (#27636)
This PR adds functionality for loading the diff for an arbitrary git commit, and displaying it in a tab. To retrieve the diff for the commit, I'm using a single `git cat-file --batch` invocation to efficiently load both the old and new versions of each file that was changed in the commit. Todo * Features * [x] Open the commit view when clicking the most recent commit message in the commit panel * [x] Open the commit view when clicking a SHA in a git blame column * [x] Open the commit view when clicking a SHA in a commit tooltip * [x] Make it work over RPC * [x] Allow buffer search in commit view * [x] Command palette action to open the commit for the current blame line * Styling * [x] Add a header that shows the author, timestamp, and the full commit message * [x] Remove stage/unstage buttons in commit view * [x] Truncate the commit message in the tab * Bugs * [x] Dedup commit tabs within a pane * [x] Add a tooltip to the tab Release Notes: - Added the ability to show past commits in Zed. You can view the most recent commit by clicking its message in the commit panel. And when viewing a git blame, you can show any commit by clicking its sha.
This commit is contained in:
parent
33912011b7
commit
8546dc101d
28 changed files with 1742 additions and 603 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -4402,7 +4402,6 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
"chrono",
|
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
|
@ -4451,7 +4450,6 @@ dependencies = [
|
||||||
"text",
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"time",
|
"time",
|
||||||
"time_format",
|
|
||||||
"tree-sitter-html",
|
"tree-sitter-html",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"tree-sitter-typescript",
|
"tree-sitter-typescript",
|
||||||
|
@ -5715,6 +5713,7 @@ dependencies = [
|
||||||
"askpass",
|
"askpass",
|
||||||
"assistant_settings",
|
"assistant_settings",
|
||||||
"buffer_diff",
|
"buffer_diff",
|
||||||
|
"chrono",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
"component",
|
"component",
|
||||||
|
@ -5732,6 +5731,7 @@ dependencies = [
|
||||||
"linkify",
|
"linkify",
|
||||||
"linkme",
|
"linkme",
|
||||||
"log",
|
"log",
|
||||||
|
"markdown",
|
||||||
"menu",
|
"menu",
|
||||||
"multi_buffer",
|
"multi_buffer",
|
||||||
"notifications",
|
"notifications",
|
||||||
|
|
|
@ -483,7 +483,7 @@ impl TrackedBuffer {
|
||||||
buffer_without_edits
|
buffer_without_edits
|
||||||
.update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
|
.update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
|
||||||
let primary_diff_update = self.diff.update(cx, |diff, cx| {
|
let primary_diff_update = self.diff.update(cx, |diff, cx| {
|
||||||
diff.set_base_text(
|
diff.set_base_text_buffer(
|
||||||
buffer_without_edits,
|
buffer_without_edits,
|
||||||
self.buffer.read(cx).text_snapshot(),
|
self.buffer.read(cx).text_snapshot(),
|
||||||
cx,
|
cx,
|
||||||
|
@ -500,7 +500,7 @@ impl TrackedBuffer {
|
||||||
buffer.undo_operations(unreviewed_edits_to_undo, cx)
|
buffer.undo_operations(unreviewed_edits_to_undo, cx)
|
||||||
});
|
});
|
||||||
let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
|
let secondary_diff_update = self.secondary_diff.update(cx, |diff, cx| {
|
||||||
diff.set_base_text(
|
diff.set_base_text_buffer(
|
||||||
buffer_without_unreviewed_edits.clone(),
|
buffer_without_unreviewed_edits.clone(),
|
||||||
self.buffer.read(cx).text_snapshot(),
|
self.buffer.read(cx).text_snapshot(),
|
||||||
cx,
|
cx,
|
||||||
|
@ -559,13 +559,7 @@ impl TrackedBuffer {
|
||||||
if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
|
if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
|
||||||
primary_diff
|
primary_diff
|
||||||
.update(cx, |diff, cx| {
|
.update(cx, |diff, cx| {
|
||||||
diff.set_snapshot(
|
diff.set_snapshot(primary_diff_snapshot, &buffer_snapshot, None, cx)
|
||||||
&buffer_snapshot,
|
|
||||||
primary_diff_snapshot,
|
|
||||||
false,
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
@ -574,9 +568,8 @@ impl TrackedBuffer {
|
||||||
secondary_diff
|
secondary_diff
|
||||||
.update(cx, |diff, cx| {
|
.update(cx, |diff, cx| {
|
||||||
diff.set_snapshot(
|
diff.set_snapshot(
|
||||||
&buffer_snapshot,
|
|
||||||
secondary_diff_snapshot,
|
secondary_diff_snapshot,
|
||||||
false,
|
&buffer_snapshot,
|
||||||
None,
|
None,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
|
@ -142,6 +142,96 @@ impl std::fmt::Debug for BufferDiffInner {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BufferDiffSnapshot {
|
impl BufferDiffSnapshot {
|
||||||
|
fn empty(buffer: &text::BufferSnapshot, cx: &mut App) -> BufferDiffSnapshot {
|
||||||
|
BufferDiffSnapshot {
|
||||||
|
inner: BufferDiffInner {
|
||||||
|
base_text: language::Buffer::build_empty_snapshot(cx),
|
||||||
|
hunks: SumTree::new(buffer),
|
||||||
|
pending_hunks: SumTree::new(buffer),
|
||||||
|
base_text_exists: false,
|
||||||
|
},
|
||||||
|
secondary_diff: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_with_base_text(
|
||||||
|
buffer: text::BufferSnapshot,
|
||||||
|
base_text: Option<Arc<String>>,
|
||||||
|
language: Option<Arc<Language>>,
|
||||||
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> impl Future<Output = Self> + use<> {
|
||||||
|
let base_text_pair;
|
||||||
|
let base_text_exists;
|
||||||
|
let base_text_snapshot;
|
||||||
|
if let Some(text) = &base_text {
|
||||||
|
let base_text_rope = Rope::from(text.as_str());
|
||||||
|
base_text_pair = Some((text.clone(), base_text_rope.clone()));
|
||||||
|
let snapshot = language::Buffer::build_snapshot(
|
||||||
|
base_text_rope,
|
||||||
|
language.clone(),
|
||||||
|
language_registry.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
base_text_snapshot = cx.background_spawn(snapshot);
|
||||||
|
base_text_exists = true;
|
||||||
|
} else {
|
||||||
|
base_text_pair = None;
|
||||||
|
base_text_snapshot = Task::ready(language::Buffer::build_empty_snapshot(cx));
|
||||||
|
base_text_exists = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let hunks = cx.background_spawn({
|
||||||
|
let buffer = buffer.clone();
|
||||||
|
async move { compute_hunks(base_text_pair, buffer) }
|
||||||
|
});
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
|
||||||
|
Self {
|
||||||
|
inner: BufferDiffInner {
|
||||||
|
base_text,
|
||||||
|
hunks,
|
||||||
|
base_text_exists,
|
||||||
|
pending_hunks: SumTree::new(&buffer),
|
||||||
|
},
|
||||||
|
secondary_diff: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_base_buffer(
|
||||||
|
buffer: text::BufferSnapshot,
|
||||||
|
base_text: Option<Arc<String>>,
|
||||||
|
base_text_snapshot: language::BufferSnapshot,
|
||||||
|
cx: &App,
|
||||||
|
) -> impl Future<Output = Self> + use<> {
|
||||||
|
let base_text_exists = base_text.is_some();
|
||||||
|
let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone()));
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
Self {
|
||||||
|
inner: BufferDiffInner {
|
||||||
|
base_text: base_text_snapshot,
|
||||||
|
pending_hunks: SumTree::new(&buffer),
|
||||||
|
hunks: compute_hunks(base_text_pair, buffer),
|
||||||
|
base_text_exists,
|
||||||
|
},
|
||||||
|
secondary_diff: None,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn new_sync(
|
||||||
|
buffer: text::BufferSnapshot,
|
||||||
|
diff_base: String,
|
||||||
|
cx: &mut gpui::TestAppContext,
|
||||||
|
) -> BufferDiffSnapshot {
|
||||||
|
cx.executor().block(cx.update(|cx| {
|
||||||
|
Self::new_with_base_text(buffer, Some(Arc::new(diff_base)), None, None, cx)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.inner.hunks.is_empty()
|
self.inner.hunks.is_empty()
|
||||||
}
|
}
|
||||||
|
@ -541,6 +631,28 @@ impl BufferDiffInner {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_state(
|
||||||
|
&mut self,
|
||||||
|
new_state: Self,
|
||||||
|
buffer: &text::BufferSnapshot,
|
||||||
|
) -> Option<Range<Anchor>> {
|
||||||
|
let (base_text_changed, changed_range) =
|
||||||
|
match (self.base_text_exists, new_state.base_text_exists) {
|
||||||
|
(false, false) => (true, None),
|
||||||
|
(true, true) if self.base_text.remote_id() == new_state.base_text.remote_id() => {
|
||||||
|
(false, new_state.compare(&self, buffer))
|
||||||
|
}
|
||||||
|
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let pending_hunks = mem::replace(&mut self.pending_hunks, SumTree::new(buffer));
|
||||||
|
*self = new_state;
|
||||||
|
if !base_text_changed {
|
||||||
|
self.pending_hunks = pending_hunks;
|
||||||
|
}
|
||||||
|
changed_range
|
||||||
|
}
|
||||||
|
|
||||||
fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option<Range<Anchor>> {
|
fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option<Range<Anchor>> {
|
||||||
let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
|
let mut new_cursor = self.hunks.cursor::<()>(new_snapshot);
|
||||||
let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
|
let mut old_cursor = old.hunks.cursor::<()>(new_snapshot);
|
||||||
|
@ -762,84 +874,34 @@ pub enum BufferDiffEvent {
|
||||||
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
|
impl EventEmitter<BufferDiffEvent> for BufferDiff {}
|
||||||
|
|
||||||
impl BufferDiff {
|
impl BufferDiff {
|
||||||
#[cfg(test)]
|
pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
|
||||||
fn build_sync(
|
BufferDiff {
|
||||||
buffer: text::BufferSnapshot,
|
buffer_id: buffer.remote_id(),
|
||||||
diff_base: String,
|
inner: BufferDiffSnapshot::empty(buffer, cx).inner,
|
||||||
cx: &mut gpui::TestAppContext,
|
secondary_diff: None,
|
||||||
) -> BufferDiffInner {
|
}
|
||||||
let snapshot =
|
|
||||||
cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx));
|
|
||||||
cx.executor().block(snapshot)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
buffer: text::BufferSnapshot,
|
pub fn new_with_base_text(
|
||||||
base_text: Option<Arc<String>>,
|
base_text: &str,
|
||||||
language: Option<Arc<Language>>,
|
buffer: &Entity<language::Buffer>,
|
||||||
language_registry: Option<Arc<LanguageRegistry>>,
|
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> impl Future<Output = BufferDiffInner> + use<> {
|
) -> Self {
|
||||||
let base_text_pair;
|
let mut base_text = base_text.to_owned();
|
||||||
let base_text_exists;
|
text::LineEnding::normalize(&mut base_text);
|
||||||
let base_text_snapshot;
|
let snapshot = BufferDiffSnapshot::new_with_base_text(
|
||||||
if let Some(text) = &base_text {
|
buffer.read(cx).text_snapshot(),
|
||||||
let base_text_rope = Rope::from(text.as_str());
|
Some(base_text.into()),
|
||||||
base_text_pair = Some((text.clone(), base_text_rope.clone()));
|
None,
|
||||||
let snapshot = language::Buffer::build_snapshot(
|
None,
|
||||||
base_text_rope,
|
|
||||||
language.clone(),
|
|
||||||
language_registry.clone(),
|
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
base_text_snapshot = cx.background_spawn(snapshot);
|
let snapshot = cx.background_executor().block(snapshot);
|
||||||
base_text_exists = true;
|
Self {
|
||||||
} else {
|
buffer_id: buffer.read(cx).remote_id(),
|
||||||
base_text_pair = None;
|
inner: snapshot.inner,
|
||||||
base_text_snapshot = Task::ready(language::Buffer::build_empty_snapshot(cx));
|
secondary_diff: None,
|
||||||
base_text_exists = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let hunks = cx.background_spawn({
|
|
||||||
let buffer = buffer.clone();
|
|
||||||
async move { compute_hunks(base_text_pair, buffer) }
|
|
||||||
});
|
|
||||||
|
|
||||||
async move {
|
|
||||||
let (base_text, hunks) = futures::join!(base_text_snapshot, hunks);
|
|
||||||
BufferDiffInner {
|
|
||||||
base_text,
|
|
||||||
hunks,
|
|
||||||
base_text_exists,
|
|
||||||
pending_hunks: SumTree::new(&buffer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_with_base_buffer(
|
|
||||||
buffer: text::BufferSnapshot,
|
|
||||||
base_text: Option<Arc<String>>,
|
|
||||||
base_text_snapshot: language::BufferSnapshot,
|
|
||||||
cx: &App,
|
|
||||||
) -> impl Future<Output = BufferDiffInner> + use<> {
|
|
||||||
let base_text_exists = base_text.is_some();
|
|
||||||
let base_text_pair = base_text.map(|text| (text, base_text_snapshot.as_rope().clone()));
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
BufferDiffInner {
|
|
||||||
base_text: base_text_snapshot,
|
|
||||||
pending_hunks: SumTree::new(&buffer),
|
|
||||||
hunks: compute_hunks(base_text_pair, buffer),
|
|
||||||
base_text_exists,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_empty(buffer: &text::BufferSnapshot, cx: &mut App) -> BufferDiffInner {
|
|
||||||
BufferDiffInner {
|
|
||||||
base_text: language::Buffer::build_empty_snapshot(cx),
|
|
||||||
hunks: SumTree::new(buffer),
|
|
||||||
pending_hunks: SumTree::new(buffer),
|
|
||||||
base_text_exists: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -917,9 +979,9 @@ impl BufferDiff {
|
||||||
language_registry: Option<Arc<LanguageRegistry>>,
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> anyhow::Result<BufferDiffSnapshot> {
|
) -> anyhow::Result<BufferDiffSnapshot> {
|
||||||
let inner = if base_text_changed || language_changed {
|
Ok(if base_text_changed || language_changed {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
Self::build(
|
BufferDiffSnapshot::new_with_base_text(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
base_text,
|
base_text,
|
||||||
language.clone(),
|
language.clone(),
|
||||||
|
@ -930,7 +992,7 @@ impl BufferDiff {
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
this.read_with(cx, |this, cx| {
|
this.read_with(cx, |this, cx| {
|
||||||
Self::build_with_base_buffer(
|
BufferDiffSnapshot::new_with_base_buffer(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
base_text,
|
base_text,
|
||||||
this.base_text().clone(),
|
this.base_text().clone(),
|
||||||
|
@ -938,25 +1000,21 @@ impl BufferDiff {
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.await
|
.await
|
||||||
};
|
|
||||||
Ok(BufferDiffSnapshot {
|
|
||||||
inner,
|
|
||||||
secondary_diff: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn language_changed(&mut self, cx: &mut Context<Self>) {
|
||||||
|
cx.emit(BufferDiffEvent::LanguageChanged);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_snapshot(
|
pub fn set_snapshot(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: &text::BufferSnapshot,
|
|
||||||
new_snapshot: BufferDiffSnapshot,
|
new_snapshot: BufferDiffSnapshot,
|
||||||
language_changed: bool,
|
buffer: &text::BufferSnapshot,
|
||||||
secondary_changed_range: Option<Range<Anchor>>,
|
secondary_changed_range: Option<Range<Anchor>>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Option<Range<Anchor>> {
|
) -> Option<Range<Anchor>> {
|
||||||
let changed_range = self.set_state(new_snapshot.inner, buffer);
|
let changed_range = self.inner.set_state(new_snapshot.inner, buffer);
|
||||||
if language_changed {
|
|
||||||
cx.emit(BufferDiffEvent::LanguageChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
let changed_range = match (secondary_changed_range, changed_range) {
|
let changed_range = match (secondary_changed_range, changed_range) {
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
|
@ -980,31 +1038,6 @@ impl BufferDiff {
|
||||||
changed_range
|
changed_range
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_state(
|
|
||||||
&mut self,
|
|
||||||
new_state: BufferDiffInner,
|
|
||||||
buffer: &text::BufferSnapshot,
|
|
||||||
) -> Option<Range<Anchor>> {
|
|
||||||
let (base_text_changed, changed_range) =
|
|
||||||
match (self.inner.base_text_exists, new_state.base_text_exists) {
|
|
||||||
(false, false) => (true, None),
|
|
||||||
(true, true)
|
|
||||||
if self.inner.base_text.remote_id() == new_state.base_text.remote_id() =>
|
|
||||||
{
|
|
||||||
(false, new_state.compare(&self.inner, buffer))
|
|
||||||
}
|
|
||||||
_ => (true, Some(text::Anchor::MIN..text::Anchor::MAX)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let pending_hunks = mem::replace(&mut self.inner.pending_hunks, SumTree::new(buffer));
|
|
||||||
|
|
||||||
self.inner = new_state;
|
|
||||||
if !base_text_changed {
|
|
||||||
self.inner.pending_hunks = pending_hunks;
|
|
||||||
}
|
|
||||||
changed_range
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base_text(&self) -> &language::BufferSnapshot {
|
pub fn base_text(&self) -> &language::BufferSnapshot {
|
||||||
&self.inner.base_text
|
&self.inner.base_text
|
||||||
}
|
}
|
||||||
|
@ -1065,21 +1098,31 @@ impl BufferDiff {
|
||||||
self.hunks_intersecting_range(start..end, buffer, cx)
|
self.hunks_intersecting_range(start..end, buffer, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used in cases where the change set isn't derived from git.
|
pub fn set_base_text_buffer(
|
||||||
pub fn set_base_text(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
base_buffer: Entity<language::Buffer>,
|
base_buffer: Entity<language::Buffer>,
|
||||||
buffer: text::BufferSnapshot,
|
buffer: text::BufferSnapshot,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> oneshot::Receiver<()> {
|
) -> oneshot::Receiver<()> {
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
let this = cx.weak_entity();
|
|
||||||
let base_buffer = base_buffer.read(cx);
|
let base_buffer = base_buffer.read(cx);
|
||||||
let language_registry = base_buffer.language_registry();
|
let language_registry = base_buffer.language_registry();
|
||||||
let base_buffer = base_buffer.snapshot();
|
let base_buffer = base_buffer.snapshot();
|
||||||
|
self.set_base_text(base_buffer, language_registry, buffer, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used in cases where the change set isn't derived from git.
|
||||||
|
pub fn set_base_text(
|
||||||
|
&mut self,
|
||||||
|
base_buffer: language::BufferSnapshot,
|
||||||
|
language_registry: Option<Arc<LanguageRegistry>>,
|
||||||
|
buffer: text::BufferSnapshot,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> oneshot::Receiver<()> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let this = cx.weak_entity();
|
||||||
let base_text = Arc::new(base_buffer.text());
|
let base_text = Arc::new(base_buffer.text());
|
||||||
|
|
||||||
let snapshot = BufferDiff::build(
|
let snapshot = BufferDiffSnapshot::new_with_base_text(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
Some(base_text),
|
Some(base_text),
|
||||||
base_buffer.language().cloned(),
|
base_buffer.language().cloned(),
|
||||||
|
@ -1094,8 +1137,8 @@ impl BufferDiff {
|
||||||
let Some(this) = this.upgrade() else {
|
let Some(this) = this.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
this.update(cx, |this, _| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_state(snapshot, &buffer);
|
this.set_snapshot(snapshot, &buffer, None, cx);
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
drop(complete_on_drop)
|
drop(complete_on_drop)
|
||||||
|
@ -1110,49 +1153,17 @@ impl BufferDiff {
|
||||||
.then(|| self.inner.base_text.text())
|
.then(|| self.inner.base_text.text())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
|
|
||||||
BufferDiff {
|
|
||||||
buffer_id: buffer.remote_id(),
|
|
||||||
inner: BufferDiff::build_empty(buffer, cx),
|
|
||||||
secondary_diff: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn new_with_base_text(
|
|
||||||
base_text: &str,
|
|
||||||
buffer: &Entity<language::Buffer>,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Self {
|
|
||||||
let mut base_text = base_text.to_owned();
|
|
||||||
text::LineEnding::normalize(&mut base_text);
|
|
||||||
let snapshot = BufferDiff::build(
|
|
||||||
buffer.read(cx).text_snapshot(),
|
|
||||||
Some(base_text.into()),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
let snapshot = cx.background_executor().block(snapshot);
|
|
||||||
BufferDiff {
|
|
||||||
buffer_id: buffer.read(cx).remote_id(),
|
|
||||||
inner: snapshot,
|
|
||||||
secondary_diff: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
|
pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context<Self>) {
|
||||||
let base_text = self.base_text_string().map(Arc::new);
|
let base_text = self.base_text_string().map(Arc::new);
|
||||||
let snapshot = BufferDiff::build_with_base_buffer(
|
let snapshot = BufferDiffSnapshot::new_with_base_buffer(
|
||||||
buffer.clone(),
|
buffer.clone(),
|
||||||
base_text,
|
base_text,
|
||||||
self.inner.base_text.clone(),
|
self.inner.base_text.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
let snapshot = cx.background_executor().block(snapshot);
|
let snapshot = cx.background_executor().block(snapshot);
|
||||||
let changed_range = self.set_state(snapshot, &buffer);
|
self.set_snapshot(snapshot, &buffer, None, cx);
|
||||||
cx.emit(BufferDiffEvent::DiffChanged { changed_range });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1325,18 +1336,18 @@ mod tests {
|
||||||
.unindent();
|
.unindent();
|
||||||
|
|
||||||
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
||||||
let mut diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx);
|
let mut diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
|
||||||
assert_hunks(
|
assert_hunks(
|
||||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
|
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
|
||||||
&buffer,
|
&buffer,
|
||||||
&diff_base,
|
&diff_base,
|
||||||
&[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
|
&[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
|
||||||
);
|
);
|
||||||
|
|
||||||
buffer.edit([(0..0, "point five\n")]);
|
buffer.edit([(0..0, "point five\n")]);
|
||||||
diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx);
|
diff = BufferDiffSnapshot::new_sync(buffer.clone(), diff_base.clone(), cx);
|
||||||
assert_hunks(
|
assert_hunks(
|
||||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
|
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
|
||||||
&buffer,
|
&buffer,
|
||||||
&diff_base,
|
&diff_base,
|
||||||
&[
|
&[
|
||||||
|
@ -1345,9 +1356,9 @@ mod tests {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
|
diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
|
||||||
assert_hunks::<&str, _>(
|
assert_hunks::<&str, _>(
|
||||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
|
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
|
||||||
&buffer,
|
&buffer,
|
||||||
&diff_base,
|
&diff_base,
|
||||||
&[],
|
&[],
|
||||||
|
@ -1399,9 +1410,10 @@ mod tests {
|
||||||
.unindent();
|
.unindent();
|
||||||
|
|
||||||
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
||||||
let unstaged_diff = BufferDiff::build_sync(buffer.clone(), index_text.clone(), cx);
|
let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text.clone(), cx);
|
||||||
|
let mut uncommitted_diff =
|
||||||
let uncommitted_diff = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
|
BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
|
||||||
|
uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
|
||||||
|
|
||||||
let expected_hunks = vec![
|
let expected_hunks = vec![
|
||||||
(2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
|
(2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
|
||||||
|
@ -1420,11 +1432,7 @@ mod tests {
|
||||||
];
|
];
|
||||||
|
|
||||||
assert_hunks(
|
assert_hunks(
|
||||||
uncommitted_diff.hunks_intersecting_range(
|
uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
|
||||||
Anchor::MIN..Anchor::MAX,
|
|
||||||
&buffer,
|
|
||||||
Some(&unstaged_diff),
|
|
||||||
),
|
|
||||||
&buffer,
|
&buffer,
|
||||||
&head_text,
|
&head_text,
|
||||||
&expected_hunks,
|
&expected_hunks,
|
||||||
|
@ -1473,11 +1481,17 @@ mod tests {
|
||||||
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
|
||||||
let diff = cx
|
let diff = cx
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
BufferDiff::build(buffer.snapshot(), Some(diff_base.clone()), None, None, cx)
|
BufferDiffSnapshot::new_with_base_text(
|
||||||
|
buffer.snapshot(),
|
||||||
|
Some(diff_base.clone()),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None)
|
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer)
|
||||||
.count(),
|
.count(),
|
||||||
8
|
8
|
||||||
);
|
);
|
||||||
|
@ -1486,7 +1500,6 @@ mod tests {
|
||||||
diff.hunks_intersecting_range(
|
diff.hunks_intersecting_range(
|
||||||
buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
|
buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
|
||||||
&buffer,
|
&buffer,
|
||||||
None,
|
|
||||||
),
|
),
|
||||||
&buffer,
|
&buffer,
|
||||||
&diff_base,
|
&diff_base,
|
||||||
|
@ -1732,18 +1745,20 @@ mod tests {
|
||||||
let hunk_range =
|
let hunk_range =
|
||||||
buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
|
buffer.anchor_before(ranges[0].start)..buffer.anchor_before(ranges[0].end);
|
||||||
|
|
||||||
let unstaged = BufferDiff::build_sync(buffer.clone(), example.index_text.clone(), cx);
|
let unstaged =
|
||||||
let uncommitted = BufferDiff::build_sync(buffer.clone(), example.head_text.clone(), cx);
|
BufferDiffSnapshot::new_sync(buffer.clone(), example.index_text.clone(), cx);
|
||||||
|
let uncommitted =
|
||||||
|
BufferDiffSnapshot::new_sync(buffer.clone(), example.head_text.clone(), cx);
|
||||||
|
|
||||||
let unstaged_diff = cx.new(|cx| {
|
let unstaged_diff = cx.new(|cx| {
|
||||||
let mut diff = BufferDiff::new(&buffer, cx);
|
let mut diff = BufferDiff::new(&buffer, cx);
|
||||||
diff.set_state(unstaged, &buffer);
|
diff.set_snapshot(unstaged, &buffer, None, cx);
|
||||||
diff
|
diff
|
||||||
});
|
});
|
||||||
|
|
||||||
let uncommitted_diff = cx.new(|cx| {
|
let uncommitted_diff = cx.new(|cx| {
|
||||||
let mut diff = BufferDiff::new(&buffer, cx);
|
let mut diff = BufferDiff::new(&buffer, cx);
|
||||||
diff.set_state(uncommitted, &buffer);
|
diff.set_snapshot(uncommitted, &buffer, None, cx);
|
||||||
diff.set_secondary_diff(unstaged_diff);
|
diff.set_secondary_diff(unstaged_diff);
|
||||||
diff
|
diff
|
||||||
});
|
});
|
||||||
|
@ -1800,16 +1815,16 @@ mod tests {
|
||||||
.unindent();
|
.unindent();
|
||||||
|
|
||||||
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone());
|
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone());
|
||||||
let unstaged = BufferDiff::build_sync(buffer.clone(), index_text, cx);
|
let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
|
||||||
let uncommitted = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
|
let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
|
||||||
let unstaged_diff = cx.new(|cx| {
|
let unstaged_diff = cx.new(|cx| {
|
||||||
let mut diff = BufferDiff::new(&buffer, cx);
|
let mut diff = BufferDiff::new(&buffer, cx);
|
||||||
diff.set_state(unstaged, &buffer);
|
diff.set_snapshot(unstaged, &buffer, None, cx);
|
||||||
diff
|
diff
|
||||||
});
|
});
|
||||||
let uncommitted_diff = cx.new(|cx| {
|
let uncommitted_diff = cx.new(|cx| {
|
||||||
let mut diff = BufferDiff::new(&buffer, cx);
|
let mut diff = BufferDiff::new(&buffer, cx);
|
||||||
diff.set_state(uncommitted, &buffer);
|
diff.set_snapshot(uncommitted, &buffer, None, cx);
|
||||||
diff.set_secondary_diff(unstaged_diff.clone());
|
diff.set_secondary_diff(unstaged_diff.clone());
|
||||||
diff
|
diff
|
||||||
});
|
});
|
||||||
|
@ -1874,9 +1889,9 @@ mod tests {
|
||||||
|
|
||||||
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
|
let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1);
|
||||||
|
|
||||||
let empty_diff = cx.update(|cx| BufferDiff::build_empty(&buffer, cx));
|
let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
|
||||||
let diff_1 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
|
let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
|
||||||
let range = diff_1.compare(&empty_diff, &buffer).unwrap();
|
let range = diff_1.inner.compare(&empty_diff.inner, &buffer).unwrap();
|
||||||
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
|
assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0));
|
||||||
|
|
||||||
// Edit does not affect the diff.
|
// Edit does not affect the diff.
|
||||||
|
@ -1893,8 +1908,8 @@ mod tests {
|
||||||
"
|
"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
);
|
);
|
||||||
let diff_2 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
|
let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
|
||||||
assert_eq!(None, diff_2.compare(&diff_1, &buffer));
|
assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
|
||||||
|
|
||||||
// Edit turns a deletion hunk into a modification.
|
// Edit turns a deletion hunk into a modification.
|
||||||
buffer.edit_via_marked_text(
|
buffer.edit_via_marked_text(
|
||||||
|
@ -1910,8 +1925,8 @@ mod tests {
|
||||||
"
|
"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
);
|
);
|
||||||
let diff_3 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
|
let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
|
||||||
let range = diff_3.compare(&diff_2, &buffer).unwrap();
|
let range = diff_3.inner.compare(&diff_2.inner, &buffer).unwrap();
|
||||||
assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
|
assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0));
|
||||||
|
|
||||||
// Edit turns a modification hunk into a deletion.
|
// Edit turns a modification hunk into a deletion.
|
||||||
|
@ -1927,8 +1942,8 @@ mod tests {
|
||||||
"
|
"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
);
|
);
|
||||||
let diff_4 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
|
let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
|
||||||
let range = diff_4.compare(&diff_3, &buffer).unwrap();
|
let range = diff_4.inner.compare(&diff_3.inner, &buffer).unwrap();
|
||||||
assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
|
assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0));
|
||||||
|
|
||||||
// Edit introduces a new insertion hunk.
|
// Edit introduces a new insertion hunk.
|
||||||
|
@ -1945,8 +1960,8 @@ mod tests {
|
||||||
"
|
"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
);
|
);
|
||||||
let diff_5 = BufferDiff::build_sync(buffer.snapshot(), base_text.clone(), cx);
|
let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
|
||||||
let range = diff_5.compare(&diff_4, &buffer).unwrap();
|
let range = diff_5.inner.compare(&diff_4.inner, &buffer).unwrap();
|
||||||
assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
|
assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0));
|
||||||
|
|
||||||
// Edit removes a hunk.
|
// Edit removes a hunk.
|
||||||
|
@ -1963,8 +1978,8 @@ mod tests {
|
||||||
"
|
"
|
||||||
.unindent(),
|
.unindent(),
|
||||||
);
|
);
|
||||||
let diff_6 = BufferDiff::build_sync(buffer.snapshot(), base_text, cx);
|
let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
|
||||||
let range = diff_6.compare(&diff_5, &buffer).unwrap();
|
let range = diff_6.inner.compare(&diff_5.inner, &buffer).unwrap();
|
||||||
assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
|
assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2038,14 +2053,16 @@ mod tests {
|
||||||
head_text: String,
|
head_text: String,
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
) -> Entity<BufferDiff> {
|
) -> Entity<BufferDiff> {
|
||||||
let inner = BufferDiff::build_sync(working_copy.text.clone(), head_text, cx);
|
let inner =
|
||||||
|
BufferDiffSnapshot::new_sync(working_copy.text.clone(), head_text, cx).inner;
|
||||||
let secondary = BufferDiff {
|
let secondary = BufferDiff {
|
||||||
buffer_id: working_copy.remote_id(),
|
buffer_id: working_copy.remote_id(),
|
||||||
inner: BufferDiff::build_sync(
|
inner: BufferDiffSnapshot::new_sync(
|
||||||
working_copy.text.clone(),
|
working_copy.text.clone(),
|
||||||
index_text.to_string(),
|
index_text.to_string(),
|
||||||
cx,
|
cx,
|
||||||
),
|
)
|
||||||
|
.inner,
|
||||||
secondary_diff: None,
|
secondary_diff: None,
|
||||||
};
|
};
|
||||||
let secondary = cx.new(|_| secondary);
|
let secondary = cx.new(|_| secondary);
|
||||||
|
|
|
@ -413,6 +413,7 @@ impl Server {
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
|
.add_request_handler(forward_mutating_project_request::<proto::GitInit>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
|
.add_request_handler(forward_read_only_project_request::<proto::GetRemotes>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
|
.add_request_handler(forward_read_only_project_request::<proto::GitShow>)
|
||||||
|
.add_request_handler(forward_read_only_project_request::<proto::LoadCommitDiff>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
|
.add_request_handler(forward_read_only_project_request::<proto::GitReset>)
|
||||||
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
|
.add_request_handler(forward_read_only_project_request::<proto::GitCheckoutFiles>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
|
.add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
|
||||||
|
|
|
@ -32,7 +32,6 @@ test-support = [
|
||||||
aho-corasick.workspace = true
|
aho-corasick.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
assets.workspace = true
|
assets.workspace = true
|
||||||
chrono.workspace = true
|
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
|
@ -47,7 +46,6 @@ fuzzy.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
http_client.workspace = true
|
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
inline_completion.workspace = true
|
inline_completion.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
@ -76,7 +74,6 @@ task.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
time_format.workspace = true
|
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
tree-sitter-html = { workspace = true, optional = true }
|
tree-sitter-html = { workspace = true, optional = true }
|
||||||
tree-sitter-rust = { workspace = true, optional = true }
|
tree-sitter-rust = { workspace = true, optional = true }
|
||||||
|
|
|
@ -419,6 +419,7 @@ actions!(
|
||||||
EditLogBreakpoint,
|
EditLogBreakpoint,
|
||||||
ToggleAutoSignatureHelp,
|
ToggleAutoSignatureHelp,
|
||||||
ToggleGitBlameInline,
|
ToggleGitBlameInline,
|
||||||
|
OpenGitBlameCommit,
|
||||||
ToggleIndentGuides,
|
ToggleIndentGuides,
|
||||||
ToggleInlayHints,
|
ToggleInlayHints,
|
||||||
ToggleInlineDiagnostics,
|
ToggleInlineDiagnostics,
|
||||||
|
|
|
@ -321,6 +321,20 @@ impl DisplayMap {
|
||||||
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
|
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
|
||||||
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
|
let edits = self.buffer_subscription.consume().into_inner();
|
||||||
|
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||||
|
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||||
|
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||||
|
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||||
|
let (snapshot, edits) = self
|
||||||
|
.wrap_map
|
||||||
|
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||||
|
let mut block_map = self.block_map.write(snapshot, edits);
|
||||||
|
block_map.disable_header_for_buffer(buffer_id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fold_buffers(
|
pub fn fold_buffers(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer_ids: impl IntoIterator<Item = language::BufferId>,
|
buffer_ids: impl IntoIterator<Item = language::BufferId>,
|
||||||
|
|
|
@ -40,6 +40,7 @@ pub struct BlockMap {
|
||||||
buffer_header_height: u32,
|
buffer_header_height: u32,
|
||||||
excerpt_header_height: u32,
|
excerpt_header_height: u32,
|
||||||
pub(super) folded_buffers: HashSet<BufferId>,
|
pub(super) folded_buffers: HashSet<BufferId>,
|
||||||
|
buffers_with_disabled_headers: HashSet<BufferId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BlockMapReader<'a> {
|
pub struct BlockMapReader<'a> {
|
||||||
|
@ -422,6 +423,7 @@ impl BlockMap {
|
||||||
custom_blocks: Vec::new(),
|
custom_blocks: Vec::new(),
|
||||||
custom_blocks_by_id: TreeMap::default(),
|
custom_blocks_by_id: TreeMap::default(),
|
||||||
folded_buffers: HashSet::default(),
|
folded_buffers: HashSet::default(),
|
||||||
|
buffers_with_disabled_headers: HashSet::default(),
|
||||||
transforms: RefCell::new(transforms),
|
transforms: RefCell::new(transforms),
|
||||||
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
|
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
|
||||||
buffer_header_height,
|
buffer_header_height,
|
||||||
|
@ -642,11 +644,8 @@ impl BlockMap {
|
||||||
);
|
);
|
||||||
|
|
||||||
if buffer.show_headers() {
|
if buffer.show_headers() {
|
||||||
blocks_in_edit.extend(BlockMap::header_and_footer_blocks(
|
blocks_in_edit.extend(self.header_and_footer_blocks(
|
||||||
self.buffer_header_height,
|
|
||||||
self.excerpt_header_height,
|
|
||||||
buffer,
|
buffer,
|
||||||
&self.folded_buffers,
|
|
||||||
(start_bound, end_bound),
|
(start_bound, end_bound),
|
||||||
wrap_snapshot,
|
wrap_snapshot,
|
||||||
));
|
));
|
||||||
|
@ -714,10 +713,8 @@ impl BlockMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn header_and_footer_blocks<'a, R, T>(
|
fn header_and_footer_blocks<'a, R, T>(
|
||||||
buffer_header_height: u32,
|
&'a self,
|
||||||
excerpt_header_height: u32,
|
|
||||||
buffer: &'a multi_buffer::MultiBufferSnapshot,
|
buffer: &'a multi_buffer::MultiBufferSnapshot,
|
||||||
folded_buffers: &'a HashSet<BufferId>,
|
|
||||||
range: R,
|
range: R,
|
||||||
wrap_snapshot: &'a WrapSnapshot,
|
wrap_snapshot: &'a WrapSnapshot,
|
||||||
) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'a
|
) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'a
|
||||||
|
@ -728,6 +725,7 @@ impl BlockMap {
|
||||||
let mut boundaries = buffer.excerpt_boundaries_in_range(range).peekable();
|
let mut boundaries = buffer.excerpt_boundaries_in_range(range).peekable();
|
||||||
|
|
||||||
std::iter::from_fn(move || {
|
std::iter::from_fn(move || {
|
||||||
|
loop {
|
||||||
let excerpt_boundary = boundaries.next()?;
|
let excerpt_boundary = boundaries.next()?;
|
||||||
let wrap_row = wrap_snapshot
|
let wrap_row = wrap_snapshot
|
||||||
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
|
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
|
||||||
|
@ -748,7 +746,10 @@ impl BlockMap {
|
||||||
|
|
||||||
if let Some(new_buffer_id) = new_buffer_id {
|
if let Some(new_buffer_id) = new_buffer_id {
|
||||||
let first_excerpt = excerpt_boundary.next.clone();
|
let first_excerpt = excerpt_boundary.next.clone();
|
||||||
if folded_buffers.contains(&new_buffer_id) {
|
if self.buffers_with_disabled_headers.contains(&new_buffer_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.folded_buffers.contains(&new_buffer_id) {
|
||||||
let mut last_excerpt_end_row = first_excerpt.end_row;
|
let mut last_excerpt_end_row = first_excerpt.end_row;
|
||||||
|
|
||||||
while let Some(next_boundary) = boundaries.peek() {
|
while let Some(next_boundary) = boundaries.peek() {
|
||||||
|
@ -774,7 +775,7 @@ impl BlockMap {
|
||||||
return Some((
|
return Some((
|
||||||
BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
|
BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
|
||||||
Block::FoldedBuffer {
|
Block::FoldedBuffer {
|
||||||
height: height + buffer_header_height,
|
height: height + self.buffer_header_height,
|
||||||
first_excerpt,
|
first_excerpt,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
@ -782,19 +783,20 @@ impl BlockMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_buffer_id.is_some() {
|
if new_buffer_id.is_some() {
|
||||||
height += buffer_header_height;
|
height += self.buffer_header_height;
|
||||||
} else {
|
} else {
|
||||||
height += excerpt_header_height;
|
height += self.excerpt_header_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some((
|
return Some((
|
||||||
BlockPlacement::Above(WrapRow(wrap_row)),
|
BlockPlacement::Above(WrapRow(wrap_row)),
|
||||||
Block::ExcerptBoundary {
|
Block::ExcerptBoundary {
|
||||||
excerpt: excerpt_boundary.next,
|
excerpt: excerpt_boundary.next,
|
||||||
height,
|
height,
|
||||||
starts_new_buffer: new_buffer_id.is_some(),
|
starts_new_buffer: new_buffer_id.is_some(),
|
||||||
},
|
},
|
||||||
))
|
));
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1168,6 +1170,10 @@ impl BlockMapWriter<'_> {
|
||||||
self.remove(blocks_to_remove);
|
self.remove(blocks_to_remove);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId) {
|
||||||
|
self.0.buffers_with_disabled_headers.insert(buffer_id);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fold_buffers(
|
pub fn fold_buffers(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer_ids: impl IntoIterator<Item = BufferId>,
|
buffer_ids: impl IntoIterator<Item = BufferId>,
|
||||||
|
@ -3159,11 +3165,8 @@ mod tests {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Note that this needs to be synced with the related section in BlockMap::sync
|
// Note that this needs to be synced with the related section in BlockMap::sync
|
||||||
expected_blocks.extend(BlockMap::header_and_footer_blocks(
|
expected_blocks.extend(block_map.header_and_footer_blocks(
|
||||||
buffer_start_header_height,
|
|
||||||
excerpt_header_height,
|
|
||||||
&buffer_snapshot,
|
&buffer_snapshot,
|
||||||
&block_map.folded_buffers,
|
|
||||||
0..,
|
0..,
|
||||||
&wraps_snapshot,
|
&wraps_snapshot,
|
||||||
));
|
));
|
||||||
|
|
|
@ -16,7 +16,6 @@ pub mod actions;
|
||||||
mod blink_manager;
|
mod blink_manager;
|
||||||
mod clangd_ext;
|
mod clangd_ext;
|
||||||
mod code_context_menus;
|
mod code_context_menus;
|
||||||
pub mod commit_tooltip;
|
|
||||||
pub mod display_map;
|
pub mod display_map;
|
||||||
mod editor_settings;
|
mod editor_settings;
|
||||||
mod editor_settings_controls;
|
mod editor_settings_controls;
|
||||||
|
@ -82,19 +81,21 @@ use code_context_menus::{
|
||||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||||
CompletionsMenu, ContextMenuOrigin,
|
CompletionsMenu, ContextMenuOrigin,
|
||||||
};
|
};
|
||||||
use git::blame::GitBlame;
|
use git::blame::{GitBlame, GlobalBlameRenderer};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext,
|
Action, Animation, AnimationExt, AnyElement, AnyWeakEntity, App, AppContext,
|
||||||
AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context,
|
AsyncWindowContext, AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry,
|
||||||
DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent,
|
ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter,
|
||||||
Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers,
|
FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
|
||||||
MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size,
|
KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render,
|
||||||
Stateful, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement,
|
SharedString, Size, Stateful, Styled, StyledText, Subscription, Task, TextStyle,
|
||||||
UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
|
TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
|
||||||
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size,
|
WeakFocusHandle, Window, div, impl_actions, point, prelude::*, pulsating_between, px, relative,
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
|
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
|
||||||
|
pub use hover_popover::hover_markdown_style;
|
||||||
use hover_popover::{HoverState, hide_hover};
|
use hover_popover::{HoverState, hide_hover};
|
||||||
use indent_guides::ActiveIndentGuidesState;
|
use indent_guides::ActiveIndentGuidesState;
|
||||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||||
|
@ -124,6 +125,7 @@ use project::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use git::blame::BlameRenderer;
|
||||||
pub use proposed_changes_editor::{
|
pub use proposed_changes_editor::{
|
||||||
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||||
};
|
};
|
||||||
|
@ -187,8 +189,8 @@ use theme::{
|
||||||
observe_buffer_font_size_adjustment,
|
observe_buffer_font_size_adjustment,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
ButtonSize, ButtonStyle, Disclosure, IconButton, IconButtonShape, IconName, IconSize, Key,
|
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
|
||||||
Tooltip, h_flex, prelude::*,
|
IconSize, Key, Tooltip, h_flex, prelude::*,
|
||||||
};
|
};
|
||||||
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
@ -302,6 +304,8 @@ pub fn init_settings(cx: &mut App) {
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
init_settings(cx);
|
init_settings(cx);
|
||||||
|
|
||||||
|
cx.set_global(GlobalBlameRenderer(Arc::new(())));
|
||||||
|
|
||||||
workspace::register_project_item::<Editor>(cx);
|
workspace::register_project_item::<Editor>(cx);
|
||||||
workspace::FollowableViewRegistry::register::<Editor>(cx);
|
workspace::FollowableViewRegistry::register::<Editor>(cx);
|
||||||
workspace::register_serializable_item::<Editor>(cx);
|
workspace::register_serializable_item::<Editor>(cx);
|
||||||
|
@ -347,6 +351,10 @@ pub fn init(cx: &mut App) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SearchWithinRange;
|
pub struct SearchWithinRange;
|
||||||
|
|
||||||
trait InvalidationRegion {
|
trait InvalidationRegion {
|
||||||
|
@ -766,7 +774,7 @@ pub struct Editor {
|
||||||
show_git_blame_gutter: bool,
|
show_git_blame_gutter: bool,
|
||||||
show_git_blame_inline: bool,
|
show_git_blame_inline: bool,
|
||||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||||
git_blame_inline_tooltip: Option<WeakEntity<crate::commit_tooltip::CommitTooltip>>,
|
pub git_blame_inline_tooltip: Option<AnyWeakEntity>,
|
||||||
git_blame_inline_enabled: bool,
|
git_blame_inline_enabled: bool,
|
||||||
render_diff_hunk_controls: RenderDiffHunkControlsFn,
|
render_diff_hunk_controls: RenderDiffHunkControlsFn,
|
||||||
serialize_dirty_buffers: bool,
|
serialize_dirty_buffers: bool,
|
||||||
|
@ -848,8 +856,6 @@ pub struct EditorSnapshot {
|
||||||
gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Copy)]
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
pub struct GutterDimensions {
|
pub struct GutterDimensions {
|
||||||
pub left_padding: Pixels,
|
pub left_padding: Pixels,
|
||||||
|
@ -1643,6 +1649,21 @@ impl Editor {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn deploy_mouse_context_menu(
|
||||||
|
&mut self,
|
||||||
|
position: gpui::Point<Pixels>,
|
||||||
|
context_menu: Entity<ContextMenu>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.mouse_context_menu = Some(MouseContextMenu::new(
|
||||||
|
crate::mouse_context_menu::MenuPosition::PinnedToScreen(position),
|
||||||
|
context_menu,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
pub fn mouse_menu_is_focused(&self, window: &Window, cx: &App) -> bool {
|
pub fn mouse_menu_is_focused(&self, window: &Window, cx: &App) -> bool {
|
||||||
self.mouse_context_menu
|
self.mouse_context_menu
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -14922,6 +14943,13 @@ impl Editor {
|
||||||
self.display_map.read(cx).folded_buffers()
|
self.display_map.read(cx).folded_buffers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
|
||||||
|
self.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map.disable_header_for_buffer(buffer_id, cx);
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes any folds with the given ranges.
|
/// Removes any folds with the given ranges.
|
||||||
pub fn remove_folds_with_type<T: ToOffset + Clone>(
|
pub fn remove_folds_with_type<T: ToOffset + Clone>(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
@ -15861,6 +15889,45 @@ impl Editor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_git_blame_commit(
|
||||||
|
&mut self,
|
||||||
|
_: &OpenGitBlameCommit,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.open_git_blame_commit_internal(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_git_blame_commit_internal(
|
||||||
|
&mut self,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Option<()> {
|
||||||
|
let blame = self.blame.as_ref()?;
|
||||||
|
let snapshot = self.snapshot(window, cx);
|
||||||
|
let cursor = self.selections.newest::<Point>(cx).head();
|
||||||
|
let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?;
|
||||||
|
let blame_entry = blame
|
||||||
|
.update(cx, |blame, cx| {
|
||||||
|
blame
|
||||||
|
.blame_for_rows(
|
||||||
|
&[RowInfo {
|
||||||
|
buffer_id: Some(buffer.remote_id()),
|
||||||
|
buffer_row: Some(point.row),
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.next()
|
||||||
|
})
|
||||||
|
.flatten()?;
|
||||||
|
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||||||
|
let repo = blame.read(cx).repository(cx)?;
|
||||||
|
let workspace = self.workspace()?.downgrade();
|
||||||
|
renderer.open_blame_commit(blame_entry, repo, workspace, window, cx);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn git_blame_inline_enabled(&self) -> bool {
|
pub fn git_blame_inline_enabled(&self) -> bool {
|
||||||
self.git_blame_inline_enabled
|
self.git_blame_inline_enabled
|
||||||
}
|
}
|
||||||
|
@ -17794,8 +17861,10 @@ fn get_uncommitted_diff_for_buffer(
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
for buffer in buffers {
|
for buffer in buffers {
|
||||||
|
if project::File::from_dyn(buffer.read(cx).file()).is_some() {
|
||||||
tasks.push(project.open_uncommitted_diff(buffer.clone(), cx))
|
tasks.push(project.open_uncommitted_diff(buffer.clone(), cx))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let diffs = future::join_all(tasks).await;
|
let diffs = future::join_all(tasks).await;
|
||||||
|
@ -18911,13 +18980,13 @@ impl EditorSnapshot {
|
||||||
let git_blame_entries_width =
|
let git_blame_entries_width =
|
||||||
self.git_blame_gutter_max_author_length
|
self.git_blame_gutter_max_author_length
|
||||||
.map(|max_author_length| {
|
.map(|max_author_length| {
|
||||||
|
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||||||
const MAX_RELATIVE_TIMESTAMP: &str = "60 minutes ago";
|
const MAX_RELATIVE_TIMESTAMP: &str = "60 minutes ago";
|
||||||
|
|
||||||
/// The number of characters to dedicate to gaps and margins.
|
/// The number of characters to dedicate to gaps and margins.
|
||||||
const SPACING_WIDTH: usize = 4;
|
const SPACING_WIDTH: usize = 4;
|
||||||
|
|
||||||
let max_char_count = max_author_length
|
let max_char_count = max_author_length.min(renderer.max_author_length())
|
||||||
.min(GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED)
|
|
||||||
+ ::git::SHORT_SHA_LENGTH
|
+ ::git::SHORT_SHA_LENGTH
|
||||||
+ MAX_RELATIVE_TIMESTAMP.len()
|
+ MAX_RELATIVE_TIMESTAMP.len()
|
||||||
+ SPACING_WIDTH;
|
+ SPACING_WIDTH;
|
||||||
|
|
|
@ -3,13 +3,12 @@ use crate::{
|
||||||
ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow,
|
ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow,
|
||||||
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
|
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
|
||||||
EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock,
|
EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock,
|
||||||
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
|
GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason,
|
||||||
HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
|
InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN,
|
||||||
LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
|
MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp,
|
||||||
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection,
|
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
|
||||||
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold,
|
StickyHeaderExcerpt, ToPoint, ToggleFold,
|
||||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||||
commit_tooltip::{CommitTooltip, ParsedCommitMessage, blame_entry_relative_timestamp},
|
|
||||||
display_map::{
|
display_map::{
|
||||||
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
|
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
|
||||||
},
|
},
|
||||||
|
@ -17,13 +16,13 @@ use crate::{
|
||||||
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
|
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
|
||||||
ScrollbarAxes, ScrollbarDiagnostics, ShowScrollbar,
|
ScrollbarAxes, ScrollbarDiagnostics, ShowScrollbar,
|
||||||
},
|
},
|
||||||
git::blame::GitBlame,
|
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
|
||||||
hover_popover::{
|
hover_popover::{
|
||||||
self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, hover_at,
|
self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, hover_at,
|
||||||
},
|
},
|
||||||
inlay_hint_settings,
|
inlay_hint_settings,
|
||||||
items::BufferSearchHighlights,
|
items::BufferSearchHighlights,
|
||||||
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
mouse_context_menu::{self, MenuPosition},
|
||||||
scroll::scroll_amount::ScrollAmount,
|
scroll::scroll_amount::ScrollAmount,
|
||||||
};
|
};
|
||||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||||
|
@ -34,12 +33,12 @@ use file_icons::FileIcons;
|
||||||
use git::{Oid, blame::BlameEntry, status::FileStatus};
|
use git::{Oid, blame::BlameEntry, status::FileStatus};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Along, AnyElement, App, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds,
|
Action, Along, AnyElement, App, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds,
|
||||||
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
|
ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element,
|
||||||
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla,
|
||||||
Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
|
InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
|
||||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||||
Subscription, TextRun, TextStyleRefinement, Window, anchored, deferred, div, fill,
|
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||||
transparent_black,
|
transparent_black,
|
||||||
};
|
};
|
||||||
|
@ -76,10 +75,10 @@ use std::{
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use text::BufferId;
|
use text::BufferId;
|
||||||
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
||||||
use ui::{ButtonLike, ContextMenu, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
|
use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use util::{RangeExt, ResultExt, debug_panic};
|
use util::{RangeExt, ResultExt, debug_panic};
|
||||||
use workspace::{item::Item, notifications::NotifyTaskExt};
|
use workspace::{Workspace, item::Item, notifications::NotifyTaskExt};
|
||||||
|
|
||||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
|
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
|
||||||
|
|
||||||
|
@ -426,6 +425,7 @@ impl EditorElement {
|
||||||
register_action(editor, window, Editor::copy_file_location);
|
register_action(editor, window, Editor::copy_file_location);
|
||||||
register_action(editor, window, Editor::toggle_git_blame);
|
register_action(editor, window, Editor::toggle_git_blame);
|
||||||
register_action(editor, window, Editor::toggle_git_blame_inline);
|
register_action(editor, window, Editor::toggle_git_blame_inline);
|
||||||
|
register_action(editor, window, Editor::open_git_blame_commit);
|
||||||
register_action(editor, window, Editor::toggle_selected_diff_hunks);
|
register_action(editor, window, Editor::toggle_selected_diff_hunks);
|
||||||
register_action(editor, window, Editor::toggle_staged_selected_diff_hunks);
|
register_action(editor, window, Editor::toggle_staged_selected_diff_hunks);
|
||||||
register_action(editor, window, Editor::stage_and_next);
|
register_action(editor, window, Editor::stage_and_next);
|
||||||
|
@ -1759,14 +1759,21 @@ impl EditorElement {
|
||||||
padding * em_width
|
padding * em_width
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let workspace = editor.workspace()?.downgrade();
|
||||||
let blame_entry = blame
|
let blame_entry = blame
|
||||||
.update(cx, |blame, cx| {
|
.update(cx, |blame, cx| {
|
||||||
blame.blame_for_rows(&[*row_info], cx).next()
|
blame.blame_for_rows(&[*row_info], cx).next()
|
||||||
})
|
})
|
||||||
.flatten()?;
|
.flatten()?;
|
||||||
|
|
||||||
let mut element =
|
let mut element = render_inline_blame_entry(
|
||||||
render_inline_blame_entry(self.editor.clone(), &blame, blame_entry, &self.style, cx);
|
self.editor.clone(),
|
||||||
|
workspace,
|
||||||
|
&blame,
|
||||||
|
blame_entry,
|
||||||
|
&self.style,
|
||||||
|
cx,
|
||||||
|
)?;
|
||||||
|
|
||||||
let start_y = content_origin.y
|
let start_y = content_origin.y
|
||||||
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
||||||
|
@ -1816,6 +1823,7 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let blame = self.editor.read(cx).blame.clone()?;
|
let blame = self.editor.read(cx).blame.clone()?;
|
||||||
|
let workspace = self.editor.read(cx).workspace()?;
|
||||||
let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
|
let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
|
||||||
blame.blame_for_rows(buffer_rows, cx).collect()
|
blame.blame_for_rows(buffer_rows, cx).collect()
|
||||||
});
|
});
|
||||||
|
@ -1829,21 +1837,23 @@ impl EditorElement {
|
||||||
let start_x = em_width;
|
let start_x = em_width;
|
||||||
|
|
||||||
let mut last_used_color: Option<(PlayerColor, Oid)> = None;
|
let mut last_used_color: Option<(PlayerColor, Oid)> = None;
|
||||||
|
let blame_renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||||||
|
|
||||||
let shaped_lines = blamed_rows
|
let shaped_lines = blamed_rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.flat_map(|(ix, blame_entry)| {
|
.flat_map(|(ix, blame_entry)| {
|
||||||
if let Some(blame_entry) = blame_entry {
|
|
||||||
let mut element = render_blame_entry(
|
let mut element = render_blame_entry(
|
||||||
ix,
|
ix,
|
||||||
&blame,
|
&blame,
|
||||||
blame_entry,
|
blame_entry?,
|
||||||
&self.style,
|
&self.style,
|
||||||
&mut last_used_color,
|
&mut last_used_color,
|
||||||
self.editor.clone(),
|
self.editor.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
blame_renderer.clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
)?;
|
||||||
|
|
||||||
let start_y = ix as f32 * line_height - (scroll_top % line_height);
|
let start_y = ix as f32 * line_height - (scroll_top % line_height);
|
||||||
let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
|
let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
|
||||||
|
@ -1856,9 +1866,6 @@ impl EditorElement {
|
||||||
);
|
);
|
||||||
|
|
||||||
Some(element)
|
Some(element)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -5725,61 +5732,43 @@ fn prepaint_gutter_button(
|
||||||
|
|
||||||
fn render_inline_blame_entry(
|
fn render_inline_blame_entry(
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
blame: &gpui::Entity<GitBlame>,
|
workspace: WeakEntity<Workspace>,
|
||||||
|
blame: &Entity<GitBlame>,
|
||||||
blame_entry: BlameEntry,
|
blame_entry: BlameEntry,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> AnyElement {
|
) -> Option<AnyElement> {
|
||||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||||||
|
let blame = blame.read(cx);
|
||||||
let author = blame_entry.author.as_deref().unwrap_or_default();
|
let details = blame.details_for_entry(&blame_entry);
|
||||||
let summary_enabled = ProjectSettings::get_global(cx)
|
let repository = blame.repository(cx)?.clone();
|
||||||
.git
|
renderer.render_inline_blame_entry(
|
||||||
.show_inline_commit_summary();
|
&style.text,
|
||||||
|
blame_entry,
|
||||||
let text = match blame_entry.summary.as_ref() {
|
details,
|
||||||
Some(summary) if summary_enabled => {
|
repository,
|
||||||
format!("{}, {} - {}", author, relative_timestamp, summary)
|
workspace,
|
||||||
}
|
editor,
|
||||||
_ => format!("{}, {}", author, relative_timestamp),
|
cx,
|
||||||
};
|
)
|
||||||
let blame = blame.clone();
|
|
||||||
let blame_entry = blame_entry.clone();
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id("inline-blame")
|
|
||||||
.w_full()
|
|
||||||
.font_family(style.text.font().family)
|
|
||||||
.text_color(cx.theme().status().hint)
|
|
||||||
.line_height(style.text.line_height)
|
|
||||||
.child(Icon::new(IconName::FileGit).color(Color::Hint))
|
|
||||||
.child(text)
|
|
||||||
.gap_2()
|
|
||||||
.hoverable_tooltip(move |window, cx| {
|
|
||||||
let details = blame.read(cx).details_for_entry(&blame_entry);
|
|
||||||
let tooltip =
|
|
||||||
cx.new(|cx| CommitTooltip::blame_entry(&blame_entry, details, window, cx));
|
|
||||||
editor.update(cx, |editor, _| {
|
|
||||||
editor.git_blame_inline_tooltip = Some(tooltip.downgrade())
|
|
||||||
});
|
|
||||||
tooltip.into()
|
|
||||||
})
|
|
||||||
.into_any()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_blame_entry(
|
fn render_blame_entry(
|
||||||
ix: usize,
|
ix: usize,
|
||||||
blame: &gpui::Entity<GitBlame>,
|
blame: &Entity<GitBlame>,
|
||||||
blame_entry: BlameEntry,
|
blame_entry: BlameEntry,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
last_used_color: &mut Option<(PlayerColor, Oid)>,
|
last_used_color: &mut Option<(PlayerColor, Oid)>,
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
renderer: Arc<dyn BlameRenderer>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> AnyElement {
|
) -> Option<AnyElement> {
|
||||||
let mut sha_color = cx
|
let mut sha_color = cx
|
||||||
.theme()
|
.theme()
|
||||||
.players()
|
.players()
|
||||||
.color_for_participant(blame_entry.sha.into());
|
.color_for_participant(blame_entry.sha.into());
|
||||||
|
|
||||||
// If the last color we used is the same as the one we get for this line, but
|
// If the last color we used is the same as the one we get for this line, but
|
||||||
// the commit SHAs are different, then we try again to get a different color.
|
// the commit SHAs are different, then we try again to get a different color.
|
||||||
match *last_used_color {
|
match *last_used_color {
|
||||||
|
@ -5791,97 +5780,20 @@ fn render_blame_entry(
|
||||||
};
|
};
|
||||||
last_used_color.replace((sha_color, blame_entry.sha));
|
last_used_color.replace((sha_color, blame_entry.sha));
|
||||||
|
|
||||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
let blame = blame.read(cx);
|
||||||
|
let details = blame.details_for_entry(&blame_entry);
|
||||||
let short_commit_id = blame_entry.sha.display_short();
|
let repository = blame.repository(cx)?;
|
||||||
|
renderer.render_blame_entry(
|
||||||
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
|
&style.text,
|
||||||
let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
|
blame_entry,
|
||||||
let details = blame.read(cx).details_for_entry(&blame_entry);
|
details,
|
||||||
|
repository,
|
||||||
h_flex()
|
workspace.downgrade(),
|
||||||
.w_full()
|
editor,
|
||||||
.justify_between()
|
ix,
|
||||||
.font_family(style.text.font().family)
|
sha_color.cursor,
|
||||||
.line_height(style.text.line_height)
|
|
||||||
.id(("blame", ix))
|
|
||||||
.text_color(cx.theme().status().hint)
|
|
||||||
.pr_2()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.child(div().text_color(sha_color.cursor).child(short_commit_id))
|
|
||||||
.child(name),
|
|
||||||
)
|
|
||||||
.child(relative_timestamp)
|
|
||||||
.on_mouse_down(MouseButton::Right, {
|
|
||||||
let blame_entry = blame_entry.clone();
|
|
||||||
let details = details.clone();
|
|
||||||
move |event, window, cx| {
|
|
||||||
deploy_blame_entry_context_menu(
|
|
||||||
&blame_entry,
|
|
||||||
details.as_ref(),
|
|
||||||
editor.clone(),
|
|
||||||
event.position,
|
|
||||||
window,
|
|
||||||
cx,
|
cx,
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
|
||||||
.when_some(
|
|
||||||
details
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|details| details.permalink.clone()),
|
|
||||||
|this, url| {
|
|
||||||
this.cursor_pointer().on_click(move |_, _, cx| {
|
|
||||||
cx.stop_propagation();
|
|
||||||
cx.open_url(url.as_str())
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.hoverable_tooltip(move |window, cx| {
|
|
||||||
cx.new(|cx| CommitTooltip::blame_entry(&blame_entry, details.clone(), window, cx))
|
|
||||||
.into()
|
|
||||||
})
|
|
||||||
.into_any()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deploy_blame_entry_context_menu(
|
|
||||||
blame_entry: &BlameEntry,
|
|
||||||
details: Option<&ParsedCommitMessage>,
|
|
||||||
editor: Entity<Editor>,
|
|
||||||
position: gpui::Point<Pixels>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) {
|
|
||||||
let context_menu = ContextMenu::build(window, cx, move |menu, _, _| {
|
|
||||||
let sha = format!("{}", blame_entry.sha);
|
|
||||||
menu.on_blur_subscription(Subscription::new(|| {}))
|
|
||||||
.entry("Copy commit SHA", None, move |_, cx| {
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(sha.clone()));
|
|
||||||
})
|
|
||||||
.when_some(
|
|
||||||
details.and_then(|details| details.permalink.clone()),
|
|
||||||
|this, url| {
|
|
||||||
this.entry("Open permalink", None, move |_, cx| {
|
|
||||||
cx.open_url(url.as_str())
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.update(cx, move |editor, cx| {
|
|
||||||
editor.mouse_context_menu = Some(MouseContextMenu::new(
|
|
||||||
MenuPosition::PinnedToScreen(position),
|
|
||||||
context_menu,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
));
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -6588,9 +6500,9 @@ impl Element for EditorElement {
|
||||||
window.with_rem_size(rem_size, |window| {
|
window.with_rem_size(rem_size, |window| {
|
||||||
window.with_text_style(Some(text_style), |window| {
|
window.with_text_style(Some(text_style), |window| {
|
||||||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||||
let mut snapshot = self
|
let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
|
||||||
.editor
|
(editor.snapshot(window, cx), editor.read_only(cx))
|
||||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
});
|
||||||
let style = self.style.clone();
|
let style = self.style.clone();
|
||||||
|
|
||||||
let font_id = window.text_system().resolve_font(&style.text.font());
|
let font_id = window.text_system().resolve_font(&style.text.font());
|
||||||
|
@ -6970,11 +6882,12 @@ impl Element for EditorElement {
|
||||||
.flatten()?;
|
.flatten()?;
|
||||||
let mut element = render_inline_blame_entry(
|
let mut element = render_inline_blame_entry(
|
||||||
self.editor.clone(),
|
self.editor.clone(),
|
||||||
|
editor.workspace()?.downgrade(),
|
||||||
blame,
|
blame,
|
||||||
blame_entry,
|
blame_entry,
|
||||||
&style,
|
&style,
|
||||||
cx,
|
cx,
|
||||||
);
|
)?;
|
||||||
let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance;
|
let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance;
|
||||||
Some(
|
Some(
|
||||||
element
|
element
|
||||||
|
@ -7507,7 +7420,10 @@ impl Element for EditorElement {
|
||||||
editor.last_position_map = Some(position_map.clone())
|
editor.last_position_map = Some(position_map.clone())
|
||||||
});
|
});
|
||||||
|
|
||||||
let diff_hunk_controls = self.layout_diff_hunk_controls(
|
let diff_hunk_controls = if is_read_only {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
self.layout_diff_hunk_controls(
|
||||||
start_row..end_row,
|
start_row..end_row,
|
||||||
&row_infos,
|
&row_infos,
|
||||||
&text_hitbox,
|
&text_hitbox,
|
||||||
|
@ -7519,7 +7435,8 @@ impl Element for EditorElement {
|
||||||
self.editor.clone(),
|
self.editor.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
)
|
||||||
|
};
|
||||||
|
|
||||||
EditorLayout {
|
EditorLayout {
|
||||||
mode,
|
mode,
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
|
use crate::Editor;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use git::{
|
use git::{
|
||||||
GitHostingProvider, GitHostingProviderRegistry, Oid,
|
GitHostingProviderRegistry, GitRemote, Oid,
|
||||||
blame::{Blame, BlameEntry},
|
blame::{Blame, BlameEntry, ParsedCommitMessage},
|
||||||
parse_git_remote_url,
|
parse_git_remote_url,
|
||||||
};
|
};
|
||||||
use gpui::{App, AppContext as _, Context, Entity, Subscription, Task};
|
use gpui::{
|
||||||
use http_client::HttpClient;
|
AnyElement, App, AppContext as _, Context, Entity, Hsla, Subscription, Task, TextStyle,
|
||||||
|
WeakEntity, Window,
|
||||||
|
};
|
||||||
use language::{Bias, Buffer, BufferSnapshot, Edit};
|
use language::{Bias, Buffer, BufferSnapshot, Edit};
|
||||||
use multi_buffer::RowInfo;
|
use multi_buffer::RowInfo;
|
||||||
use project::{Project, ProjectItem};
|
use project::{Project, ProjectItem, git_store::Repository};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use sum_tree::SumTree;
|
use sum_tree::SumTree;
|
||||||
use ui::SharedString;
|
use workspace::Workspace;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
use crate::commit_tooltip::ParsedCommitMessage;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct GitBlameEntry {
|
pub struct GitBlameEntry {
|
||||||
|
@ -59,45 +59,11 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct GitRemote {
|
|
||||||
pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
|
|
||||||
pub owner: String,
|
|
||||||
pub repo: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for GitRemote {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("GitRemote")
|
|
||||||
.field("host", &self.host.name())
|
|
||||||
.field("owner", &self.owner)
|
|
||||||
.field("repo", &self.repo)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GitRemote {
|
|
||||||
pub fn host_supports_avatars(&self) -> bool {
|
|
||||||
self.host.supports_avatars()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn avatar_url(
|
|
||||||
&self,
|
|
||||||
commit: SharedString,
|
|
||||||
client: Arc<dyn HttpClient>,
|
|
||||||
) -> Option<Url> {
|
|
||||||
self.host
|
|
||||||
.commit_author_avatar_url(&self.owner, &self.repo, commit, client)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct GitBlame {
|
pub struct GitBlame {
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
entries: SumTree<GitBlameEntry>,
|
entries: SumTree<GitBlameEntry>,
|
||||||
commit_details: HashMap<Oid, crate::commit_tooltip::ParsedCommitMessage>,
|
commit_details: HashMap<Oid, ParsedCommitMessage>,
|
||||||
buffer_snapshot: BufferSnapshot,
|
buffer_snapshot: BufferSnapshot,
|
||||||
buffer_edits: text::Subscription,
|
buffer_edits: text::Subscription,
|
||||||
task: Task<Result<()>>,
|
task: Task<Result<()>>,
|
||||||
|
@ -109,6 +75,91 @@ pub struct GitBlame {
|
||||||
_regenerate_subscriptions: Vec<Subscription>,
|
_regenerate_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait BlameRenderer {
|
||||||
|
fn max_author_length(&self) -> usize;
|
||||||
|
|
||||||
|
fn render_blame_entry(
|
||||||
|
&self,
|
||||||
|
_: &TextStyle,
|
||||||
|
_: BlameEntry,
|
||||||
|
_: Option<ParsedCommitMessage>,
|
||||||
|
_: Entity<Repository>,
|
||||||
|
_: WeakEntity<Workspace>,
|
||||||
|
_: Entity<Editor>,
|
||||||
|
_: usize,
|
||||||
|
_: Hsla,
|
||||||
|
_: &mut App,
|
||||||
|
) -> Option<AnyElement>;
|
||||||
|
|
||||||
|
fn render_inline_blame_entry(
|
||||||
|
&self,
|
||||||
|
_: &TextStyle,
|
||||||
|
_: BlameEntry,
|
||||||
|
_: Option<ParsedCommitMessage>,
|
||||||
|
_: Entity<Repository>,
|
||||||
|
_: WeakEntity<Workspace>,
|
||||||
|
_: Entity<Editor>,
|
||||||
|
_: &mut App,
|
||||||
|
) -> Option<AnyElement>;
|
||||||
|
|
||||||
|
fn open_blame_commit(
|
||||||
|
&self,
|
||||||
|
_: BlameEntry,
|
||||||
|
_: Entity<Repository>,
|
||||||
|
_: WeakEntity<Workspace>,
|
||||||
|
_: &mut Window,
|
||||||
|
_: &mut App,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlameRenderer for () {
|
||||||
|
fn max_author_length(&self) -> usize {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_blame_entry(
|
||||||
|
&self,
|
||||||
|
_: &TextStyle,
|
||||||
|
_: BlameEntry,
|
||||||
|
_: Option<ParsedCommitMessage>,
|
||||||
|
_: Entity<Repository>,
|
||||||
|
_: WeakEntity<Workspace>,
|
||||||
|
_: Entity<Editor>,
|
||||||
|
_: usize,
|
||||||
|
_: Hsla,
|
||||||
|
_: &mut App,
|
||||||
|
) -> Option<AnyElement> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_inline_blame_entry(
|
||||||
|
&self,
|
||||||
|
_: &TextStyle,
|
||||||
|
_: BlameEntry,
|
||||||
|
_: Option<ParsedCommitMessage>,
|
||||||
|
_: Entity<Repository>,
|
||||||
|
_: WeakEntity<Workspace>,
|
||||||
|
_: Entity<Editor>,
|
||||||
|
_: &mut App,
|
||||||
|
) -> Option<AnyElement> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_blame_commit(
|
||||||
|
&self,
|
||||||
|
_: BlameEntry,
|
||||||
|
_: Entity<Repository>,
|
||||||
|
_: WeakEntity<Workspace>,
|
||||||
|
_: &mut Window,
|
||||||
|
_: &mut App,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct GlobalBlameRenderer(pub Arc<dyn BlameRenderer>);
|
||||||
|
|
||||||
|
impl gpui::Global for GlobalBlameRenderer {}
|
||||||
|
|
||||||
impl GitBlame {
|
impl GitBlame {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
|
@ -181,6 +232,15 @@ impl GitBlame {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn repository(&self, cx: &App) -> Option<Entity<Repository>> {
|
||||||
|
self.project
|
||||||
|
.read(cx)
|
||||||
|
.git_store()
|
||||||
|
.read(cx)
|
||||||
|
.repository_and_path_for_buffer_id(self.buffer.read(cx).remote_id(), cx)
|
||||||
|
.map(|(repo, _)| repo)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_generated_entries(&self) -> bool {
|
pub fn has_generated_entries(&self) -> bool {
|
||||||
self.generated
|
self.generated
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,7 +109,7 @@ impl ProposedChangesEditor {
|
||||||
let diff =
|
let diff =
|
||||||
this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
|
this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
|
||||||
Some(diff.update(cx, |diff, cx| {
|
Some(diff.update(cx, |diff, cx| {
|
||||||
diff.set_base_text(base_buffer.clone(), buffer, cx)
|
diff.set_base_text_buffer(base_buffer.clone(), buffer, cx)
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
|
@ -185,7 +185,7 @@ impl ProposedChangesEditor {
|
||||||
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||||
new_diffs.push(cx.new(|cx| {
|
new_diffs.push(cx.new(|cx| {
|
||||||
let mut diff = BufferDiff::new(&branch_buffer.read(cx).snapshot(), cx);
|
let mut diff = BufferDiff::new(&branch_buffer.read(cx).snapshot(), cx);
|
||||||
let _ = diff.set_base_text(
|
let _ = diff.set_base_text_buffer(
|
||||||
location.buffer.clone(),
|
location.buffer.clone(),
|
||||||
branch_buffer.read(cx).text_snapshot(),
|
branch_buffer.read(cx).text_snapshot(),
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -111,6 +111,14 @@ impl GitRepository for FakeGitRepository {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_commit(
|
||||||
|
&self,
|
||||||
|
_commit: String,
|
||||||
|
_cx: AsyncApp,
|
||||||
|
) -> BoxFuture<Result<git::repository::CommitDiff>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
fn set_index_text(
|
fn set_index_text(
|
||||||
&self,
|
&self,
|
||||||
path: RepoPath,
|
path: RepoPath,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::Oid;
|
|
||||||
use crate::commit::get_messages;
|
use crate::commit::get_messages;
|
||||||
|
use crate::{GitRemote, Oid};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use futures::AsyncWriteExt;
|
use futures::AsyncWriteExt;
|
||||||
|
use gpui::SharedString;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use std::{ops::Range, path::Path};
|
use std::{ops::Range, path::Path};
|
||||||
|
@ -20,6 +21,14 @@ pub struct Blame {
|
||||||
pub remote_url: Option<String>,
|
pub remote_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct ParsedCommitMessage {
|
||||||
|
pub message: SharedString,
|
||||||
|
pub permalink: Option<url::Url>,
|
||||||
|
pub pull_request: Option<crate::hosting_provider::PullRequest>,
|
||||||
|
pub remote: Option<GitRemote>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Blame {
|
impl Blame {
|
||||||
pub async fn for_path(
|
pub async fn for_path(
|
||||||
git_binary: &Path,
|
git_binary: &Path,
|
||||||
|
|
Binary file not shown.
|
@ -15,6 +15,41 @@ pub struct PullRequest {
|
||||||
pub url: Url,
|
pub url: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GitRemote {
|
||||||
|
pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
|
||||||
|
pub owner: String,
|
||||||
|
pub repo: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for GitRemote {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("GitRemote")
|
||||||
|
.field("host", &self.host.name())
|
||||||
|
.field("owner", &self.owner)
|
||||||
|
.field("repo", &self.repo)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GitRemote {
|
||||||
|
pub fn host_supports_avatars(&self) -> bool {
|
||||||
|
self.host.supports_avatars()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn avatar_url(
|
||||||
|
&self,
|
||||||
|
commit: SharedString,
|
||||||
|
client: Arc<dyn HttpClient>,
|
||||||
|
) -> Option<Url> {
|
||||||
|
self.host
|
||||||
|
.commit_author_avatar_url(&self.owner, &self.repo, commit, client)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct BuildCommitPermalinkParams<'a> {
|
pub struct BuildCommitPermalinkParams<'a> {
|
||||||
pub sha: &'a str,
|
pub sha: &'a str,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
use crate::status::GitStatus;
|
use crate::commit::parse_git_diff_name_status;
|
||||||
|
use crate::status::{GitStatus, StatusCode};
|
||||||
use crate::{Oid, SHORT_SHA_LENGTH};
|
use crate::{Oid, SHORT_SHA_LENGTH};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::future::BoxFuture;
|
use futures::future::BoxFuture;
|
||||||
use futures::{AsyncWriteExt, FutureExt as _, select_biased};
|
use futures::{AsyncWriteExt, FutureExt as _, select_biased};
|
||||||
use git2::BranchType;
|
use git2::BranchType;
|
||||||
use gpui::{AsyncApp, BackgroundExecutor, SharedString};
|
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rope::Rope;
|
use rope::Rope;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::borrow::{Borrow, Cow};
|
use std::borrow::{Borrow, Cow};
|
||||||
use std::ffi::{OsStr, OsString};
|
use std::ffi::{OsStr, OsString};
|
||||||
use std::future;
|
|
||||||
use std::path::Component;
|
use std::path::Component;
|
||||||
use std::process::{ExitStatus, Stdio};
|
use std::process::{ExitStatus, Stdio};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
@ -21,6 +21,10 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use std::{
|
||||||
|
future,
|
||||||
|
io::{BufRead, BufReader, BufWriter, Read},
|
||||||
|
};
|
||||||
use sum_tree::MapSeekTarget;
|
use sum_tree::MapSeekTarget;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -133,6 +137,18 @@ pub struct CommitDetails {
|
||||||
pub committer_name: SharedString,
|
pub committer_name: SharedString,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommitDiff {
|
||||||
|
pub files: Vec<CommitFile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CommitFile {
|
||||||
|
pub path: RepoPath,
|
||||||
|
pub old_text: Option<String>,
|
||||||
|
pub new_text: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl CommitDetails {
|
impl CommitDetails {
|
||||||
pub fn short_sha(&self) -> SharedString {
|
pub fn short_sha(&self) -> SharedString {
|
||||||
self.sha[..SHORT_SHA_LENGTH].to_string().into()
|
self.sha[..SHORT_SHA_LENGTH].to_string().into()
|
||||||
|
@ -206,6 +222,7 @@ pub trait GitRepository: Send + Sync {
|
||||||
|
|
||||||
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
|
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
|
||||||
|
|
||||||
|
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>>;
|
||||||
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>>;
|
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>>;
|
||||||
|
|
||||||
/// Returns the absolute path to the repository. For worktrees, this will be the path to the
|
/// Returns the absolute path to the repository. For worktrees, this will be the path to the
|
||||||
|
@ -405,6 +422,108 @@ impl GitRepository for RealGitRepository {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> {
|
||||||
|
let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
|
||||||
|
else {
|
||||||
|
return future::ready(Err(anyhow!("no working directory"))).boxed();
|
||||||
|
};
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let show_output = util::command::new_std_command("git")
|
||||||
|
.current_dir(&working_directory)
|
||||||
|
.args([
|
||||||
|
"--no-optional-locks",
|
||||||
|
"show",
|
||||||
|
"--format=%P",
|
||||||
|
"-z",
|
||||||
|
"--no-renames",
|
||||||
|
"--name-status",
|
||||||
|
])
|
||||||
|
.arg(&commit)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.output()
|
||||||
|
.map_err(|e| anyhow!("Failed to start git show process: {e}"))?;
|
||||||
|
|
||||||
|
let show_stdout = String::from_utf8_lossy(&show_output.stdout);
|
||||||
|
let mut lines = show_stdout.split('\n');
|
||||||
|
let parent_sha = lines.next().unwrap().trim().trim_end_matches('\0');
|
||||||
|
let changes = parse_git_diff_name_status(lines.next().unwrap_or(""));
|
||||||
|
|
||||||
|
let mut cat_file_process = util::command::new_std_command("git")
|
||||||
|
.current_dir(&working_directory)
|
||||||
|
.args(["--no-optional-locks", "cat-file", "--batch=%(objectsize)"])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| anyhow!("Failed to start git cat-file process: {e}"))?;
|
||||||
|
|
||||||
|
use std::io::Write as _;
|
||||||
|
let mut files = Vec::<CommitFile>::new();
|
||||||
|
let mut stdin = BufWriter::with_capacity(512, cat_file_process.stdin.take().unwrap());
|
||||||
|
let mut stdout = BufReader::new(cat_file_process.stdout.take().unwrap());
|
||||||
|
let mut info_line = String::new();
|
||||||
|
let mut newline = [b'\0'];
|
||||||
|
for (path, status_code) in changes {
|
||||||
|
match status_code {
|
||||||
|
StatusCode::Modified => {
|
||||||
|
writeln!(&mut stdin, "{commit}:{}", path.display())?;
|
||||||
|
writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
|
||||||
|
}
|
||||||
|
StatusCode::Added => {
|
||||||
|
writeln!(&mut stdin, "{commit}:{}", path.display())?;
|
||||||
|
}
|
||||||
|
StatusCode::Deleted => {
|
||||||
|
writeln!(&mut stdin, "{parent_sha}:{}", path.display())?;
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
stdin.flush()?;
|
||||||
|
|
||||||
|
info_line.clear();
|
||||||
|
stdout.read_line(&mut info_line)?;
|
||||||
|
|
||||||
|
let len = info_line.trim_end().parse().with_context(|| {
|
||||||
|
format!("invalid object size output from cat-file {info_line}")
|
||||||
|
})?;
|
||||||
|
let mut text = vec![0; len];
|
||||||
|
stdout.read_exact(&mut text)?;
|
||||||
|
stdout.read_exact(&mut newline)?;
|
||||||
|
let text = String::from_utf8_lossy(&text).to_string();
|
||||||
|
|
||||||
|
let mut old_text = None;
|
||||||
|
let mut new_text = None;
|
||||||
|
match status_code {
|
||||||
|
StatusCode::Modified => {
|
||||||
|
info_line.clear();
|
||||||
|
stdout.read_line(&mut info_line)?;
|
||||||
|
let len = info_line.trim_end().parse().with_context(|| {
|
||||||
|
format!("invalid object size output from cat-file {}", info_line)
|
||||||
|
})?;
|
||||||
|
let mut parent_text = vec![0; len];
|
||||||
|
stdout.read_exact(&mut parent_text)?;
|
||||||
|
stdout.read_exact(&mut newline)?;
|
||||||
|
old_text = Some(String::from_utf8_lossy(&parent_text).to_string());
|
||||||
|
new_text = Some(text);
|
||||||
|
}
|
||||||
|
StatusCode::Added => new_text = Some(text),
|
||||||
|
StatusCode::Deleted => old_text = Some(text),
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push(CommitFile {
|
||||||
|
path: path.into(),
|
||||||
|
old_text,
|
||||||
|
new_text,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CommitDiff { files })
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
fn reset(
|
fn reset(
|
||||||
&self,
|
&self,
|
||||||
commit: String,
|
commit: String,
|
||||||
|
|
|
@ -21,6 +21,7 @@ anyhow.workspace = true
|
||||||
askpass.workspace = true
|
askpass.workspace = true
|
||||||
assistant_settings.workspace = true
|
assistant_settings.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
|
@ -36,6 +37,7 @@ language_model.workspace = true
|
||||||
linkify.workspace = true
|
linkify.workspace = true
|
||||||
linkme.workspace = true
|
linkme.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
markdown.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
multi_buffer.workspace = true
|
multi_buffer.workspace = true
|
||||||
notifications.workspace = true
|
notifications.workspace = true
|
||||||
|
|
234
crates/git_ui/src/blame_ui.rs
Normal file
234
crates/git_ui/src/blame_ui.rs
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
use crate::{commit_tooltip::CommitTooltip, commit_view::CommitView};
|
||||||
|
use editor::{BlameRenderer, Editor};
|
||||||
|
use git::{
|
||||||
|
blame::{BlameEntry, ParsedCommitMessage},
|
||||||
|
repository::CommitSummary,
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, App, AppContext as _, ClipboardItem, Element as _, Entity, Hsla,
|
||||||
|
InteractiveElement as _, MouseButton, Pixels, StatefulInteractiveElement as _, Styled as _,
|
||||||
|
Subscription, TextStyle, WeakEntity, Window, div,
|
||||||
|
};
|
||||||
|
use project::{git_store::Repository, project_settings::ProjectSettings};
|
||||||
|
use settings::Settings as _;
|
||||||
|
use ui::{
|
||||||
|
ActiveTheme, Color, ContextMenu, FluentBuilder as _, Icon, IconName, ParentElement as _, h_flex,
|
||||||
|
};
|
||||||
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
|
||||||
|
|
||||||
|
pub struct GitBlameRenderer;
|
||||||
|
|
||||||
|
impl BlameRenderer for GitBlameRenderer {
|
||||||
|
fn max_author_length(&self) -> usize {
|
||||||
|
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_blame_entry(
|
||||||
|
&self,
|
||||||
|
style: &TextStyle,
|
||||||
|
blame_entry: BlameEntry,
|
||||||
|
details: Option<ParsedCommitMessage>,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
ix: usize,
|
||||||
|
sha_color: Hsla,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<AnyElement> {
|
||||||
|
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
||||||
|
let short_commit_id = blame_entry.sha.display_short();
|
||||||
|
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
|
||||||
|
let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.justify_between()
|
||||||
|
.font_family(style.font().family)
|
||||||
|
.line_height(style.line_height)
|
||||||
|
.id(("blame", ix))
|
||||||
|
.text_color(cx.theme().status().hint)
|
||||||
|
.pr_2()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(div().text_color(sha_color).child(short_commit_id))
|
||||||
|
.child(name),
|
||||||
|
)
|
||||||
|
.child(relative_timestamp)
|
||||||
|
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||||
|
.cursor_pointer()
|
||||||
|
.on_mouse_down(MouseButton::Right, {
|
||||||
|
let blame_entry = blame_entry.clone();
|
||||||
|
let details = details.clone();
|
||||||
|
move |event, window, cx| {
|
||||||
|
deploy_blame_entry_context_menu(
|
||||||
|
&blame_entry,
|
||||||
|
details.as_ref(),
|
||||||
|
editor.clone(),
|
||||||
|
event.position,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on_click({
|
||||||
|
let blame_entry = blame_entry.clone();
|
||||||
|
let repository = repository.clone();
|
||||||
|
let workspace = workspace.clone();
|
||||||
|
move |_, window, cx| {
|
||||||
|
CommitView::open(
|
||||||
|
CommitSummary {
|
||||||
|
sha: blame_entry.sha.to_string().into(),
|
||||||
|
subject: blame_entry.summary.clone().unwrap_or_default().into(),
|
||||||
|
commit_timestamp: blame_entry.committer_time.unwrap_or_default(),
|
||||||
|
has_parent: true,
|
||||||
|
},
|
||||||
|
repository.downgrade(),
|
||||||
|
workspace.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.hoverable_tooltip(move |window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
CommitTooltip::blame_entry(
|
||||||
|
&blame_entry,
|
||||||
|
details.clone(),
|
||||||
|
repository.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_inline_blame_entry(
|
||||||
|
&self,
|
||||||
|
style: &TextStyle,
|
||||||
|
blame_entry: BlameEntry,
|
||||||
|
details: Option<ParsedCommitMessage>,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Option<AnyElement> {
|
||||||
|
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
||||||
|
let author = blame_entry.author.as_deref().unwrap_or_default();
|
||||||
|
let summary_enabled = ProjectSettings::get_global(cx)
|
||||||
|
.git
|
||||||
|
.show_inline_commit_summary();
|
||||||
|
|
||||||
|
let text = match blame_entry.summary.as_ref() {
|
||||||
|
Some(summary) if summary_enabled => {
|
||||||
|
format!("{}, {} - {}", author, relative_timestamp, summary)
|
||||||
|
}
|
||||||
|
_ => format!("{}, {}", author, relative_timestamp),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(
|
||||||
|
h_flex()
|
||||||
|
.id("inline-blame")
|
||||||
|
.w_full()
|
||||||
|
.font_family(style.font().family)
|
||||||
|
.text_color(cx.theme().status().hint)
|
||||||
|
.line_height(style.line_height)
|
||||||
|
.child(Icon::new(IconName::FileGit).color(Color::Hint))
|
||||||
|
.child(text)
|
||||||
|
.gap_2()
|
||||||
|
.hoverable_tooltip(move |window, cx| {
|
||||||
|
let tooltip = cx.new(|cx| {
|
||||||
|
CommitTooltip::blame_entry(
|
||||||
|
&blame_entry,
|
||||||
|
details.clone(),
|
||||||
|
repository.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
editor.update(cx, |editor, _| {
|
||||||
|
editor.git_blame_inline_tooltip = Some(tooltip.downgrade().into())
|
||||||
|
});
|
||||||
|
tooltip.into()
|
||||||
|
})
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_blame_commit(
|
||||||
|
&self,
|
||||||
|
blame_entry: BlameEntry,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
CommitView::open(
|
||||||
|
CommitSummary {
|
||||||
|
sha: blame_entry.sha.to_string().into(),
|
||||||
|
subject: blame_entry.summary.clone().unwrap_or_default().into(),
|
||||||
|
commit_timestamp: blame_entry.committer_time.unwrap_or_default(),
|
||||||
|
has_parent: true,
|
||||||
|
},
|
||||||
|
repository.downgrade(),
|
||||||
|
workspace.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deploy_blame_entry_context_menu(
|
||||||
|
blame_entry: &BlameEntry,
|
||||||
|
details: Option<&ParsedCommitMessage>,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
position: gpui::Point<Pixels>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let context_menu = ContextMenu::build(window, cx, move |menu, _, _| {
|
||||||
|
let sha = format!("{}", blame_entry.sha);
|
||||||
|
menu.on_blur_subscription(Subscription::new(|| {}))
|
||||||
|
.entry("Copy commit SHA", None, move |_, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(sha.clone()));
|
||||||
|
})
|
||||||
|
.when_some(
|
||||||
|
details.and_then(|details| details.permalink.clone()),
|
||||||
|
|this, url| {
|
||||||
|
this.entry("Open permalink", None, move |_, cx| {
|
||||||
|
cx.open_url(url.as_str())
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.update(cx, move |editor, cx| {
|
||||||
|
editor.deploy_mouse_context_menu(position, context_menu, window, cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
|
||||||
|
match blame_entry.author_offset_date_time() {
|
||||||
|
Ok(timestamp) => {
|
||||||
|
let local = chrono::Local::now().offset().local_minus_utc();
|
||||||
|
time_format::format_localized_timestamp(
|
||||||
|
timestamp,
|
||||||
|
time::OffsetDateTime::now_utc(),
|
||||||
|
time::UtcOffset::from_whole_seconds(local).unwrap(),
|
||||||
|
time_format::TimestampFormat::Relative,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(_) => "Error parsing date".to_string(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,22 @@
|
||||||
|
use crate::commit_view::CommitView;
|
||||||
|
use editor::hover_markdown_style;
|
||||||
use futures::Future;
|
use futures::Future;
|
||||||
use git::PullRequest;
|
|
||||||
use git::blame::BlameEntry;
|
use git::blame::BlameEntry;
|
||||||
|
use git::repository::CommitSummary;
|
||||||
|
use git::{GitRemote, blame::ParsedCommitMessage};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
||||||
StatefulInteractiveElement,
|
StatefulInteractiveElement, WeakEntity, prelude::*,
|
||||||
};
|
};
|
||||||
use markdown::Markdown;
|
use markdown::Markdown;
|
||||||
|
use project::git_store::Repository;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::hash::Hash;
|
use std::hash::Hash;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use time::{OffsetDateTime, UtcOffset};
|
use time::{OffsetDateTime, UtcOffset};
|
||||||
use time_format::format_local_timestamp;
|
use time_format::format_local_timestamp;
|
||||||
use ui::{Avatar, Divider, IconButtonShape, prelude::*, tooltip_container};
|
use ui::{Avatar, Divider, IconButtonShape, prelude::*, tooltip_container};
|
||||||
use url::Url;
|
use workspace::Workspace;
|
||||||
|
|
||||||
use crate::git::blame::GitRemote;
|
|
||||||
use crate::hover_popover::hover_markdown_style;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CommitDetails {
|
pub struct CommitDetails {
|
||||||
|
@ -26,14 +27,6 @@ pub struct CommitDetails {
|
||||||
pub message: Option<ParsedCommitMessage>,
|
pub message: Option<ParsedCommitMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct ParsedCommitMessage {
|
|
||||||
pub message: SharedString,
|
|
||||||
pub permalink: Option<Url>,
|
|
||||||
pub pull_request: Option<PullRequest>,
|
|
||||||
pub remote: Option<GitRemote>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CommitAvatar<'a> {
|
struct CommitAvatar<'a> {
|
||||||
commit: &'a CommitDetails,
|
commit: &'a CommitDetails,
|
||||||
}
|
}
|
||||||
|
@ -54,10 +47,10 @@ impl<'a> CommitAvatar<'a> {
|
||||||
.commit
|
.commit
|
||||||
.message
|
.message
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|details| details.remote.as_ref())
|
.and_then(|details| details.remote.clone())
|
||||||
.filter(|remote| remote.host_supports_avatars())?;
|
.filter(|remote| remote.host_supports_avatars())?;
|
||||||
|
|
||||||
let avatar_url = CommitAvatarAsset::new(remote.clone(), self.commit.sha.clone());
|
let avatar_url = CommitAvatarAsset::new(remote, self.commit.sha.clone());
|
||||||
|
|
||||||
let element = match window.use_asset::<CommitAvatarAsset>(&avatar_url, cx) {
|
let element = match window.use_asset::<CommitAvatarAsset>(&avatar_url, cx) {
|
||||||
// Loading or no avatar found
|
// Loading or no avatar found
|
||||||
|
@ -115,12 +108,16 @@ pub struct CommitTooltip {
|
||||||
commit: CommitDetails,
|
commit: CommitDetails,
|
||||||
scroll_handle: ScrollHandle,
|
scroll_handle: ScrollHandle,
|
||||||
markdown: Entity<Markdown>,
|
markdown: Entity<Markdown>,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommitTooltip {
|
impl CommitTooltip {
|
||||||
pub fn blame_entry(
|
pub fn blame_entry(
|
||||||
blame: &BlameEntry,
|
blame: &BlameEntry,
|
||||||
details: Option<ParsedCommitMessage>,
|
details: Option<ParsedCommitMessage>,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -141,12 +138,20 @@ impl CommitTooltip {
|
||||||
author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(),
|
author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(),
|
||||||
message: details,
|
message: details,
|
||||||
},
|
},
|
||||||
|
repository,
|
||||||
|
workspace,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(commit: CommitDetails, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(
|
||||||
|
commit: CommitDetails,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
let mut style = hover_markdown_style(window, cx);
|
let mut style = hover_markdown_style(window, cx);
|
||||||
if let Some(code_block) = &style.code_block.text {
|
if let Some(code_block) = &style.code_block.text {
|
||||||
style.base_text_style.refine(code_block);
|
style.base_text_style.refine(code_block);
|
||||||
|
@ -166,6 +171,8 @@ impl CommitTooltip {
|
||||||
});
|
});
|
||||||
Self {
|
Self {
|
||||||
commit,
|
commit,
|
||||||
|
repository,
|
||||||
|
workspace,
|
||||||
scroll_handle: ScrollHandle::new(),
|
scroll_handle: ScrollHandle::new(),
|
||||||
markdown,
|
markdown,
|
||||||
}
|
}
|
||||||
|
@ -208,6 +215,27 @@ impl Render for CommitTooltip {
|
||||||
|
|
||||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
||||||
let message_max_height = window.line_height() * 12 + (ui_font_size / 0.4);
|
let message_max_height = window.line_height() * 12 + (ui_font_size / 0.4);
|
||||||
|
let repo = self.repository.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
let commit_summary = CommitSummary {
|
||||||
|
sha: self.commit.sha.clone(),
|
||||||
|
subject: self
|
||||||
|
.commit
|
||||||
|
.message
|
||||||
|
.as_ref()
|
||||||
|
.map_or(Default::default(), |message| {
|
||||||
|
message
|
||||||
|
.message
|
||||||
|
.split('\n')
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.trim_end()
|
||||||
|
.to_string()
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
commit_timestamp: self.commit.commit_time.unix_timestamp(),
|
||||||
|
has_parent: false,
|
||||||
|
};
|
||||||
|
|
||||||
tooltip_container(window, cx, move |this, _, cx| {
|
tooltip_container(window, cx, move |this, _, cx| {
|
||||||
this.occlude()
|
this.occlude()
|
||||||
|
@ -283,24 +311,16 @@ impl Render for CommitTooltip {
|
||||||
.icon(IconName::FileGit)
|
.icon(IconName::FileGit)
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.disabled(
|
.on_click(
|
||||||
self.commit
|
move |_, window, cx| {
|
||||||
.message
|
CommitView::open(
|
||||||
.as_ref()
|
commit_summary.clone(),
|
||||||
.map_or(true, |details| {
|
repo.downgrade(),
|
||||||
details.permalink.is_none()
|
workspace.clone(),
|
||||||
}),
|
window,
|
||||||
)
|
cx,
|
||||||
.when_some(
|
);
|
||||||
self.commit
|
|
||||||
.message
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|details| details.permalink.clone()),
|
|
||||||
|this, url| {
|
|
||||||
this.on_click(move |_, _, cx| {
|
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.open_url(url.as_str())
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
527
crates/git_ui/src/commit_view.rs
Normal file
527
crates/git_ui/src/commit_view.rs
Normal file
|
@ -0,0 +1,527 @@
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
|
||||||
|
use editor::{Editor, EditorEvent, MultiBuffer};
|
||||||
|
use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
|
||||||
|
FocusHandle, Focusable, IntoElement, Render, WeakEntity, Window,
|
||||||
|
};
|
||||||
|
use language::{
|
||||||
|
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
|
||||||
|
Point, Rope, TextBuffer,
|
||||||
|
};
|
||||||
|
use multi_buffer::PathKey;
|
||||||
|
use project::{Project, WorktreeId, git_store::Repository};
|
||||||
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
ffi::OsStr,
|
||||||
|
fmt::Write as _,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use ui::{Color, Icon, IconName, Label, LabelCommon as _};
|
||||||
|
use util::{ResultExt, truncate_and_trailoff};
|
||||||
|
use workspace::{
|
||||||
|
Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||||
|
item::{BreadcrumbText, ItemEvent, TabContentParams},
|
||||||
|
searchable::SearchableItemHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct CommitView {
|
||||||
|
commit: CommitDetails,
|
||||||
|
editor: Entity<Editor>,
|
||||||
|
multibuffer: Entity<MultiBuffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GitBlob {
|
||||||
|
path: RepoPath,
|
||||||
|
worktree_id: WorktreeId,
|
||||||
|
is_deleted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CommitMetadataFile {
|
||||||
|
title: Arc<Path>,
|
||||||
|
worktree_id: WorktreeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
const COMMIT_METADATA_NAMESPACE: &'static str = "0";
|
||||||
|
const FILE_NAMESPACE: &'static str = "1";
|
||||||
|
|
||||||
|
impl CommitView {
|
||||||
|
pub fn open(
|
||||||
|
commit: CommitSummary,
|
||||||
|
repo: WeakEntity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let commit_diff = repo
|
||||||
|
.update(cx, |repo, _| repo.load_commit_diff(commit.sha.to_string()))
|
||||||
|
.ok();
|
||||||
|
let commit_details = repo
|
||||||
|
.update(cx, |repo, _| repo.show(commit.sha.to_string()))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
window
|
||||||
|
.spawn(cx, async move |cx| {
|
||||||
|
let (commit_diff, commit_details) = futures::join!(commit_diff?, commit_details?);
|
||||||
|
let commit_diff = commit_diff.log_err()?.log_err()?;
|
||||||
|
let commit_details = commit_details.log_err()?.log_err()?;
|
||||||
|
let repo = repo.upgrade()?;
|
||||||
|
|
||||||
|
workspace
|
||||||
|
.update_in(cx, |workspace, window, cx| {
|
||||||
|
let project = workspace.project();
|
||||||
|
let commit_view = cx.new(|cx| {
|
||||||
|
CommitView::new(
|
||||||
|
commit_details,
|
||||||
|
commit_diff,
|
||||||
|
repo,
|
||||||
|
project.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let pane = workspace.active_pane();
|
||||||
|
pane.update(cx, |pane, cx| {
|
||||||
|
let ix = pane.items().position(|item| {
|
||||||
|
let commit_view = item.downcast::<CommitView>();
|
||||||
|
commit_view
|
||||||
|
.map_or(false, |view| view.read(cx).commit.sha == commit.sha)
|
||||||
|
});
|
||||||
|
if let Some(ix) = ix {
|
||||||
|
pane.activate_item(ix, true, true, window, cx);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
pane.add_item(Box::new(commit_view), true, true, None, window, cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(
|
||||||
|
commit: CommitDetails,
|
||||||
|
commit_diff: CommitDiff,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let language_registry = project.read(cx).languages().clone();
|
||||||
|
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadOnly));
|
||||||
|
let editor = cx.new(|cx| {
|
||||||
|
let mut editor =
|
||||||
|
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||||
|
editor.disable_inline_diagnostics();
|
||||||
|
editor.set_expand_all_diff_hunks(cx);
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
|
let first_worktree_id = project
|
||||||
|
.read(cx)
|
||||||
|
.worktrees(cx)
|
||||||
|
.next()
|
||||||
|
.map(|worktree| worktree.read(cx).id());
|
||||||
|
|
||||||
|
let mut metadata_buffer_id = None;
|
||||||
|
if let Some(worktree_id) = first_worktree_id {
|
||||||
|
let file = Arc::new(CommitMetadataFile {
|
||||||
|
title: PathBuf::from(format!("commit {}", commit.sha)).into(),
|
||||||
|
worktree_id,
|
||||||
|
});
|
||||||
|
let buffer = cx.new(|cx| {
|
||||||
|
let buffer = TextBuffer::new_normalized(
|
||||||
|
0,
|
||||||
|
cx.entity_id().as_non_zero_u64().into(),
|
||||||
|
LineEnding::default(),
|
||||||
|
format_commit(&commit).into(),
|
||||||
|
);
|
||||||
|
metadata_buffer_id = Some(buffer.remote_id());
|
||||||
|
Buffer::build(buffer, Some(file.clone()), Capability::ReadWrite)
|
||||||
|
});
|
||||||
|
multibuffer.update(cx, |multibuffer, cx| {
|
||||||
|
multibuffer.set_excerpts_for_path(
|
||||||
|
PathKey::namespaced(COMMIT_METADATA_NAMESPACE, file.title.clone()),
|
||||||
|
buffer.clone(),
|
||||||
|
vec![Point::zero()..buffer.read(cx).max_point()],
|
||||||
|
0,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.disable_header_for_buffer(metadata_buffer_id.unwrap(), cx);
|
||||||
|
editor.change_selections(None, window, cx, |selections| {
|
||||||
|
selections.select_ranges(vec![0..0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.spawn(async move |this, mut cx| {
|
||||||
|
for file in commit_diff.files {
|
||||||
|
let is_deleted = file.new_text.is_none();
|
||||||
|
let new_text = file.new_text.unwrap_or_default();
|
||||||
|
let old_text = file.old_text;
|
||||||
|
let worktree_id = repository
|
||||||
|
.update(cx, |repository, cx| {
|
||||||
|
repository
|
||||||
|
.repo_path_to_project_path(&file.path, cx)
|
||||||
|
.map(|path| path.worktree_id)
|
||||||
|
.or(first_worktree_id)
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| anyhow!("project has no worktrees"))?;
|
||||||
|
let file = Arc::new(GitBlob {
|
||||||
|
path: file.path.clone(),
|
||||||
|
is_deleted,
|
||||||
|
worktree_id,
|
||||||
|
}) as Arc<dyn language::File>;
|
||||||
|
|
||||||
|
let buffer = build_buffer(new_text, file, &language_registry, &mut cx).await?;
|
||||||
|
let buffer_diff =
|
||||||
|
build_buffer_diff(old_text, &buffer, &language_registry, &mut cx).await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.multibuffer.update(cx, |multibuffer, cx| {
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
let diff = buffer_diff.read(cx);
|
||||||
|
let diff_hunk_ranges = diff
|
||||||
|
.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx)
|
||||||
|
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let path = snapshot.file().unwrap().path().clone();
|
||||||
|
let _is_newly_added = multibuffer.set_excerpts_for_path(
|
||||||
|
PathKey::namespaced(FILE_NAMESPACE, path),
|
||||||
|
buffer,
|
||||||
|
diff_hunk_ranges,
|
||||||
|
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
multibuffer.add_diff(buffer_diff, cx);
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
commit,
|
||||||
|
editor,
|
||||||
|
multibuffer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl language::File for GitBlob {
|
||||||
|
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disk_state(&self) -> DiskState {
|
||||||
|
if self.is_deleted {
|
||||||
|
DiskState::Deleted
|
||||||
|
} else {
|
||||||
|
DiskState::New
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Arc<Path> {
|
||||||
|
&self.path.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_path(&self, _: &App) -> PathBuf {
|
||||||
|
self.path.to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
|
||||||
|
self.path.file_name().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worktree_id(&self, _: &App) -> WorktreeId {
|
||||||
|
self.worktree_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto(&self, _cx: &App) -> language::proto::File {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_private(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl language::File for CommitMetadataFile {
|
||||||
|
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disk_state(&self) -> DiskState {
|
||||||
|
DiskState::New
|
||||||
|
}
|
||||||
|
|
||||||
|
fn path(&self) -> &Arc<Path> {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
fn full_path(&self, _: &App) -> PathBuf {
|
||||||
|
self.title.as_ref().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_name<'a>(&'a self, _: &'a App) -> &'a OsStr {
|
||||||
|
self.title.file_name().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn worktree_id(&self, _: &App) -> WorktreeId {
|
||||||
|
self.worktree_id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto(&self, _: &App) -> language::proto::File {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_private(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_buffer(
|
||||||
|
mut text: String,
|
||||||
|
blob: Arc<dyn File>,
|
||||||
|
language_registry: &Arc<language::LanguageRegistry>,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<Entity<Buffer>> {
|
||||||
|
let line_ending = LineEnding::detect(&text);
|
||||||
|
LineEnding::normalize(&mut text);
|
||||||
|
let text = Rope::from(text);
|
||||||
|
let language = cx.update(|cx| language_registry.language_for_file(&blob, Some(&text), cx))?;
|
||||||
|
let language = if let Some(language) = language {
|
||||||
|
language_registry
|
||||||
|
.load_language(&language)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|e| e.log_err())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let buffer = cx.new(|cx| {
|
||||||
|
let buffer = TextBuffer::new_normalized(
|
||||||
|
0,
|
||||||
|
cx.entity_id().as_non_zero_u64().into(),
|
||||||
|
line_ending,
|
||||||
|
text,
|
||||||
|
);
|
||||||
|
let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
|
||||||
|
buffer.set_language(language, cx);
|
||||||
|
buffer
|
||||||
|
})?;
|
||||||
|
Ok(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_buffer_diff(
|
||||||
|
mut old_text: Option<String>,
|
||||||
|
buffer: &Entity<Buffer>,
|
||||||
|
language_registry: &Arc<LanguageRegistry>,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<Entity<BufferDiff>> {
|
||||||
|
if let Some(old_text) = &mut old_text {
|
||||||
|
LineEnding::normalize(old_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let buffer = cx.update(|cx| buffer.read(cx).snapshot())?;
|
||||||
|
|
||||||
|
let base_buffer = cx
|
||||||
|
.update(|cx| {
|
||||||
|
Buffer::build_snapshot(
|
||||||
|
old_text.as_deref().unwrap_or("").into(),
|
||||||
|
buffer.language().cloned(),
|
||||||
|
Some(language_registry.clone()),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let diff_snapshot = cx
|
||||||
|
.update(|cx| {
|
||||||
|
BufferDiffSnapshot::new_with_base_buffer(
|
||||||
|
buffer.text.clone(),
|
||||||
|
old_text.map(Arc::new),
|
||||||
|
base_buffer,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.new(|cx| {
|
||||||
|
let mut diff = BufferDiff::new(&buffer.text, cx);
|
||||||
|
diff.set_snapshot(diff_snapshot, &buffer.text, None, cx);
|
||||||
|
diff
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_commit(commit: &CommitDetails) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
writeln!(&mut result, "commit {}", commit.sha).unwrap();
|
||||||
|
writeln!(
|
||||||
|
&mut result,
|
||||||
|
"Author: {} <{}>",
|
||||||
|
commit.committer_name, commit.committer_email
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
writeln!(
|
||||||
|
&mut result,
|
||||||
|
"Date: {}",
|
||||||
|
time_format::format_local_timestamp(
|
||||||
|
time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp).unwrap(),
|
||||||
|
time::OffsetDateTime::now_utc(),
|
||||||
|
time_format::TimestampFormat::MediumAbsolute,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
result.push('\n');
|
||||||
|
for line in commit.message.split('\n') {
|
||||||
|
if line.is_empty() {
|
||||||
|
result.push('\n');
|
||||||
|
} else {
|
||||||
|
writeln!(&mut result, " {}", line).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result.ends_with("\n\n") {
|
||||||
|
result.pop();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<EditorEvent> for CommitView {}
|
||||||
|
|
||||||
|
impl Focusable for CommitView {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.editor.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for CommitView {
|
||||||
|
type Event = EditorEvent;
|
||||||
|
|
||||||
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||||
|
Some(Icon::new(IconName::GitBranch).color(Color::Muted))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_content(&self, params: TabContentParams, _window: &Window, _: &App) -> AnyElement {
|
||||||
|
let short_sha = self.commit.sha.get(0..7).unwrap_or(&*self.commit.sha);
|
||||||
|
let subject = truncate_and_trailoff(self.commit.message.split('\n').next().unwrap(), 20);
|
||||||
|
Label::new(format!("{short_sha} - {subject}",))
|
||||||
|
.color(if params.selected {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_tooltip_text(&self, _: &App) -> Option<ui::SharedString> {
|
||||||
|
let short_sha = self.commit.sha.get(0..16).unwrap_or(&*self.commit.sha);
|
||||||
|
let subject = self.commit.message.split('\n').next().unwrap();
|
||||||
|
Some(format!("{short_sha} - {subject}").into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||||
|
Editor::to_item_events(event, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||||
|
Some("Commit View Opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.editor
|
||||||
|
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_singleton(&self, _: &App) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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 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 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 CommitView {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.editor.clone()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
use crate::askpass_modal::AskPassModal;
|
use crate::askpass_modal::AskPassModal;
|
||||||
use crate::commit_modal::CommitModal;
|
use crate::commit_modal::CommitModal;
|
||||||
|
use crate::commit_tooltip::CommitTooltip;
|
||||||
|
use crate::commit_view::CommitView;
|
||||||
use crate::git_panel_settings::StatusStyle;
|
use crate::git_panel_settings::StatusStyle;
|
||||||
use crate::project_diff::Diff;
|
use crate::project_diff::{self, Diff, ProjectDiff};
|
||||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||||
|
use crate::{branch_picker, picker_prompt, render_remote_button};
|
||||||
use crate::{ProjectDiff, picker_prompt, project_diff};
|
|
||||||
use crate::{branch_picker, render_remote_button};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||||
};
|
};
|
||||||
|
@ -13,13 +13,13 @@ use anyhow::Result;
|
||||||
use askpass::AskPassDelegate;
|
use askpass::AskPassDelegate;
|
||||||
use assistant_settings::AssistantSettings;
|
use assistant_settings::AssistantSettings;
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::commit_tooltip::CommitTooltip;
|
|
||||||
|
|
||||||
use editor::{
|
use editor::{
|
||||||
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||||
scroll::ScrollbarAutoHide,
|
scroll::ScrollbarAutoHide,
|
||||||
};
|
};
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
|
use git::blame::ParsedCommitMessage;
|
||||||
use git::repository::{
|
use git::repository::{
|
||||||
Branch, CommitDetails, CommitSummary, DiffType, PushOptions, Remote, RemoteCommandOutput,
|
Branch, CommitDetails, CommitSummary, DiffType, PushOptions, Remote, RemoteCommandOutput,
|
||||||
ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
|
||||||
|
@ -3001,6 +3001,7 @@ impl GitPanel {
|
||||||
let active_repository = self.active_repository.as_ref()?;
|
let active_repository = self.active_repository.as_ref()?;
|
||||||
let branch = active_repository.read(cx).current_branch()?;
|
let branch = active_repository.read(cx).current_branch()?;
|
||||||
let commit = branch.most_recent_commit.as_ref()?.clone();
|
let commit = branch.most_recent_commit.as_ref()?.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
|
||||||
let this = cx.entity();
|
let this = cx.entity();
|
||||||
Some(
|
Some(
|
||||||
|
@ -3023,14 +3024,31 @@ impl GitPanel {
|
||||||
.truncate(),
|
.truncate(),
|
||||||
)
|
)
|
||||||
.id("commit-msg-hover")
|
.id("commit-msg-hover")
|
||||||
.hoverable_tooltip(move |window, cx| {
|
.on_click({
|
||||||
|
let commit = commit.clone();
|
||||||
|
let repo = active_repository.downgrade();
|
||||||
|
move |_, window, cx| {
|
||||||
|
CommitView::open(
|
||||||
|
commit.clone(),
|
||||||
|
repo.clone(),
|
||||||
|
workspace.clone().clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.hoverable_tooltip({
|
||||||
|
let repo = active_repository.clone();
|
||||||
|
move |window, cx| {
|
||||||
GitPanelMessageTooltip::new(
|
GitPanelMessageTooltip::new(
|
||||||
this.clone(),
|
this.clone(),
|
||||||
commit.sha.clone(),
|
commit.sha.clone(),
|
||||||
|
repo.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(div().flex_1())
|
.child(div().flex_1())
|
||||||
|
@ -3938,31 +3956,35 @@ impl GitPanelMessageTooltip {
|
||||||
fn new(
|
fn new(
|
||||||
git_panel: Entity<GitPanel>,
|
git_panel: Entity<GitPanel>,
|
||||||
sha: SharedString,
|
sha: SharedString,
|
||||||
|
repository: Entity<Repository>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let details = git_panel
|
let (details, workspace) = git_panel.update(cx, |git_panel, cx| {
|
||||||
.update(cx, |git_panel, cx| {
|
(
|
||||||
git_panel.load_commit_details(sha.to_string(), cx)
|
git_panel.load_commit_details(sha.to_string(), cx),
|
||||||
})?
|
git_panel.workspace.clone(),
|
||||||
.await?;
|
)
|
||||||
|
})?;
|
||||||
|
let details = details.await?;
|
||||||
|
|
||||||
let commit_details = editor::commit_tooltip::CommitDetails {
|
let commit_details = crate::commit_tooltip::CommitDetails {
|
||||||
sha: details.sha.clone(),
|
sha: details.sha.clone(),
|
||||||
author_name: details.committer_name.clone(),
|
author_name: details.committer_name.clone(),
|
||||||
author_email: details.committer_email.clone(),
|
author_email: details.committer_email.clone(),
|
||||||
commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
|
commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
|
||||||
message: Some(editor::commit_tooltip::ParsedCommitMessage {
|
message: Some(ParsedCommitMessage {
|
||||||
message: details.message.clone(),
|
message: details.message.clone(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.update_in(cx, |this: &mut GitPanelMessageTooltip, window, cx| {
|
this.update_in(cx, |this: &mut GitPanelMessageTooltip, window, cx| {
|
||||||
this.commit_tooltip =
|
this.commit_tooltip = Some(cx.new(move |cx| {
|
||||||
Some(cx.new(move |cx| CommitTooltip::new(commit_details, window, cx)));
|
CommitTooltip::new(commit_details, repository, workspace, window, cx)
|
||||||
|
}));
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::any::Any;
|
||||||
use ::settings::Settings;
|
use ::settings::Settings;
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
use commit_modal::CommitModal;
|
use commit_modal::CommitModal;
|
||||||
|
mod blame_ui;
|
||||||
use git::{
|
use git::{
|
||||||
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
|
||||||
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
|
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
|
||||||
|
@ -17,6 +18,8 @@ use workspace::Workspace;
|
||||||
mod askpass_modal;
|
mod askpass_modal;
|
||||||
pub mod branch_picker;
|
pub mod branch_picker;
|
||||||
mod commit_modal;
|
mod commit_modal;
|
||||||
|
pub mod commit_tooltip;
|
||||||
|
mod commit_view;
|
||||||
pub mod git_panel;
|
pub mod git_panel;
|
||||||
mod git_panel_settings;
|
mod git_panel_settings;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
|
@ -30,6 +33,8 @@ actions!(git, [ResetOnboarding]);
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
GitPanelSettings::register(cx);
|
GitPanelSettings::register(cx);
|
||||||
|
|
||||||
|
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
|
||||||
|
|
||||||
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
cx.observe_new(|workspace: &mut Workspace, _, cx| {
|
||||||
ProjectDiff::register(workspace, cx);
|
ProjectDiff::register(workspace, cx);
|
||||||
CommitModal::register(workspace);
|
CommitModal::register(workspace);
|
||||||
|
|
|
@ -9,7 +9,7 @@ use serde_json::Value;
|
||||||
use std::{ops::Range, str::FromStr, sync::Arc};
|
use std::{ops::Range, str::FromStr, sync::Arc};
|
||||||
use text::*;
|
use text::*;
|
||||||
|
|
||||||
pub use proto::{BufferState, Operation};
|
pub use proto::{BufferState, File, Operation};
|
||||||
|
|
||||||
/// Deserializes a `[text::LineEnding]` from the RPC representation.
|
/// Deserializes a `[text::LineEnding]` from the RPC representation.
|
||||||
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
|
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
|
||||||
|
|
|
@ -21,8 +21,8 @@ use git::{
|
||||||
blame::Blame,
|
blame::Blame,
|
||||||
parse_git_remote_url,
|
parse_git_remote_url,
|
||||||
repository::{
|
repository::{
|
||||||
Branch, CommitDetails, DiffType, GitRepository, GitRepositoryCheckpoint, PushOptions,
|
Branch, CommitDetails, CommitDiff, CommitFile, DiffType, GitRepository,
|
||||||
Remote, RemoteCommandOutput, RepoPath, ResetMode,
|
GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
|
||||||
},
|
},
|
||||||
status::FileStatus,
|
status::FileStatus,
|
||||||
};
|
};
|
||||||
|
@ -289,6 +289,7 @@ impl GitStore {
|
||||||
client.add_entity_request_handler(Self::handle_commit);
|
client.add_entity_request_handler(Self::handle_commit);
|
||||||
client.add_entity_request_handler(Self::handle_reset);
|
client.add_entity_request_handler(Self::handle_reset);
|
||||||
client.add_entity_request_handler(Self::handle_show);
|
client.add_entity_request_handler(Self::handle_show);
|
||||||
|
client.add_entity_request_handler(Self::handle_load_commit_diff);
|
||||||
client.add_entity_request_handler(Self::handle_checkout_files);
|
client.add_entity_request_handler(Self::handle_checkout_files);
|
||||||
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
|
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
|
||||||
client.add_entity_request_handler(Self::handle_set_index_text);
|
client.add_entity_request_handler(Self::handle_set_index_text);
|
||||||
|
@ -1885,6 +1886,32 @@ impl GitStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_load_commit_diff(
|
||||||
|
this: Entity<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::LoadCommitDiff>,
|
||||||
|
mut cx: AsyncApp,
|
||||||
|
) -> Result<proto::LoadCommitDiffResponse> {
|
||||||
|
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
|
||||||
|
let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
|
||||||
|
|
||||||
|
let commit_diff = repository_handle
|
||||||
|
.update(&mut cx, |repository_handle, _| {
|
||||||
|
repository_handle.load_commit_diff(envelope.payload.commit)
|
||||||
|
})?
|
||||||
|
.await??;
|
||||||
|
Ok(proto::LoadCommitDiffResponse {
|
||||||
|
files: commit_diff
|
||||||
|
.files
|
||||||
|
.into_iter()
|
||||||
|
.map(|file| proto::CommitFile {
|
||||||
|
path: file.path.to_string(),
|
||||||
|
old_text: file.old_text,
|
||||||
|
new_text: file.new_text,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_reset(
|
async fn handle_reset(
|
||||||
this: Entity<Self>,
|
this: Entity<Self>,
|
||||||
envelope: TypedEnvelope<proto::GitReset>,
|
envelope: TypedEnvelope<proto::GitReset>,
|
||||||
|
@ -2389,7 +2416,10 @@ impl BufferDiffState {
|
||||||
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
|
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
|
||||||
{
|
{
|
||||||
unstaged_diff.update(cx, |diff, cx| {
|
unstaged_diff.update(cx, |diff, cx| {
|
||||||
diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
|
if language_changed {
|
||||||
|
diff.language_changed(cx);
|
||||||
|
}
|
||||||
|
diff.set_snapshot(new_unstaged_diff, &buffer, None, cx)
|
||||||
})?
|
})?
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -2398,14 +2428,11 @@ impl BufferDiffState {
|
||||||
if let Some((uncommitted_diff, new_uncommitted_diff)) =
|
if let Some((uncommitted_diff, new_uncommitted_diff)) =
|
||||||
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
|
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
|
||||||
{
|
{
|
||||||
uncommitted_diff.update(cx, |uncommitted_diff, cx| {
|
uncommitted_diff.update(cx, |diff, cx| {
|
||||||
uncommitted_diff.set_snapshot(
|
if language_changed {
|
||||||
&buffer,
|
diff.language_changed(cx);
|
||||||
new_uncommitted_diff,
|
}
|
||||||
language_changed,
|
diff.set_snapshot(new_uncommitted_diff, &buffer, unstaged_changed_range, cx);
|
||||||
unstaged_changed_range,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2869,6 +2896,40 @@ impl Repository {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_commit_diff(&self, commit: String) -> oneshot::Receiver<Result<CommitDiff>> {
|
||||||
|
self.send_job(|git_repo, cx| async move {
|
||||||
|
match git_repo {
|
||||||
|
RepositoryState::Local(git_repository) => {
|
||||||
|
git_repository.load_commit(commit, cx).await
|
||||||
|
}
|
||||||
|
RepositoryState::Remote {
|
||||||
|
client,
|
||||||
|
project_id,
|
||||||
|
work_directory_id,
|
||||||
|
} => {
|
||||||
|
let response = client
|
||||||
|
.request(proto::LoadCommitDiff {
|
||||||
|
project_id: project_id.0,
|
||||||
|
work_directory_id: work_directory_id.to_proto(),
|
||||||
|
commit,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
Ok(CommitDiff {
|
||||||
|
files: response
|
||||||
|
.files
|
||||||
|
.into_iter()
|
||||||
|
.map(|file| CommitFile {
|
||||||
|
path: PathBuf::from(file.path).into(),
|
||||||
|
old_text: file.old_text,
|
||||||
|
new_text: file.new_text,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
|
fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
|
||||||
Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
|
Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
|
||||||
}
|
}
|
||||||
|
|
|
@ -365,6 +365,9 @@ message Envelope {
|
||||||
|
|
||||||
LanguageServerIdForName language_server_id_for_name = 332;
|
LanguageServerIdForName language_server_id_for_name = 332;
|
||||||
LanguageServerIdForNameResponse language_server_id_for_name_response = 333; // current max
|
LanguageServerIdForNameResponse language_server_id_for_name_response = 333; // current max
|
||||||
|
|
||||||
|
LoadCommitDiff load_commit_diff = 334;
|
||||||
|
LoadCommitDiffResponse load_commit_diff_response = 335; // current max
|
||||||
}
|
}
|
||||||
|
|
||||||
reserved 87 to 88;
|
reserved 87 to 88;
|
||||||
|
@ -3365,6 +3368,23 @@ message GitCommitDetails {
|
||||||
string committer_name = 5;
|
string committer_name = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message LoadCommitDiff {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
reserved 2;
|
||||||
|
uint64 work_directory_id = 3;
|
||||||
|
string commit = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoadCommitDiffResponse {
|
||||||
|
repeated CommitFile files = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CommitFile {
|
||||||
|
string path = 1;
|
||||||
|
optional string old_text = 2;
|
||||||
|
optional string new_text = 3;
|
||||||
|
}
|
||||||
|
|
||||||
message GitReset {
|
message GitReset {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
reserved 2;
|
reserved 2;
|
||||||
|
|
|
@ -340,6 +340,8 @@ messages!(
|
||||||
(ListRemoteDirectoryResponse, Background),
|
(ListRemoteDirectoryResponse, Background),
|
||||||
(ListToolchains, Foreground),
|
(ListToolchains, Foreground),
|
||||||
(ListToolchainsResponse, Foreground),
|
(ListToolchainsResponse, Foreground),
|
||||||
|
(LoadCommitDiff, Foreground),
|
||||||
|
(LoadCommitDiffResponse, Foreground),
|
||||||
(LspExtExpandMacro, Background),
|
(LspExtExpandMacro, Background),
|
||||||
(LspExtExpandMacroResponse, Background),
|
(LspExtExpandMacroResponse, Background),
|
||||||
(LspExtOpenDocs, Background),
|
(LspExtOpenDocs, Background),
|
||||||
|
@ -534,6 +536,7 @@ request_messages!(
|
||||||
(JoinRoom, JoinRoomResponse),
|
(JoinRoom, JoinRoomResponse),
|
||||||
(LeaveChannelBuffer, Ack),
|
(LeaveChannelBuffer, Ack),
|
||||||
(LeaveRoom, Ack),
|
(LeaveRoom, Ack),
|
||||||
|
(LoadCommitDiff, LoadCommitDiffResponse),
|
||||||
(MarkNotificationRead, Ack),
|
(MarkNotificationRead, Ack),
|
||||||
(MoveChannel, Ack),
|
(MoveChannel, Ack),
|
||||||
(OnTypeFormatting, OnTypeFormattingResponse),
|
(OnTypeFormatting, OnTypeFormattingResponse),
|
||||||
|
@ -668,6 +671,7 @@ entity_messages!(
|
||||||
JoinProject,
|
JoinProject,
|
||||||
LeaveProject,
|
LeaveProject,
|
||||||
LinkedEditingRange,
|
LinkedEditingRange,
|
||||||
|
LoadCommitDiff,
|
||||||
MultiLspQuery,
|
MultiLspQuery,
|
||||||
RestartLanguageServers,
|
RestartLanguageServers,
|
||||||
OnTypeFormatting,
|
OnTypeFormatting,
|
||||||
|
|
|
@ -81,6 +81,7 @@ impl MultibufferHint {
|
||||||
|
|
||||||
if active_pane_item.is_singleton(cx)
|
if active_pane_item.is_singleton(cx)
|
||||||
|| active_pane_item.breadcrumbs(cx.theme(), cx).is_none()
|
|| active_pane_item.breadcrumbs(cx.theme(), cx).is_none()
|
||||||
|
|| !active_pane_item.can_save(cx)
|
||||||
{
|
{
|
||||||
return ToolbarItemLocation::Hidden;
|
return ToolbarItemLocation::Hidden;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue