Detailed changes
@@ -4402,7 +4402,6 @@ dependencies = [
"anyhow",
"assets",
"buffer_diff",
- "chrono",
"client",
"clock",
"collections",
@@ -4451,7 +4450,6 @@ dependencies = [
"text",
"theme",
"time",
- "time_format",
"tree-sitter-html",
"tree-sitter-rust",
"tree-sitter-typescript",
@@ -5715,6 +5713,7 @@ dependencies = [
"askpass",
"assistant_settings",
"buffer_diff",
+ "chrono",
"collections",
"command_palette_hooks",
"component",
@@ -5732,6 +5731,7 @@ dependencies = [
"linkify",
"linkme",
"log",
+ "markdown",
"menu",
"multi_buffer",
"notifications",
@@ -483,7 +483,7 @@ impl TrackedBuffer {
buffer_without_edits
.update(cx, |buffer, cx| buffer.undo_operations(edits_to_undo, cx));
let primary_diff_update = self.diff.update(cx, |diff, cx| {
- diff.set_base_text(
+ diff.set_base_text_buffer(
buffer_without_edits,
self.buffer.read(cx).text_snapshot(),
cx,
@@ -500,7 +500,7 @@ impl TrackedBuffer {
buffer.undo_operations(unreviewed_edits_to_undo, 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(),
self.buffer.read(cx).text_snapshot(),
cx,
@@ -559,13 +559,7 @@ impl TrackedBuffer {
if let Ok(primary_diff_snapshot) = primary_diff_snapshot {
primary_diff
.update(cx, |diff, cx| {
- diff.set_snapshot(
- &buffer_snapshot,
- primary_diff_snapshot,
- false,
- None,
- cx,
- )
+ diff.set_snapshot(primary_diff_snapshot, &buffer_snapshot, None, cx)
})
.ok();
}
@@ -574,9 +568,8 @@ impl TrackedBuffer {
secondary_diff
.update(cx, |diff, cx| {
diff.set_snapshot(
- &buffer_snapshot,
secondary_diff_snapshot,
- false,
+ &buffer_snapshot,
None,
cx,
)
@@ -142,6 +142,96 @@ impl std::fmt::Debug for BufferDiffInner {
}
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 {
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>> {
let mut new_cursor = self.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 BufferDiff {
- #[cfg(test)]
- fn build_sync(
- buffer: text::BufferSnapshot,
- diff_base: String,
- cx: &mut gpui::TestAppContext,
- ) -> BufferDiffInner {
- let snapshot =
- cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx));
- cx.executor().block(snapshot)
- }
-
- fn build(
- buffer: text::BufferSnapshot,
- base_text: Option<Arc<String>>,
- language: Option<Arc<Language>>,
- language_registry: Option<Arc<LanguageRegistry>>,
- cx: &mut App,
- ) -> impl Future<Output = BufferDiffInner> + 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);
- BufferDiffInner {
- base_text,
- hunks,
- base_text_exists,
- pending_hunks: SumTree::new(&buffer),
- }
+ pub fn new(buffer: &text::BufferSnapshot, cx: &mut App) -> Self {
+ BufferDiff {
+ buffer_id: buffer.remote_id(),
+ inner: BufferDiffSnapshot::empty(buffer, cx).inner,
+ secondary_diff: None,
}
}
- 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,
+ #[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 = BufferDiffSnapshot::new_with_base_text(
+ buffer.read(cx).text_snapshot(),
+ Some(base_text.into()),
+ None,
+ None,
+ cx,
+ );
+ let snapshot = cx.background_executor().block(snapshot);
+ Self {
+ buffer_id: buffer.read(cx).remote_id(),
+ inner: snapshot.inner,
+ secondary_diff: None,
}
}
@@ -917,9 +979,9 @@ impl BufferDiff {
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut AsyncApp,
) -> anyhow::Result<BufferDiffSnapshot> {
- let inner = if base_text_changed || language_changed {
+ Ok(if base_text_changed || language_changed {
cx.update(|cx| {
- Self::build(
+ BufferDiffSnapshot::new_with_base_text(
buffer.clone(),
base_text,
language.clone(),
@@ -930,7 +992,7 @@ impl BufferDiff {
.await
} else {
this.read_with(cx, |this, cx| {
- Self::build_with_base_buffer(
+ BufferDiffSnapshot::new_with_base_buffer(
buffer.clone(),
base_text,
this.base_text().clone(),
@@ -938,25 +1000,21 @@ impl BufferDiff {
)
})?
.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(
&mut self,
- buffer: &text::BufferSnapshot,
new_snapshot: BufferDiffSnapshot,
- language_changed: bool,
+ buffer: &text::BufferSnapshot,
secondary_changed_range: Option<Range<Anchor>>,
cx: &mut Context<Self>,
) -> Option<Range<Anchor>> {
- let changed_range = self.set_state(new_snapshot.inner, buffer);
- if language_changed {
- cx.emit(BufferDiffEvent::LanguageChanged);
- }
+ let changed_range = self.inner.set_state(new_snapshot.inner, buffer);
let changed_range = match (secondary_changed_range, changed_range) {
(None, None) => None,
@@ -980,31 +1038,6 @@ impl BufferDiff {
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 {
&self.inner.base_text
}
@@ -1065,21 +1098,31 @@ impl BufferDiff {
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(
+ pub fn set_base_text_buffer(
&mut self,
base_buffer: Entity<language::Buffer>,
buffer: text::BufferSnapshot,
cx: &mut Context<Self>,
) -> oneshot::Receiver<()> {
- let (tx, rx) = oneshot::channel();
- let this = cx.weak_entity();
let base_buffer = base_buffer.read(cx);
let language_registry = base_buffer.language_registry();
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 snapshot = BufferDiff::build(
+ let snapshot = BufferDiffSnapshot::new_with_base_text(
buffer.clone(),
Some(base_text),
base_buffer.language().cloned(),
@@ -1094,8 +1137,8 @@ impl BufferDiff {
let Some(this) = this.upgrade() else {
return;
};
- this.update(cx, |this, _| {
- this.set_state(snapshot, &buffer);
+ this.update(cx, |this, cx| {
+ this.set_snapshot(snapshot, &buffer, None, cx);
})
.log_err();
drop(complete_on_drop)
@@ -1110,49 +1153,17 @@ impl BufferDiff {
.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"))]
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 snapshot = BufferDiff::build_with_base_buffer(
+ let snapshot = BufferDiffSnapshot::new_with_base_buffer(
buffer.clone(),
base_text,
self.inner.base_text.clone(),
cx,
);
let snapshot = cx.background_executor().block(snapshot);
- let changed_range = self.set_state(snapshot, &buffer);
- cx.emit(BufferDiffEvent::DiffChanged { changed_range });
+ self.set_snapshot(snapshot, &buffer, None, cx);
}
}
@@ -1325,18 +1336,18 @@ mod tests {
.unindent();
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(
- diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
&buffer,
&diff_base,
&[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified_none())],
);
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(
- diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
&buffer,
&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, _>(
- diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None),
+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
&buffer,
&diff_base,
&[],
@@ -1399,9 +1410,10 @@ mod tests {
.unindent();
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
- let unstaged_diff = BufferDiff::build_sync(buffer.clone(), index_text.clone(), cx);
-
- let uncommitted_diff = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
+ let unstaged_diff = BufferDiffSnapshot::new_sync(buffer.clone(), index_text.clone(), cx);
+ let mut uncommitted_diff =
+ BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
+ uncommitted_diff.secondary_diff = Some(Box::new(unstaged_diff));
let expected_hunks = vec![
(2..3, "two\n", "TWO\n", DiffHunkStatus::modified_none()),
@@ -1420,11 +1432,7 @@ mod tests {
];
assert_hunks(
- uncommitted_diff.hunks_intersecting_range(
- Anchor::MIN..Anchor::MAX,
- &buffer,
- Some(&unstaged_diff),
- ),
+ uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer),
&buffer,
&head_text,
&expected_hunks,
@@ -1473,11 +1481,17 @@ mod tests {
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text);
let diff = 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;
assert_eq!(
- diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None)
+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer)
.count(),
8
);
@@ -1486,7 +1500,6 @@ mod tests {
diff.hunks_intersecting_range(
buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)),
&buffer,
- None,
),
&buffer,
&diff_base,
@@ -1732,18 +1745,20 @@ mod tests {
let hunk_range =
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 uncommitted = BufferDiff::build_sync(buffer.clone(), example.head_text.clone(), cx);
+ let unstaged =
+ 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 mut diff = BufferDiff::new(&buffer, cx);
- diff.set_state(unstaged, &buffer);
+ diff.set_snapshot(unstaged, &buffer, None, cx);
diff
});
let uncommitted_diff = cx.new(|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
});
@@ -1800,16 +1815,16 @@ mod tests {
.unindent();
let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text.clone());
- let unstaged = BufferDiff::build_sync(buffer.clone(), index_text, cx);
- let uncommitted = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx);
+ let unstaged = BufferDiffSnapshot::new_sync(buffer.clone(), index_text, cx);
+ let uncommitted = BufferDiffSnapshot::new_sync(buffer.clone(), head_text.clone(), cx);
let unstaged_diff = cx.new(|cx| {
let mut diff = BufferDiff::new(&buffer, cx);
- diff.set_state(unstaged, &buffer);
+ diff.set_snapshot(unstaged, &buffer, None, cx);
diff
});
let uncommitted_diff = cx.new(|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
});
@@ -1874,9 +1889,9 @@ mod tests {
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 diff_1 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
- let range = diff_1.compare(&empty_diff, &buffer).unwrap();
+ let empty_diff = cx.update(|cx| BufferDiffSnapshot::empty(&buffer, cx));
+ let diff_1 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+ 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));
// Edit does not affect the diff.
@@ -1893,8 +1908,8 @@ mod tests {
"
.unindent(),
);
- let diff_2 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
- assert_eq!(None, diff_2.compare(&diff_1, &buffer));
+ let diff_2 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+ assert_eq!(None, diff_2.inner.compare(&diff_1.inner, &buffer));
// Edit turns a deletion hunk into a modification.
buffer.edit_via_marked_text(
@@ -1910,8 +1925,8 @@ mod tests {
"
.unindent(),
);
- let diff_3 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
- let range = diff_3.compare(&diff_2, &buffer).unwrap();
+ let diff_3 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+ 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));
// Edit turns a modification hunk into a deletion.
@@ -1927,8 +1942,8 @@ mod tests {
"
.unindent(),
);
- let diff_4 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx);
- let range = diff_4.compare(&diff_3, &buffer).unwrap();
+ let diff_4 = BufferDiffSnapshot::new_sync(buffer.clone(), base_text.clone(), cx);
+ 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));
// Edit introduces a new insertion hunk.
@@ -1945,8 +1960,8 @@ mod tests {
"
.unindent(),
);
- let diff_5 = BufferDiff::build_sync(buffer.snapshot(), base_text.clone(), cx);
- let range = diff_5.compare(&diff_4, &buffer).unwrap();
+ let diff_5 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text.clone(), cx);
+ 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));
// Edit removes a hunk.
@@ -1963,8 +1978,8 @@ mod tests {
"
.unindent(),
);
- let diff_6 = BufferDiff::build_sync(buffer.snapshot(), base_text, cx);
- let range = diff_6.compare(&diff_5, &buffer).unwrap();
+ let diff_6 = BufferDiffSnapshot::new_sync(buffer.snapshot(), base_text, cx);
+ 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));
}
@@ -2038,14 +2053,16 @@ mod tests {
head_text: String,
cx: &mut TestAppContext,
) -> 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 {
buffer_id: working_copy.remote_id(),
- inner: BufferDiff::build_sync(
+ inner: BufferDiffSnapshot::new_sync(
working_copy.text.clone(),
index_text.to_string(),
cx,
- ),
+ )
+ .inner,
secondary_diff: None,
};
let secondary = cx.new(|_| secondary);
@@ -413,6 +413,7 @@ impl Server {
.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::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::GitCheckoutFiles>)
.add_request_handler(forward_mutating_project_request::<proto::SetIndexText>)
@@ -32,7 +32,6 @@ test-support = [
aho-corasick.workspace = true
anyhow.workspace = true
assets.workspace = true
-chrono.workspace = true
client.workspace = true
clock.workspace = true
collections.workspace = true
@@ -47,7 +46,6 @@ fuzzy.workspace = true
fs.workspace = true
git.workspace = true
gpui.workspace = true
-http_client.workspace = true
indoc.workspace = true
inline_completion.workspace = true
itertools.workspace = true
@@ -76,7 +74,6 @@ task.workspace = true
telemetry.workspace = true
text.workspace = true
time.workspace = true
-time_format.workspace = true
theme.workspace = true
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
@@ -419,6 +419,7 @@ actions!(
EditLogBreakpoint,
ToggleAutoSignatureHelp,
ToggleGitBlameInline,
+ OpenGitBlameCommit,
ToggleIndentGuides,
ToggleInlayHints,
ToggleInlineDiagnostics,
@@ -321,6 +321,20 @@ impl DisplayMap {
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(
&mut self,
buffer_ids: impl IntoIterator<Item = language::BufferId>,
@@ -40,6 +40,7 @@ pub struct BlockMap {
buffer_header_height: u32,
excerpt_header_height: u32,
pub(super) folded_buffers: HashSet<BufferId>,
+ buffers_with_disabled_headers: HashSet<BufferId>,
}
pub struct BlockMapReader<'a> {
@@ -422,6 +423,7 @@ impl BlockMap {
custom_blocks: Vec::new(),
custom_blocks_by_id: TreeMap::default(),
folded_buffers: HashSet::default(),
+ buffers_with_disabled_headers: HashSet::default(),
transforms: RefCell::new(transforms),
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
buffer_header_height,
@@ -642,11 +644,8 @@ impl BlockMap {
);
if buffer.show_headers() {
- blocks_in_edit.extend(BlockMap::header_and_footer_blocks(
- self.buffer_header_height,
- self.excerpt_header_height,
+ blocks_in_edit.extend(self.header_and_footer_blocks(
buffer,
- &self.folded_buffers,
(start_bound, end_bound),
wrap_snapshot,
));
@@ -714,10 +713,8 @@ impl BlockMap {
}
fn header_and_footer_blocks<'a, R, T>(
- buffer_header_height: u32,
- excerpt_header_height: u32,
+ &'a self,
buffer: &'a multi_buffer::MultiBufferSnapshot,
- folded_buffers: &'a HashSet<BufferId>,
range: R,
wrap_snapshot: &'a WrapSnapshot,
) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'a
@@ -728,73 +725,78 @@ impl BlockMap {
let mut boundaries = buffer.excerpt_boundaries_in_range(range).peekable();
std::iter::from_fn(move || {
- let excerpt_boundary = boundaries.next()?;
- let wrap_row = wrap_snapshot
- .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
- .row();
-
- let new_buffer_id = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
- (None, next) => Some(next.buffer_id),
- (Some(prev), next) => {
- if prev.buffer_id != next.buffer_id {
- Some(next.buffer_id)
- } else {
- None
+ loop {
+ let excerpt_boundary = boundaries.next()?;
+ let wrap_row = wrap_snapshot
+ .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
+ .row();
+
+ let new_buffer_id = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
+ (None, next) => Some(next.buffer_id),
+ (Some(prev), next) => {
+ if prev.buffer_id != next.buffer_id {
+ Some(next.buffer_id)
+ } else {
+ None
+ }
}
- }
- };
+ };
- let mut height = 0;
+ let mut height = 0;
- if let Some(new_buffer_id) = new_buffer_id {
- let first_excerpt = excerpt_boundary.next.clone();
- if folded_buffers.contains(&new_buffer_id) {
- let mut last_excerpt_end_row = first_excerpt.end_row;
+ if let Some(new_buffer_id) = new_buffer_id {
+ let first_excerpt = excerpt_boundary.next.clone();
+ 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;
- while let Some(next_boundary) = boundaries.peek() {
- if next_boundary.next.buffer_id == new_buffer_id {
- last_excerpt_end_row = next_boundary.next.end_row;
- } else {
- break;
+ while let Some(next_boundary) = boundaries.peek() {
+ if next_boundary.next.buffer_id == new_buffer_id {
+ last_excerpt_end_row = next_boundary.next.end_row;
+ } else {
+ break;
+ }
+
+ boundaries.next();
}
- boundaries.next();
+ let wrap_end_row = wrap_snapshot
+ .make_wrap_point(
+ Point::new(
+ last_excerpt_end_row.0,
+ buffer.line_len(last_excerpt_end_row),
+ ),
+ Bias::Right,
+ )
+ .row();
+
+ return Some((
+ BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
+ Block::FoldedBuffer {
+ height: height + self.buffer_header_height,
+ first_excerpt,
+ },
+ ));
}
+ }
- let wrap_end_row = wrap_snapshot
- .make_wrap_point(
- Point::new(
- last_excerpt_end_row.0,
- buffer.line_len(last_excerpt_end_row),
- ),
- Bias::Right,
- )
- .row();
-
- return Some((
- BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
- Block::FoldedBuffer {
- height: height + buffer_header_height,
- first_excerpt,
- },
- ));
+ if new_buffer_id.is_some() {
+ height += self.buffer_header_height;
+ } else {
+ height += self.excerpt_header_height;
}
- }
- if new_buffer_id.is_some() {
- height += buffer_header_height;
- } else {
- height += excerpt_header_height;
+ return Some((
+ BlockPlacement::Above(WrapRow(wrap_row)),
+ Block::ExcerptBoundary {
+ excerpt: excerpt_boundary.next,
+ height,
+ starts_new_buffer: new_buffer_id.is_some(),
+ },
+ ));
}
-
- Some((
- BlockPlacement::Above(WrapRow(wrap_row)),
- Block::ExcerptBoundary {
- excerpt: excerpt_boundary.next,
- height,
- starts_new_buffer: new_buffer_id.is_some(),
- },
- ))
})
}
@@ -1168,6 +1170,10 @@ impl BlockMapWriter<'_> {
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(
&mut self,
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
- expected_blocks.extend(BlockMap::header_and_footer_blocks(
- buffer_start_header_height,
- excerpt_header_height,
+ expected_blocks.extend(block_map.header_and_footer_blocks(
&buffer_snapshot,
- &block_map.folded_buffers,
0..,
&wraps_snapshot,
));
@@ -16,7 +16,6 @@ pub mod actions;
mod blink_manager;
mod clangd_ext;
mod code_context_menus;
-pub mod commit_tooltip;
pub mod display_map;
mod editor_settings;
mod editor_settings_controls;
@@ -82,19 +81,21 @@ use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionsMenu, ContextMenuOrigin,
};
-use git::blame::GitBlame;
+use git::blame::{GitBlame, GlobalBlameRenderer};
use gpui::{
- Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext,
- AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context,
- DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent,
- Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers,
- MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size,
- Stateful, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement,
- UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
- div, impl_actions, point, prelude::*, pulsating_between, px, relative, size,
+ Action, Animation, AnimationExt, AnyElement, AnyWeakEntity, App, AppContext,
+ AsyncWindowContext, AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry,
+ ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter,
+ FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
+ KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render,
+ SharedString, Size, Stateful, Styled, StyledText, Subscription, Task, TextStyle,
+ TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
+ WeakFocusHandle, Window, div, impl_actions, point, prelude::*, pulsating_between, px, relative,
+ size,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
+pub use hover_popover::hover_markdown_style;
use hover_popover::{HoverState, hide_hover};
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
@@ -124,6 +125,7 @@ use project::{
},
};
+pub use git::blame::BlameRenderer;
pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
@@ -187,8 +189,8 @@ use theme::{
observe_buffer_font_size_adjustment,
};
use ui::{
- ButtonSize, ButtonStyle, Disclosure, IconButton, IconButtonShape, IconName, IconSize, Key,
- Tooltip, h_flex, prelude::*,
+ ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
+ IconSize, Key, Tooltip, h_flex, prelude::*,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{
@@ -302,6 +304,8 @@ pub fn init_settings(cx: &mut App) {
pub fn init(cx: &mut App) {
init_settings(cx);
+ cx.set_global(GlobalBlameRenderer(Arc::new(())));
+
workspace::register_project_item::<Editor>(cx);
workspace::FollowableViewRegistry::register::<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;
trait InvalidationRegion {
@@ -766,7 +774,7 @@ pub struct Editor {
show_git_blame_gutter: bool,
show_git_blame_inline: bool,
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,
render_diff_hunk_controls: RenderDiffHunkControlsFn,
serialize_dirty_buffers: bool,
@@ -848,8 +856,6 @@ pub struct EditorSnapshot {
gutter_hovered: bool,
}
-const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
-
#[derive(Default, Debug, Clone, Copy)]
pub struct GutterDimensions {
pub left_padding: Pixels,
@@ -1643,6 +1649,21 @@ impl Editor {
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 {
self.mouse_context_menu
.as_ref()
@@ -14922,6 +14943,13 @@ impl Editor {
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.
pub fn remove_folds_with_type<T: ToOffset + Clone>(
&mut self,
@@ -15861,6 +15889,45 @@ impl Editor {
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 {
self.git_blame_inline_enabled
}
@@ -17794,7 +17861,9 @@ fn get_uncommitted_diff_for_buffer(
let mut tasks = Vec::new();
project.update(cx, |project, cx| {
for buffer in buffers {
- tasks.push(project.open_uncommitted_diff(buffer.clone(), cx))
+ if project::File::from_dyn(buffer.read(cx).file()).is_some() {
+ tasks.push(project.open_uncommitted_diff(buffer.clone(), cx))
+ }
}
});
cx.spawn(async move |cx| {
@@ -18911,13 +18980,13 @@ impl EditorSnapshot {
let git_blame_entries_width =
self.git_blame_gutter_max_author_length
.map(|max_author_length| {
+ let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
const MAX_RELATIVE_TIMESTAMP: &str = "60 minutes ago";
/// The number of characters to dedicate to gaps and margins.
const SPACING_WIDTH: usize = 4;
- let max_char_count = max_author_length
- .min(GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED)
+ let max_char_count = max_author_length.min(renderer.max_author_length())
+ ::git::SHORT_SHA_LENGTH
+ MAX_RELATIVE_TIMESTAMP.len()
+ SPACING_WIDTH;
@@ -3,13 +3,12 @@ use crate::{
ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow,
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock,
- GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
- HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
- LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
- PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection,
- SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold,
+ GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason,
+ InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN,
+ MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp,
+ Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
+ StickyHeaderExcerpt, ToPoint, ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
- commit_tooltip::{CommitTooltip, ParsedCommitMessage, blame_entry_relative_timestamp},
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
},
@@ -17,13 +16,13 @@ use crate::{
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
ScrollbarAxes, ScrollbarDiagnostics, ShowScrollbar,
},
- git::blame::GitBlame,
+ git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
hover_popover::{
self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, hover_at,
},
inlay_hint_settings,
items::BufferSearchHighlights,
- mouse_context_menu::{self, MenuPosition, MouseContextMenu},
+ mouse_context_menu::{self, MenuPosition},
scroll::scroll_amount::ScrollAmount,
};
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
@@ -34,12 +33,12 @@ use file_icons::FileIcons;
use git::{Oid, blame::BlameEntry, status::FileStatus};
use gpui::{
Action, Along, AnyElement, App, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds,
- ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
- Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
- Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
+ ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element,
+ ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla,
+ InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
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,
transparent_black,
};
@@ -76,10 +75,10 @@ use std::{
use sum_tree::Bias;
use text::BufferId;
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 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.;
@@ -426,6 +425,7 @@ impl EditorElement {
register_action(editor, window, Editor::copy_file_location);
register_action(editor, window, Editor::toggle_git_blame);
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_staged_selected_diff_hunks);
register_action(editor, window, Editor::stage_and_next);
@@ -1759,14 +1759,21 @@ impl EditorElement {
padding * em_width
};
+ let workspace = editor.workspace()?.downgrade();
let blame_entry = blame
.update(cx, |blame, cx| {
blame.blame_for_rows(&[*row_info], cx).next()
})
.flatten()?;
- let mut element =
- render_inline_blame_entry(self.editor.clone(), &blame, blame_entry, &self.style, cx);
+ let mut element = render_inline_blame_entry(
+ self.editor.clone(),
+ workspace,
+ &blame,
+ blame_entry,
+ &self.style,
+ cx,
+ )?;
let start_y = content_origin.y
+ 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 workspace = self.editor.read(cx).workspace()?;
let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
blame.blame_for_rows(buffer_rows, cx).collect()
});
@@ -1829,36 +1837,35 @@ impl EditorElement {
let start_x = em_width;
let mut last_used_color: Option<(PlayerColor, Oid)> = None;
+ let blame_renderer = cx.global::<GlobalBlameRenderer>().0.clone();
let shaped_lines = blamed_rows
.into_iter()
.enumerate()
.flat_map(|(ix, blame_entry)| {
- if let Some(blame_entry) = blame_entry {
- let mut element = render_blame_entry(
- ix,
- &blame,
- blame_entry,
- &self.style,
- &mut last_used_color,
- self.editor.clone(),
- cx,
- );
+ let mut element = render_blame_entry(
+ ix,
+ &blame,
+ blame_entry?,
+ &self.style,
+ &mut last_used_color,
+ self.editor.clone(),
+ workspace.clone(),
+ blame_renderer.clone(),
+ cx,
+ )?;
- let start_y = ix as f32 * line_height - (scroll_top % line_height);
- let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
+ let start_y = ix as f32 * line_height - (scroll_top % line_height);
+ let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
- element.prepaint_as_root(
- absolute_offset,
- size(width, AvailableSpace::MinContent),
- window,
- cx,
- );
+ element.prepaint_as_root(
+ absolute_offset,
+ size(width, AvailableSpace::MinContent),
+ window,
+ cx,
+ );
- Some(element)
- } else {
- None
- }
+ Some(element)
})
.collect();
@@ -5725,61 +5732,43 @@ fn prepaint_gutter_button(
fn render_inline_blame_entry(
editor: Entity<Editor>,
- blame: &gpui::Entity<GitBlame>,
+ workspace: WeakEntity<Workspace>,
+ blame: &Entity<GitBlame>,
blame_entry: BlameEntry,
style: &EditorStyle,
cx: &mut App,
-) -> 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),
- };
- 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()
+) -> Option<AnyElement> {
+ let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
+ let blame = blame.read(cx);
+ let details = blame.details_for_entry(&blame_entry);
+ let repository = blame.repository(cx)?.clone();
+ renderer.render_inline_blame_entry(
+ &style.text,
+ blame_entry,
+ details,
+ repository,
+ workspace,
+ editor,
+ cx,
+ )
}
fn render_blame_entry(
ix: usize,
- blame: &gpui::Entity<GitBlame>,
+ blame: &Entity<GitBlame>,
blame_entry: BlameEntry,
style: &EditorStyle,
last_used_color: &mut Option<(PlayerColor, Oid)>,
editor: Entity<Editor>,
+ workspace: Entity<Workspace>,
+ renderer: Arc<dyn BlameRenderer>,
cx: &mut App,
-) -> AnyElement {
+) -> Option<AnyElement> {
let mut sha_color = cx
.theme()
.players()
.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
// the commit SHAs are different, then we try again to get a different color.
match *last_used_color {
@@ -5791,97 +5780,20 @@ fn render_blame_entry(
};
last_used_color.replace((sha_color, blame_entry.sha));
- 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);
- let details = blame.read(cx).details_for_entry(&blame_entry);
-
- h_flex()
- .w_full()
- .justify_between()
- .font_family(style.text.font().family)
- .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,
- );
- }
- })
- .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();
- });
+ let blame = blame.read(cx);
+ let details = blame.details_for_entry(&blame_entry);
+ let repository = blame.repository(cx)?;
+ renderer.render_blame_entry(
+ &style.text,
+ blame_entry,
+ details,
+ repository,
+ workspace.downgrade(),
+ editor,
+ ix,
+ sha_color.cursor,
+ cx,
+ )
}
#[derive(Debug)]
@@ -6588,9 +6500,9 @@ impl Element for EditorElement {
window.with_rem_size(rem_size, |window| {
window.with_text_style(Some(text_style), |window| {
window.with_content_mask(Some(ContentMask { bounds }), |window| {
- let mut snapshot = self
- .editor
- .update(cx, |editor, cx| editor.snapshot(window, cx));
+ let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
+ (editor.snapshot(window, cx), editor.read_only(cx))
+ });
let style = self.style.clone();
let font_id = window.text_system().resolve_font(&style.text.font());
@@ -6970,11 +6882,12 @@ impl Element for EditorElement {
.flatten()?;
let mut element = render_inline_blame_entry(
self.editor.clone(),
+ editor.workspace()?.downgrade(),
blame,
blame_entry,
&style,
cx,
- );
+ )?;
let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance;
Some(
element
@@ -7507,19 +7420,23 @@ impl Element for EditorElement {
editor.last_position_map = Some(position_map.clone())
});
- let diff_hunk_controls = self.layout_diff_hunk_controls(
- start_row..end_row,
- &row_infos,
- &text_hitbox,
- &position_map,
- newest_selection_head,
- line_height,
- scroll_pixel_position,
- &display_hunks,
- self.editor.clone(),
- window,
- cx,
- );
+ let diff_hunk_controls = if is_read_only {
+ vec![]
+ } else {
+ self.layout_diff_hunk_controls(
+ start_row..end_row,
+ &row_infos,
+ &text_hitbox,
+ &position_map,
+ newest_selection_head,
+ line_height,
+ scroll_pixel_position,
+ &display_hunks,
+ self.editor.clone(),
+ window,
+ cx,
+ )
+ };
EditorLayout {
mode,
@@ -1,22 +1,22 @@
+use crate::Editor;
use anyhow::Result;
use collections::HashMap;
use git::{
- GitHostingProvider, GitHostingProviderRegistry, Oid,
- blame::{Blame, BlameEntry},
+ GitHostingProviderRegistry, GitRemote, Oid,
+ blame::{Blame, BlameEntry, ParsedCommitMessage},
parse_git_remote_url,
};
-use gpui::{App, AppContext as _, Context, Entity, Subscription, Task};
-use http_client::HttpClient;
+use gpui::{
+ AnyElement, App, AppContext as _, Context, Entity, Hsla, Subscription, Task, TextStyle,
+ WeakEntity, Window,
+};
use language::{Bias, Buffer, BufferSnapshot, Edit};
use multi_buffer::RowInfo;
-use project::{Project, ProjectItem};
+use project::{Project, ProjectItem, git_store::Repository};
use smallvec::SmallVec;
use std::{sync::Arc, time::Duration};
use sum_tree::SumTree;
-use ui::SharedString;
-use url::Url;
-
-use crate::commit_tooltip::ParsedCommitMessage;
+use workspace::Workspace;
#[derive(Clone, Debug, Default)]
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 {
project: Entity<Project>,
buffer: Entity<Buffer>,
entries: SumTree<GitBlameEntry>,
- commit_details: HashMap<Oid, crate::commit_tooltip::ParsedCommitMessage>,
+ commit_details: HashMap<Oid, ParsedCommitMessage>,
buffer_snapshot: BufferSnapshot,
buffer_edits: text::Subscription,
task: Task<Result<()>>,
@@ -109,6 +75,91 @@ pub struct GitBlame {
_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 {
pub fn new(
buffer: Entity<Buffer>,
@@ -181,6 +232,15 @@ impl GitBlame {
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 {
self.generated
}
@@ -109,7 +109,7 @@ impl ProposedChangesEditor {
let diff =
this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
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<_>>()
@@ -185,7 +185,7 @@ impl ProposedChangesEditor {
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
new_diffs.push(cx.new(|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(),
branch_buffer.read(cx).text_snapshot(),
cx,
@@ -111,6 +111,14 @@ impl GitRepository for FakeGitRepository {
.boxed()
}
+ fn load_commit(
+ &self,
+ _commit: String,
+ _cx: AsyncApp,
+ ) -> BoxFuture<Result<git::repository::CommitDiff>> {
+ unimplemented!()
+ }
+
fn set_index_text(
&self,
path: RepoPath,
@@ -1,8 +1,9 @@
-use crate::Oid;
use crate::commit::get_messages;
+use crate::{GitRemote, Oid};
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
use futures::AsyncWriteExt;
+use gpui::SharedString;
use serde::{Deserialize, Serialize};
use std::process::Stdio;
use std::{ops::Range, path::Path};
@@ -20,6 +21,14 @@ pub struct Blame {
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 {
pub async fn for_path(
git_binary: &Path,
@@ -15,6 +15,41 @@ pub struct PullRequest {
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 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 anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
use futures::future::BoxFuture;
use futures::{AsyncWriteExt, FutureExt as _, select_biased};
use git2::BranchType;
-use gpui::{AsyncApp, BackgroundExecutor, SharedString};
+use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, SharedString};
use parking_lot::Mutex;
use rope::Rope;
use schemars::JsonSchema;
use serde::Deserialize;
use std::borrow::{Borrow, Cow};
use std::ffi::{OsStr, OsString};
-use std::future;
use std::path::Component;
use std::process::{ExitStatus, Stdio};
use std::sync::LazyLock;
@@ -21,6 +21,10 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
+use std::{
+ future,
+ io::{BufRead, BufReader, BufWriter, Read},
+};
use sum_tree::MapSeekTarget;
use thiserror::Error;
use util::ResultExt;
@@ -133,6 +137,18 @@ pub struct CommitDetails {
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 {
pub fn short_sha(&self) -> SharedString {
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 load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>>;
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
@@ -405,6 +422,108 @@ impl GitRepository for RealGitRepository {
.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(
&self,
commit: String,
@@ -21,6 +21,7 @@ anyhow.workspace = true
askpass.workspace = true
assistant_settings.workspace = true
buffer_diff.workspace = true
+chrono.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
@@ -36,6 +37,7 @@ language_model.workspace = true
linkify.workspace = true
linkme.workspace = true
log.workspace = true
+markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
notifications.workspace = true
@@ -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 git::PullRequest;
use git::blame::BlameEntry;
+use git::repository::CommitSummary;
+use git::{GitRemote, blame::ParsedCommitMessage};
use gpui::{
App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
- StatefulInteractiveElement,
+ StatefulInteractiveElement, WeakEntity, prelude::*,
};
use markdown::Markdown;
+use project::git_store::Repository;
use settings::Settings;
use std::hash::Hash;
use theme::ThemeSettings;
use time::{OffsetDateTime, UtcOffset};
use time_format::format_local_timestamp;
use ui::{Avatar, Divider, IconButtonShape, prelude::*, tooltip_container};
-use url::Url;
-
-use crate::git::blame::GitRemote;
-use crate::hover_popover::hover_markdown_style;
+use workspace::Workspace;
#[derive(Clone, Debug)]
pub struct CommitDetails {
@@ -26,14 +27,6 @@ pub struct CommitDetails {
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> {
commit: &'a CommitDetails,
}
@@ -54,10 +47,10 @@ impl<'a> CommitAvatar<'a> {
.commit
.message
.as_ref()
- .and_then(|details| details.remote.as_ref())
+ .and_then(|details| details.remote.clone())
.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) {
// Loading or no avatar found
@@ -115,12 +108,16 @@ pub struct CommitTooltip {
commit: CommitDetails,
scroll_handle: ScrollHandle,
markdown: Entity<Markdown>,
+ repository: Entity<Repository>,
+ workspace: WeakEntity<Workspace>,
}
impl CommitTooltip {
pub fn blame_entry(
blame: &BlameEntry,
details: Option<ParsedCommitMessage>,
+ repository: Entity<Repository>,
+ workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -141,12 +138,20 @@ impl CommitTooltip {
author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(),
message: details,
},
+ repository,
+ workspace,
window,
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);
if let Some(code_block) = &style.code_block.text {
style.base_text_style.refine(code_block);
@@ -166,6 +171,8 @@ impl CommitTooltip {
});
Self {
commit,
+ repository,
+ workspace,
scroll_handle: ScrollHandle::new(),
markdown,
}
@@ -208,6 +215,27 @@ impl Render for CommitTooltip {
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 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| {
this.occlude()
@@ -283,24 +311,16 @@ impl Render for CommitTooltip {
.icon(IconName::FileGit)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
- .disabled(
- self.commit
- .message
- .as_ref()
- .map_or(true, |details| {
- details.permalink.is_none()
- }),
- )
- .when_some(
- self.commit
- .message
- .as_ref()
- .and_then(|details| details.permalink.clone()),
- |this, url| {
- this.on_click(move |_, _, cx| {
- cx.stop_propagation();
- cx.open_url(url.as_str())
- })
+ .on_click(
+ move |_, window, cx| {
+ CommitView::open(
+ commit_summary.clone(),
+ repo.downgrade(),
+ workspace.clone(),
+ window,
+ cx,
+ );
+ cx.stop_propagation();
},
),
)
@@ -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::commit_modal::CommitModal;
+use crate::commit_tooltip::CommitTooltip;
+use crate::commit_view::CommitView;
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::{ProjectDiff, picker_prompt, project_diff};
-use crate::{branch_picker, render_remote_button};
+use crate::{branch_picker, picker_prompt, render_remote_button};
use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
@@ -13,13 +13,13 @@ use anyhow::Result;
use askpass::AskPassDelegate;
use assistant_settings::AssistantSettings;
use db::kvp::KEY_VALUE_STORE;
-use editor::commit_tooltip::CommitTooltip;
use editor::{
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
scroll::ScrollbarAutoHide,
};
use futures::StreamExt as _;
+use git::blame::ParsedCommitMessage;
use git::repository::{
Branch, CommitDetails, CommitSummary, DiffType, PushOptions, Remote, RemoteCommandOutput,
ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
@@ -3001,6 +3001,7 @@ impl GitPanel {
let active_repository = self.active_repository.as_ref()?;
let branch = active_repository.read(cx).current_branch()?;
let commit = branch.most_recent_commit.as_ref()?.clone();
+ let workspace = self.workspace.clone();
let this = cx.entity();
Some(
@@ -3023,14 +3024,31 @@ impl GitPanel {
.truncate(),
)
.id("commit-msg-hover")
- .hoverable_tooltip(move |window, cx| {
- GitPanelMessageTooltip::new(
- this.clone(),
- commit.sha.clone(),
- window,
- cx,
- )
- .into()
+ .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(
+ this.clone(),
+ commit.sha.clone(),
+ repo.clone(),
+ window,
+ cx,
+ )
+ .into()
+ }
}),
)
.child(div().flex_1())
@@ -3938,31 +3956,35 @@ impl GitPanelMessageTooltip {
fn new(
git_panel: Entity<GitPanel>,
sha: SharedString,
+ repository: Entity<Repository>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
cx.new(|cx| {
cx.spawn_in(window, async move |this, cx| {
- let details = git_panel
- .update(cx, |git_panel, cx| {
- git_panel.load_commit_details(sha.to_string(), cx)
- })?
- .await?;
+ let (details, workspace) = git_panel.update(cx, |git_panel, cx| {
+ (
+ git_panel.load_commit_details(sha.to_string(), cx),
+ git_panel.workspace.clone(),
+ )
+ })?;
+ let details = details.await?;
- let commit_details = editor::commit_tooltip::CommitDetails {
+ let commit_details = crate::commit_tooltip::CommitDetails {
sha: details.sha.clone(),
author_name: details.committer_name.clone(),
author_email: details.committer_email.clone(),
commit_time: OffsetDateTime::from_unix_timestamp(details.commit_timestamp)?,
- message: Some(editor::commit_tooltip::ParsedCommitMessage {
+ message: Some(ParsedCommitMessage {
message: details.message.clone(),
..Default::default()
}),
};
this.update_in(cx, |this: &mut GitPanelMessageTooltip, window, cx| {
- this.commit_tooltip =
- Some(cx.new(move |cx| CommitTooltip::new(commit_details, window, cx)));
+ this.commit_tooltip = Some(cx.new(move |cx| {
+ CommitTooltip::new(commit_details, repository, workspace, window, cx)
+ }));
cx.notify();
})
})
@@ -3,6 +3,7 @@ use std::any::Any;
use ::settings::Settings;
use command_palette_hooks::CommandPaletteFilter;
use commit_modal::CommitModal;
+mod blame_ui;
use git::{
repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus},
status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
@@ -17,6 +18,8 @@ use workspace::Workspace;
mod askpass_modal;
pub mod branch_picker;
mod commit_modal;
+pub mod commit_tooltip;
+mod commit_view;
pub mod git_panel;
mod git_panel_settings;
pub mod onboarding;
@@ -30,6 +33,8 @@ actions!(git, [ResetOnboarding]);
pub fn init(cx: &mut App) {
GitPanelSettings::register(cx);
+ editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
+
cx.observe_new(|workspace: &mut Workspace, _, cx| {
ProjectDiff::register(workspace, cx);
CommitModal::register(workspace);
@@ -9,7 +9,7 @@ use serde_json::Value;
use std::{ops::Range, str::FromStr, sync::Arc};
use text::*;
-pub use proto::{BufferState, Operation};
+pub use proto::{BufferState, File, Operation};
/// Deserializes a `[text::LineEnding]` from the RPC representation.
pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding {
@@ -21,8 +21,8 @@ use git::{
blame::Blame,
parse_git_remote_url,
repository::{
- Branch, CommitDetails, DiffType, GitRepository, GitRepositoryCheckpoint, PushOptions,
- Remote, RemoteCommandOutput, RepoPath, ResetMode,
+ Branch, CommitDetails, CommitDiff, CommitFile, DiffType, GitRepository,
+ GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, ResetMode,
},
status::FileStatus,
};
@@ -289,6 +289,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_commit);
client.add_entity_request_handler(Self::handle_reset);
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_open_commit_message_buffer);
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(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitReset>,
@@ -2389,7 +2416,10 @@ impl BufferDiffState {
unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
{
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 {
None
@@ -2398,14 +2428,11 @@ impl BufferDiffState {
if let Some((uncommitted_diff, new_uncommitted_diff)) =
uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
{
- uncommitted_diff.update(cx, |uncommitted_diff, cx| {
- uncommitted_diff.set_snapshot(
- &buffer,
- new_uncommitted_diff,
- language_changed,
- unstaged_changed_range,
- cx,
- );
+ uncommitted_diff.update(cx, |diff, cx| {
+ if language_changed {
+ diff.language_changed(cx);
+ }
+ diff.set_snapshot(new_uncommitted_diff, &buffer, 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>> {
Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
}
@@ -365,6 +365,9 @@ message Envelope {
LanguageServerIdForName language_server_id_for_name = 332;
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;
@@ -3365,6 +3368,23 @@ message GitCommitDetails {
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 {
uint64 project_id = 1;
reserved 2;
@@ -340,6 +340,8 @@ messages!(
(ListRemoteDirectoryResponse, Background),
(ListToolchains, Foreground),
(ListToolchainsResponse, Foreground),
+ (LoadCommitDiff, Foreground),
+ (LoadCommitDiffResponse, Foreground),
(LspExtExpandMacro, Background),
(LspExtExpandMacroResponse, Background),
(LspExtOpenDocs, Background),
@@ -534,6 +536,7 @@ request_messages!(
(JoinRoom, JoinRoomResponse),
(LeaveChannelBuffer, Ack),
(LeaveRoom, Ack),
+ (LoadCommitDiff, LoadCommitDiffResponse),
(MarkNotificationRead, Ack),
(MoveChannel, Ack),
(OnTypeFormatting, OnTypeFormattingResponse),
@@ -668,6 +671,7 @@ entity_messages!(
JoinProject,
LeaveProject,
LinkedEditingRange,
+ LoadCommitDiff,
MultiLspQuery,
RestartLanguageServers,
OnTypeFormatting,
@@ -81,6 +81,7 @@ impl MultibufferHint {
if active_pane_item.is_singleton(cx)
|| active_pane_item.breadcrumbs(cx.theme(), cx).is_none()
+ || !active_pane_item.can_save(cx)
{
return ToolbarItemLocation::Hidden;
}