Merge pull request #1632 from zed-industries/git-gutter
Tracking PR: Git gutter
This commit is contained in:
commit
836b536a90
34 changed files with 1687 additions and 73 deletions
52
Cargo.lock
generated
52
Cargo.lock
generated
|
@ -1031,6 +1031,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"envy",
|
||||
"futures",
|
||||
"git",
|
||||
"gpui",
|
||||
"hyper",
|
||||
"language",
|
||||
|
@ -1061,6 +1062,7 @@ dependencies = [
|
|||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
@ -1697,6 +1699,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools",
|
||||
|
@ -2224,6 +2227,39 @@ dependencies = [
|
|||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"clock",
|
||||
"collections",
|
||||
"futures",
|
||||
"git2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
"smol",
|
||||
"sum_tree",
|
||||
"text",
|
||||
"unindent",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
|
@ -2840,6 +2876,7 @@ dependencies = [
|
|||
"env_logger",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
@ -2894,6 +2931,18 @@ version = "0.2.126"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.14.0+1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libz-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.7.3"
|
||||
|
@ -3970,6 +4019,7 @@ dependencies = [
|
|||
"fsevent",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"ignore",
|
||||
"language",
|
||||
|
@ -6332,6 +6382,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
"git2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"serde_json",
|
||||
|
|
|
@ -74,6 +74,15 @@
|
|||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files"
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
// What shell to use when opening a terminal. May take 3 values:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[package]
|
||||
authors = ["Nathan Sobo <nathan@warp.dev>"]
|
||||
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
|
@ -26,6 +26,7 @@ base64 = "0.13"
|
|||
clap = { version = "3.1", features = ["derive"], optional = true }
|
||||
envy = "0.4.2"
|
||||
futures = "0.3"
|
||||
git = { path = "../git" }
|
||||
hyper = "0.14"
|
||||
lazy_static = "1.4"
|
||||
lipsum = { version = "0.8", optional = true }
|
||||
|
@ -65,11 +66,13 @@ project = { path = "../project", features = ["test-support"] }
|
|||
settings = { path = "../settings", features = ["test-support"] }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
git = { path = "../git", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.9"
|
||||
util = { path = "../util" }
|
||||
lazy_static = "1.4"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
unindent = "0.1"
|
||||
|
||||
[features]
|
||||
seed-support = ["clap", "lipsum", "reqwest"]
|
||||
|
|
|
@ -51,6 +51,7 @@ use std::{
|
|||
time::Duration,
|
||||
};
|
||||
use theme::ThemeRegistry;
|
||||
use unindent::Unindent as _;
|
||||
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
||||
|
||||
#[ctor::ctor]
|
||||
|
@ -946,6 +947,136 @@ async fn test_propagate_saves_and_fs_changes(
|
|||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_git_diff_base_change(
|
||||
executor: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/dir",
|
||||
json!({
|
||||
".git": {
|
||||
},
|
||||
"a.txt": "
|
||||
one
|
||||
two
|
||||
three
|
||||
".unindent(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let diff_base = "
|
||||
one
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let new_diff_base = "
|
||||
one
|
||||
two
|
||||
"
|
||||
.unindent();
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
|
||||
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||
|
||||
// Create the buffer
|
||||
let buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "/dir/a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait for it to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Create remote buffer
|
||||
let buffer_b = project_b
|
||||
.update(cx_b, |p, cx| p.open_buffer((worktree_id, "/dir/a.txt"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Wait remote buffer to catch up to the new diff
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test diffing
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "", "two\n")],
|
||||
);
|
||||
});
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.as_fake()
|
||||
.set_index_for_repo(
|
||||
Path::new("/dir/.git"),
|
||||
&[(Path::new("a.txt"), new_diff_base.clone())],
|
||||
)
|
||||
.await;
|
||||
|
||||
// Wait for buffer_a to receive it
|
||||
executor.run_until_parked();
|
||||
|
||||
// Smoke test new diffing
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
|
||||
// Smoke test B
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.diff_base(), Some(new_diff_base.as_ref()));
|
||||
git::diff::assert_hunks(
|
||||
buffer.snapshot().git_diff_hunks_in_range(0..4),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(2..3, "", "three\n")],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_fs_operations(
|
||||
executor: Arc<Deterministic>,
|
||||
|
|
|
@ -206,6 +206,7 @@ impl Server {
|
|||
.add_message_handler(Server::unfollow)
|
||||
.add_message_handler(Server::update_followers)
|
||||
.add_request_handler(Server::get_channel_messages)
|
||||
.add_message_handler(Server::update_diff_base)
|
||||
.add_request_handler(Server::get_private_user_info);
|
||||
|
||||
Arc::new(server)
|
||||
|
@ -1728,6 +1729,21 @@ impl Server {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_diff_base(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateDiffBase>,
|
||||
) -> Result<()> {
|
||||
let receiver_ids = self.store().await.project_connection_ids(
|
||||
ProjectId::from_proto(request.payload.project_id),
|
||||
request.sender_id,
|
||||
)?;
|
||||
broadcast(request.sender_id, receiver_ids, |connection_id| {
|
||||
self.peer
|
||||
.forward_send(request.sender_id, connection_id, request.payload.clone())
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_private_user_info(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::GetPrivateUserInfo>,
|
||||
|
|
|
@ -25,6 +25,7 @@ clock = { path = "../clock" }
|
|||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
|
|
|
@ -274,6 +274,7 @@ impl FoldMap {
|
|||
if buffer.edit_count() != new_buffer.edit_count()
|
||||
|| buffer.parse_count() != new_buffer.parse_count()
|
||||
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count()
|
||||
|| buffer.git_diff_update_count() != new_buffer.git_diff_update_count()
|
||||
|| buffer.trailing_excerpt_update_count()
|
||||
!= new_buffer.trailing_excerpt_update_count()
|
||||
{
|
||||
|
|
|
@ -16,6 +16,7 @@ use crate::{
|
|||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::*,
|
||||
|
@ -36,7 +37,7 @@ use gpui::{
|
|||
use json::json;
|
||||
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
||||
use project::ProjectPath;
|
||||
use settings::Settings;
|
||||
use settings::{GitGutter, Settings};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
|
@ -45,6 +46,7 @@ use std::{
|
|||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::DiffStyle;
|
||||
|
||||
struct SelectionLayout {
|
||||
head: DisplayPoint,
|
||||
|
@ -524,30 +526,143 @@ impl EditorElement {
|
|||
layout: &mut LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) {
|
||||
let scroll_top =
|
||||
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
||||
struct GutterLayout {
|
||||
line_height: f32,
|
||||
// scroll_position: Vector2F,
|
||||
scroll_top: f32,
|
||||
bounds: RectF,
|
||||
}
|
||||
|
||||
struct DiffLayout<'a> {
|
||||
buffer_row: u32,
|
||||
last_diff: Option<&'a DiffHunk<u32>>,
|
||||
}
|
||||
|
||||
fn diff_quad(
|
||||
hunk: &DiffHunk<u32>,
|
||||
gutter_layout: &GutterLayout,
|
||||
diff_style: &DiffStyle,
|
||||
) -> Quad {
|
||||
let color = match hunk.status() {
|
||||
DiffHunkStatus::Added => diff_style.inserted,
|
||||
DiffHunkStatus::Modified => diff_style.modified,
|
||||
|
||||
//TODO: This rendering is entirely a horrible hack
|
||||
DiffHunkStatus::Removed => {
|
||||
let row = hunk.buffer_range.start;
|
||||
|
||||
let offset = gutter_layout.line_height / 2.;
|
||||
let start_y =
|
||||
row as f32 * gutter_layout.line_height + offset - gutter_layout.scroll_top;
|
||||
let end_y = start_y + gutter_layout.line_height;
|
||||
|
||||
let width = diff_style.removed_width_em * gutter_layout.line_height;
|
||||
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
|
||||
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||
|
||||
return Quad {
|
||||
bounds: highlight_bounds,
|
||||
background: Some(diff_style.deleted),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: 1. * gutter_layout.line_height,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let start_row = hunk.buffer_range.start;
|
||||
let end_row = hunk.buffer_range.end;
|
||||
|
||||
let start_y = start_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||
let end_y = end_row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||
|
||||
let width = diff_style.width_em * gutter_layout.line_height;
|
||||
let highlight_origin = gutter_layout.bounds.origin() + vec2f(-width, start_y);
|
||||
let highlight_size = vec2f(width * 2., end_y - start_y);
|
||||
let highlight_bounds = RectF::new(highlight_origin, highlight_size);
|
||||
|
||||
Quad {
|
||||
bounds: highlight_bounds,
|
||||
background: Some(color),
|
||||
border: Border::new(0., Color::transparent_black()),
|
||||
corner_radius: diff_style.corner_radius * gutter_layout.line_height,
|
||||
}
|
||||
}
|
||||
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let gutter_layout = {
|
||||
let line_height = layout.position_map.line_height;
|
||||
GutterLayout {
|
||||
scroll_top: scroll_position.y() * line_height,
|
||||
line_height,
|
||||
bounds,
|
||||
}
|
||||
};
|
||||
|
||||
let mut diff_layout = DiffLayout {
|
||||
buffer_row: scroll_position.y() as u32,
|
||||
last_diff: None,
|
||||
};
|
||||
|
||||
let diff_style = &cx.global::<Settings>().theme.editor.diff.clone();
|
||||
let show_gutter = matches!(
|
||||
&cx.global::<Settings>()
|
||||
.git_overrides
|
||||
.git_gutter
|
||||
.unwrap_or_default(),
|
||||
GitGutter::TrackedFiles
|
||||
);
|
||||
|
||||
// line is `None` when there's a line wrap
|
||||
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
|
||||
if let Some(line) = line {
|
||||
let line_origin = bounds.origin()
|
||||
+ vec2f(
|
||||
bounds.width() - line.width() - layout.gutter_padding,
|
||||
ix as f32 * layout.position_map.line_height
|
||||
- (scroll_top % layout.position_map.line_height),
|
||||
);
|
||||
line.paint(
|
||||
line_origin,
|
||||
visible_bounds,
|
||||
layout.position_map.line_height,
|
||||
cx,
|
||||
ix as f32 * gutter_layout.line_height
|
||||
- (gutter_layout.scroll_top % gutter_layout.line_height),
|
||||
);
|
||||
|
||||
line.paint(line_origin, visible_bounds, gutter_layout.line_height, cx);
|
||||
|
||||
if show_gutter {
|
||||
//This line starts a buffer line, so let's do the diff calculation
|
||||
let new_hunk = get_hunk(diff_layout.buffer_row, &layout.diff_hunks);
|
||||
|
||||
let (is_ending, is_starting) = match (diff_layout.last_diff, new_hunk) {
|
||||
(Some(old_hunk), Some(new_hunk)) if new_hunk == old_hunk => {
|
||||
(false, false)
|
||||
}
|
||||
(a, b) => (a.is_some(), b.is_some()),
|
||||
};
|
||||
|
||||
if is_ending {
|
||||
let last_hunk = diff_layout.last_diff.take().unwrap();
|
||||
cx.scene
|
||||
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style));
|
||||
}
|
||||
|
||||
if is_starting {
|
||||
let new_hunk = new_hunk.unwrap();
|
||||
diff_layout.last_diff = Some(new_hunk);
|
||||
};
|
||||
|
||||
diff_layout.buffer_row += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we ran out with a diff hunk still being prepped, paint it now
|
||||
if let Some(last_hunk) = diff_layout.last_diff {
|
||||
cx.scene
|
||||
.push_quad(diff_quad(last_hunk, &gutter_layout, diff_style))
|
||||
}
|
||||
|
||||
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
||||
let mut x = bounds.width() - layout.gutter_padding;
|
||||
let mut y = *row as f32 * layout.position_map.line_height - scroll_top;
|
||||
let mut y = *row as f32 * gutter_layout.line_height - gutter_layout.scroll_top;
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
|
||||
y += (layout.position_map.line_height - indicator.size().y()) / 2.;
|
||||
y += (gutter_layout.line_height - indicator.size().y()) / 2.;
|
||||
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||
}
|
||||
}
|
||||
|
@ -1252,6 +1367,27 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the hunk that contains buffer_line, starting from start_idx
|
||||
/// Returns none if there is none found, and
|
||||
fn get_hunk(buffer_line: u32, hunks: &[DiffHunk<u32>]) -> Option<&DiffHunk<u32>> {
|
||||
for i in 0..hunks.len() {
|
||||
// Safety: Index out of bounds is handled by the check above
|
||||
let hunk = hunks.get(i).unwrap();
|
||||
if hunk.buffer_range.contains(&(buffer_line as u32)) {
|
||||
return Some(hunk);
|
||||
} else if hunk.status() == DiffHunkStatus::Removed && buffer_line == hunk.buffer_range.start
|
||||
{
|
||||
return Some(hunk);
|
||||
} else if hunk.buffer_range.start > buffer_line as u32 {
|
||||
// If we've passed the buffer_line, just stop
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
// We reached the end of the array without finding a hunk, just return none.
|
||||
return None;
|
||||
}
|
||||
|
||||
impl Element for EditorElement {
|
||||
type LayoutState = LayoutState;
|
||||
type PaintState = ();
|
||||
|
@ -1425,6 +1561,11 @@ impl Element for EditorElement {
|
|||
let line_number_layouts =
|
||||
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
||||
|
||||
let diff_hunks = snapshot
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(start_row..end_row)
|
||||
.collect();
|
||||
|
||||
let mut max_visible_line_width = 0.0;
|
||||
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
||||
for line in &line_layouts {
|
||||
|
@ -1573,6 +1714,7 @@ impl Element for EditorElement {
|
|||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
line_number_layouts,
|
||||
diff_hunks,
|
||||
blocks,
|
||||
selections,
|
||||
context_menu,
|
||||
|
@ -1710,6 +1852,7 @@ pub struct LayoutState {
|
|||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
diff_hunks: Vec<DiffHunk<u32>>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
||||
}
|
||||
|
|
|
@ -478,6 +478,17 @@ impl Item for Editor {
|
|||
})
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
_project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.buffer().update(cx, |multibuffer, cx| {
|
||||
multibuffer.git_diff_recalc(cx);
|
||||
});
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
|
||||
let mut result = Vec::new();
|
||||
match event {
|
||||
|
|
|
@ -4,6 +4,7 @@ pub use anchor::{Anchor, AnchorRangeExt};
|
|||
use anyhow::Result;
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||
use git::diff::DiffHunk;
|
||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
pub use language::Completion;
|
||||
use language::{
|
||||
|
@ -90,6 +91,7 @@ struct BufferState {
|
|||
last_selections_update_count: usize,
|
||||
last_diagnostics_update_count: usize,
|
||||
last_file_update_count: usize,
|
||||
last_git_diff_update_count: usize,
|
||||
excerpts: Vec<ExcerptId>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
@ -101,6 +103,7 @@ pub struct MultiBufferSnapshot {
|
|||
parse_count: usize,
|
||||
diagnostics_update_count: usize,
|
||||
trailing_excerpt_update_count: usize,
|
||||
git_diff_update_count: usize,
|
||||
edit_count: usize,
|
||||
is_dirty: bool,
|
||||
has_conflict: bool,
|
||||
|
@ -202,6 +205,7 @@ impl MultiBuffer {
|
|||
last_selections_update_count: buffer_state.last_selections_update_count,
|
||||
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
|
||||
last_file_update_count: buffer_state.last_file_update_count,
|
||||
last_git_diff_update_count: buffer_state.last_git_diff_update_count,
|
||||
excerpts: buffer_state.excerpts.clone(),
|
||||
_subscriptions: [
|
||||
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
||||
|
@ -308,6 +312,17 @@ impl MultiBuffer {
|
|||
self.read(cx).symbols_containing(offset, theme)
|
||||
}
|
||||
|
||||
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let buffers = self.buffers.borrow();
|
||||
for buffer_state in buffers.values() {
|
||||
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
|
||||
buffer_state
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit<I, S, T>(
|
||||
&mut self,
|
||||
edits: I,
|
||||
|
@ -827,6 +842,7 @@ impl MultiBuffer {
|
|||
last_selections_update_count: buffer_snapshot.selections_update_count(),
|
||||
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
|
||||
last_file_update_count: buffer_snapshot.file_update_count(),
|
||||
last_git_diff_update_count: buffer_snapshot.git_diff_update_count(),
|
||||
excerpts: Default::default(),
|
||||
_subscriptions: [
|
||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||
|
@ -1249,6 +1265,7 @@ impl MultiBuffer {
|
|||
let mut excerpts_to_edit = Vec::new();
|
||||
let mut reparsed = false;
|
||||
let mut diagnostics_updated = false;
|
||||
let mut git_diff_updated = false;
|
||||
let mut is_dirty = false;
|
||||
let mut has_conflict = false;
|
||||
let mut edited = false;
|
||||
|
@ -1260,6 +1277,7 @@ impl MultiBuffer {
|
|||
let selections_update_count = buffer.selections_update_count();
|
||||
let diagnostics_update_count = buffer.diagnostics_update_count();
|
||||
let file_update_count = buffer.file_update_count();
|
||||
let git_diff_update_count = buffer.git_diff_update_count();
|
||||
|
||||
let buffer_edited = version.changed_since(&buffer_state.last_version);
|
||||
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
|
||||
|
@ -1268,17 +1286,21 @@ impl MultiBuffer {
|
|||
let buffer_diagnostics_updated =
|
||||
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
|
||||
let buffer_file_updated = file_update_count > buffer_state.last_file_update_count;
|
||||
let buffer_git_diff_updated =
|
||||
git_diff_update_count > buffer_state.last_git_diff_update_count;
|
||||
if buffer_edited
|
||||
|| buffer_reparsed
|
||||
|| buffer_selections_updated
|
||||
|| buffer_diagnostics_updated
|
||||
|| buffer_file_updated
|
||||
|| buffer_git_diff_updated
|
||||
{
|
||||
buffer_state.last_version = version;
|
||||
buffer_state.last_parse_count = parse_count;
|
||||
buffer_state.last_selections_update_count = selections_update_count;
|
||||
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
|
||||
buffer_state.last_file_update_count = file_update_count;
|
||||
buffer_state.last_git_diff_update_count = git_diff_update_count;
|
||||
excerpts_to_edit.extend(
|
||||
buffer_state
|
||||
.excerpts
|
||||
|
@ -1290,6 +1312,7 @@ impl MultiBuffer {
|
|||
edited |= buffer_edited;
|
||||
reparsed |= buffer_reparsed;
|
||||
diagnostics_updated |= buffer_diagnostics_updated;
|
||||
git_diff_updated |= buffer_git_diff_updated;
|
||||
is_dirty |= buffer.is_dirty();
|
||||
has_conflict |= buffer.has_conflict();
|
||||
}
|
||||
|
@ -1302,6 +1325,9 @@ impl MultiBuffer {
|
|||
if diagnostics_updated {
|
||||
snapshot.diagnostics_update_count += 1;
|
||||
}
|
||||
if git_diff_updated {
|
||||
snapshot.git_diff_update_count += 1;
|
||||
}
|
||||
snapshot.is_dirty = is_dirty;
|
||||
snapshot.has_conflict = has_conflict;
|
||||
|
||||
|
@ -2479,6 +2505,10 @@ impl MultiBufferSnapshot {
|
|||
self.diagnostics_update_count
|
||||
}
|
||||
|
||||
pub fn git_diff_update_count(&self) -> usize {
|
||||
self.git_diff_update_count
|
||||
}
|
||||
|
||||
pub fn trailing_excerpt_update_count(&self) -> usize {
|
||||
self.trailing_excerpt_update_count
|
||||
}
|
||||
|
@ -2529,6 +2559,15 @@ impl MultiBufferSnapshot {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range<'a>(
|
||||
&'a self,
|
||||
row_range: Range<u32>,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
self.as_singleton()
|
||||
.into_iter()
|
||||
.flat_map(move |(_, _, buffer)| buffer.git_diff_hunks_in_range(row_range.clone()))
|
||||
}
|
||||
|
||||
pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
|
||||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
|
||||
|
|
28
crates/git/Cargo.toml
Normal file
28
crates/git/Cargo.toml
Normal file
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "git"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/git.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
clock = { path = "../clock" }
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
lazy_static = "1.4.0"
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
text = { path = "../text" }
|
||||
collections = { path = "../collections" }
|
||||
util = { path = "../util" }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
smol = "1.2"
|
||||
parking_lot = "0.11.1"
|
||||
async-trait = "0.1"
|
||||
futures = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
unindent = "0.1.7"
|
||||
|
||||
[features]
|
||||
test-support = []
|
354
crates/git/src/diff.rs
Normal file
354
crates/git/src/diff.rs
Normal file
|
@ -0,0 +1,354 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point};
|
||||
|
||||
pub use git2 as libgit;
|
||||
use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DiffHunkStatus {
|
||||
Added,
|
||||
Modified,
|
||||
Removed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DiffHunk<T> {
|
||||
pub buffer_range: Range<T>,
|
||||
pub head_byte_range: Range<usize>,
|
||||
}
|
||||
|
||||
impl DiffHunk<u32> {
|
||||
pub fn status(&self) -> DiffHunkStatus {
|
||||
if self.head_byte_range.is_empty() {
|
||||
DiffHunkStatus::Added
|
||||
} else if self.buffer_range.is_empty() {
|
||||
DiffHunkStatus::Removed
|
||||
} else {
|
||||
DiffHunkStatus::Modified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for DiffHunk<Anchor> {
|
||||
type Summary = DiffHunkSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
DiffHunkSummary {
|
||||
buffer_range: self.buffer_range.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DiffHunkSummary {
|
||||
buffer_range: Range<Anchor>,
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for DiffHunkSummary {
|
||||
type Context = text::BufferSnapshot;
|
||||
|
||||
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
|
||||
self.buffer_range.start = self
|
||||
.buffer_range
|
||||
.start
|
||||
.min(&other.buffer_range.start, buffer);
|
||||
self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BufferDiff {
|
||||
last_buffer_version: Option<clock::Global>,
|
||||
tree: SumTree<DiffHunk<Anchor>>,
|
||||
}
|
||||
|
||||
impl BufferDiff {
|
||||
pub fn new() -> BufferDiff {
|
||||
BufferDiff {
|
||||
last_buffer_version: None,
|
||||
tree: SumTree::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hunks_in_range<'a>(
|
||||
&'a self,
|
||||
query_row_range: Range<u32>,
|
||||
buffer: &'a BufferSnapshot,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let start = buffer.anchor_before(Point::new(query_row_range.start, 0));
|
||||
let end = buffer.anchor_after(Point::new(query_row_range.end, 0));
|
||||
|
||||
let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| {
|
||||
let before_start = summary.buffer_range.end.cmp(&start, buffer).is_lt();
|
||||
let after_end = summary.buffer_range.start.cmp(&end, buffer).is_gt();
|
||||
!before_start && !after_end
|
||||
});
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
cursor.next(buffer);
|
||||
let hunk = cursor.item()?;
|
||||
|
||||
let range = hunk.buffer_range.to_point(buffer);
|
||||
let end_row = if range.end.column > 0 {
|
||||
range.end.row + 1
|
||||
} else {
|
||||
range.end.row
|
||||
};
|
||||
|
||||
Some(DiffHunk {
|
||||
buffer_range: range.start.row..end_row,
|
||||
head_byte_range: hunk.head_byte_range.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn needs_update(&self, buffer: &text::BufferSnapshot) -> bool {
|
||||
match &self.last_buffer_version {
|
||||
Some(last) => buffer.version().changed_since(last),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) {
|
||||
let mut tree = SumTree::new();
|
||||
|
||||
let buffer_text = buffer.as_rope().to_string();
|
||||
let patch = Self::diff(&diff_base, &buffer_text);
|
||||
|
||||
if let Some(patch) = patch {
|
||||
let mut divergence = 0;
|
||||
for hunk_index in 0..patch.num_hunks() {
|
||||
let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence);
|
||||
tree.push(hunk, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
self.tree = tree;
|
||||
self.last_buffer_version = Some(buffer.version().clone());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
self.hunks_in_range(0..u32::MAX, text)
|
||||
}
|
||||
|
||||
fn diff<'a>(head: &'a str, current: &'a str) -> Option<GitPatch<'a>> {
|
||||
let mut options = GitOptions::default();
|
||||
options.context_lines(0);
|
||||
|
||||
let patch = GitPatch::from_buffers(
|
||||
head.as_bytes(),
|
||||
None,
|
||||
current.as_bytes(),
|
||||
None,
|
||||
Some(&mut options),
|
||||
);
|
||||
|
||||
match patch {
|
||||
Ok(patch) => Some(patch),
|
||||
|
||||
Err(err) => {
|
||||
log::error!("`GitPatch::from_buffers` failed: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_patch_hunk<'a>(
|
||||
patch: &GitPatch<'a>,
|
||||
hunk_index: usize,
|
||||
buffer: &text::BufferSnapshot,
|
||||
buffer_row_divergence: &mut i64,
|
||||
) -> DiffHunk<Anchor> {
|
||||
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
|
||||
assert!(line_item_count > 0);
|
||||
|
||||
let mut first_deletion_buffer_row: Option<u32> = None;
|
||||
let mut buffer_row_range: Option<Range<u32>> = None;
|
||||
let mut head_byte_range: Option<Range<usize>> = None;
|
||||
|
||||
for line_index in 0..line_item_count {
|
||||
let line = patch.line_in_hunk(hunk_index, line_index).unwrap();
|
||||
let kind = line.origin_value();
|
||||
let content_offset = line.content_offset() as isize;
|
||||
let content_len = line.content().len() as isize;
|
||||
|
||||
if kind == GitDiffLineType::Addition {
|
||||
*buffer_row_divergence += 1;
|
||||
let row = line.new_lineno().unwrap().saturating_sub(1);
|
||||
|
||||
match &mut buffer_row_range {
|
||||
Some(buffer_row_range) => buffer_row_range.end = row + 1,
|
||||
None => buffer_row_range = Some(row..row + 1),
|
||||
}
|
||||
}
|
||||
|
||||
if kind == GitDiffLineType::Deletion {
|
||||
*buffer_row_divergence -= 1;
|
||||
let end = content_offset + content_len;
|
||||
|
||||
match &mut head_byte_range {
|
||||
Some(head_byte_range) => head_byte_range.end = end as usize,
|
||||
None => head_byte_range = Some(content_offset as usize..end as usize),
|
||||
}
|
||||
|
||||
if first_deletion_buffer_row.is_none() {
|
||||
let old_row = line.old_lineno().unwrap().saturating_sub(1);
|
||||
let row = old_row as i64 + *buffer_row_divergence;
|
||||
first_deletion_buffer_row = Some(row as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//unwrap_or deletion without addition
|
||||
let buffer_row_range = buffer_row_range.unwrap_or_else(|| {
|
||||
//we cannot have an addition-less hunk without deletion(s) or else there would be no hunk
|
||||
let row = first_deletion_buffer_row.unwrap();
|
||||
row..row
|
||||
});
|
||||
|
||||
//unwrap_or addition without deletion
|
||||
let head_byte_range = head_byte_range.unwrap_or(0..0);
|
||||
|
||||
let start = Point::new(buffer_row_range.start, 0);
|
||||
let end = Point::new(buffer_row_range.end, 0);
|
||||
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||
DiffHunk {
|
||||
buffer_range,
|
||||
head_byte_range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Range (crossing new lines), old, new
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[track_caller]
|
||||
pub fn assert_hunks<Iter>(
|
||||
diff_hunks: Iter,
|
||||
buffer: &BufferSnapshot,
|
||||
diff_base: &str,
|
||||
expected_hunks: &[(Range<u32>, &str, &str)],
|
||||
) where
|
||||
Iter: Iterator<Item = DiffHunk<u32>>,
|
||||
{
|
||||
let actual_hunks = diff_hunks
|
||||
.map(|hunk| {
|
||||
(
|
||||
hunk.buffer_range.clone(),
|
||||
&diff_base[hunk.head_byte_range],
|
||||
buffer
|
||||
.text_for_range(
|
||||
Point::new(hunk.buffer_range.start, 0)
|
||||
..Point::new(hunk.buffer_range.end, 0),
|
||||
)
|
||||
.collect::<String>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expected_hunks: Vec<_> = expected_hunks
|
||||
.iter()
|
||||
.map(|(r, s, h)| (r.clone(), *s, h.to_string()))
|
||||
.collect();
|
||||
|
||||
assert_eq!(actual_hunks, expected_hunks);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use text::Buffer;
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[test]
|
||||
fn test_buffer_diff_simple() {
|
||||
let diff_base = "
|
||||
one
|
||||
two
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let buffer_text = "
|
||||
one
|
||||
HELLO
|
||||
three
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let mut buffer = Buffer::new(0, 0, buffer_text);
|
||||
let mut diff = BufferDiff::new();
|
||||
smol::block_on(diff.update(&diff_base, &buffer));
|
||||
assert_hunks(
|
||||
diff.hunks(&buffer),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(1..2, "two\n", "HELLO\n")],
|
||||
);
|
||||
|
||||
buffer.edit([(0..0, "point five\n")]);
|
||||
smol::block_on(diff.update(&diff_base, &buffer));
|
||||
assert_hunks(
|
||||
diff.hunks(&buffer),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_diff_range() {
|
||||
let diff_base = "
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let buffer_text = "
|
||||
A
|
||||
one
|
||||
B
|
||||
two
|
||||
C
|
||||
three
|
||||
HELLO
|
||||
four
|
||||
five
|
||||
SIXTEEN
|
||||
seven
|
||||
eight
|
||||
WORLD
|
||||
nine
|
||||
|
||||
ten
|
||||
|
||||
"
|
||||
.unindent();
|
||||
|
||||
let buffer = Buffer::new(0, 0, buffer_text);
|
||||
let mut diff = BufferDiff::new();
|
||||
smol::block_on(diff.update(&diff_base, &buffer));
|
||||
assert_eq!(diff.hunks(&buffer).count(), 8);
|
||||
|
||||
assert_hunks(
|
||||
diff.hunks_in_range(7..12, &buffer),
|
||||
&buffer,
|
||||
&diff_base,
|
||||
&[
|
||||
(6..7, "", "HELLO\n"),
|
||||
(9..10, "six\n", "SIXTEEN\n"),
|
||||
(12..13, "", "WORLD\n"),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
12
crates/git/src/git.rs
Normal file
12
crates/git/src/git.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use std::ffi::OsStr;
|
||||
|
||||
pub use git2 as libgit;
|
||||
pub use lazy_static::lazy_static;
|
||||
|
||||
pub mod diff;
|
||||
pub mod repository;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git");
|
||||
pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
||||
}
|
61
crates/git/src/repository.rs
Normal file
61
crates/git/src/repository.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub use git2::Repository as LibGitRepository;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait GitRepository: Send {
|
||||
fn load_index(&self, relative_file_path: &Path) -> Option<String>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl GitRepository for LibGitRepository {
|
||||
fn load_index(&self, relative_file_path: &Path) -> Option<String> {
|
||||
fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
|
||||
const STAGE_NORMAL: i32 = 0;
|
||||
let index = repo.index()?;
|
||||
let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
|
||||
Some(entry) => entry.id,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let content = repo.find_blob(oid)?.content().to_owned();
|
||||
Ok(Some(String::from_utf8(content)?))
|
||||
}
|
||||
|
||||
match logic(&self, relative_file_path) {
|
||||
Ok(value) => return value,
|
||||
Err(err) => log::error!("Error loading head text: {:?}", err),
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FakeGitRepository {
|
||||
state: Arc<Mutex<FakeGitRepositoryState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FakeGitRepositoryState {
|
||||
pub index_contents: HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
impl FakeGitRepository {
|
||||
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
|
||||
Arc::new(Mutex::new(FakeGitRepository { state }))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl GitRepository for FakeGitRepository {
|
||||
fn load_index(&self, path: &Path) -> Option<String> {
|
||||
let state = self.state.lock();
|
||||
state.index_contents.get(path).cloned()
|
||||
}
|
||||
}
|
|
@ -25,6 +25,7 @@ client = { path = "../client" }
|
|||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
lsp = { path = "../lsp" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
|
|
@ -45,8 +45,16 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
|
|||
|
||||
pub use lsp::DiagnosticSeverity;
|
||||
|
||||
struct GitDiffStatus {
|
||||
diff: git::diff::BufferDiff,
|
||||
update_in_progress: bool,
|
||||
update_requested: bool,
|
||||
}
|
||||
|
||||
pub struct Buffer {
|
||||
text: TextBuffer,
|
||||
diff_base: Option<String>,
|
||||
git_diff_status: GitDiffStatus,
|
||||
file: Option<Arc<dyn File>>,
|
||||
saved_version: clock::Global,
|
||||
saved_version_fingerprint: String,
|
||||
|
@ -66,6 +74,7 @@ pub struct Buffer {
|
|||
diagnostics_update_count: usize,
|
||||
diagnostics_timestamp: clock::Lamport,
|
||||
file_update_count: usize,
|
||||
git_diff_update_count: usize,
|
||||
completion_triggers: Vec<String>,
|
||||
completion_triggers_timestamp: clock::Lamport,
|
||||
deferred_ops: OperationQueue<Operation>,
|
||||
|
@ -73,11 +82,13 @@ pub struct Buffer {
|
|||
|
||||
pub struct BufferSnapshot {
|
||||
text: text::BufferSnapshot,
|
||||
pub git_diff: git::diff::BufferDiff,
|
||||
pub(crate) syntax: SyntaxSnapshot,
|
||||
file: Option<Arc<dyn File>>,
|
||||
diagnostics: DiagnosticSet,
|
||||
diagnostics_update_count: usize,
|
||||
file_update_count: usize,
|
||||
git_diff_update_count: usize,
|
||||
remote_selections: TreeMap<ReplicaId, SelectionSet>,
|
||||
selections_update_count: usize,
|
||||
language: Option<Arc<Language>>,
|
||||
|
@ -328,17 +339,20 @@ impl Buffer {
|
|||
Self::build(
|
||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn from_file<T: Into<String>>(
|
||||
replica_id: ReplicaId,
|
||||
base_text: T,
|
||||
diff_base: Option<T>,
|
||||
file: Arc<dyn File>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self::build(
|
||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||
diff_base.map(|h| h.into().into_boxed_str().into()),
|
||||
Some(file),
|
||||
)
|
||||
}
|
||||
|
@ -349,7 +363,11 @@ impl Buffer {
|
|||
file: Option<Arc<dyn File>>,
|
||||
) -> Result<Self> {
|
||||
let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
|
||||
let mut this = Self::build(buffer, file);
|
||||
let mut this = Self::build(
|
||||
buffer,
|
||||
message.diff_base.map(|text| text.into_boxed_str().into()),
|
||||
file,
|
||||
);
|
||||
this.text.set_line_ending(proto::deserialize_line_ending(
|
||||
proto::LineEnding::from_i32(message.line_ending)
|
||||
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
||||
|
@ -362,6 +380,7 @@ impl Buffer {
|
|||
id: self.remote_id(),
|
||||
file: self.file.as_ref().map(|f| f.to_proto()),
|
||||
base_text: self.base_text().to_string(),
|
||||
diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
|
||||
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||
}
|
||||
}
|
||||
|
@ -404,7 +423,7 @@ impl Buffer {
|
|||
self
|
||||
}
|
||||
|
||||
fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>) -> Self {
|
||||
fn build(buffer: TextBuffer, diff_base: Option<String>, file: Option<Arc<dyn File>>) -> Self {
|
||||
let saved_mtime = if let Some(file) = file.as_ref() {
|
||||
file.mtime()
|
||||
} else {
|
||||
|
@ -418,6 +437,12 @@ impl Buffer {
|
|||
transaction_depth: 0,
|
||||
was_dirty_before_starting_transaction: None,
|
||||
text: buffer,
|
||||
diff_base,
|
||||
git_diff_status: GitDiffStatus {
|
||||
diff: git::diff::BufferDiff::new(),
|
||||
update_in_progress: false,
|
||||
update_requested: false,
|
||||
},
|
||||
file,
|
||||
syntax_map: Mutex::new(SyntaxMap::new()),
|
||||
parsing_in_background: false,
|
||||
|
@ -432,6 +457,7 @@ impl Buffer {
|
|||
diagnostics_update_count: 0,
|
||||
diagnostics_timestamp: Default::default(),
|
||||
file_update_count: 0,
|
||||
git_diff_update_count: 0,
|
||||
completion_triggers: Default::default(),
|
||||
completion_triggers_timestamp: Default::default(),
|
||||
deferred_ops: OperationQueue::new(),
|
||||
|
@ -447,11 +473,13 @@ impl Buffer {
|
|||
BufferSnapshot {
|
||||
text,
|
||||
syntax,
|
||||
git_diff: self.git_diff_status.diff.clone(),
|
||||
file: self.file.clone(),
|
||||
remote_selections: self.remote_selections.clone(),
|
||||
diagnostics: self.diagnostics.clone(),
|
||||
diagnostics_update_count: self.diagnostics_update_count,
|
||||
file_update_count: self.file_update_count,
|
||||
git_diff_update_count: self.git_diff_update_count,
|
||||
language: self.language.clone(),
|
||||
parse_count: self.parse_count,
|
||||
selections_update_count: self.selections_update_count,
|
||||
|
@ -584,6 +612,7 @@ impl Buffer {
|
|||
cx,
|
||||
);
|
||||
}
|
||||
self.git_diff_recalc(cx);
|
||||
cx.emit(Event::Reloaded);
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -633,6 +662,55 @@ impl Buffer {
|
|||
task
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn diff_base(&self) -> Option<&str> {
|
||||
self.diff_base.as_deref()
|
||||
}
|
||||
|
||||
pub fn update_diff_base(&mut self, diff_base: Option<String>, cx: &mut ModelContext<Self>) {
|
||||
self.diff_base = diff_base;
|
||||
self.git_diff_recalc(cx);
|
||||
}
|
||||
|
||||
pub fn needs_git_diff_recalc(&self) -> bool {
|
||||
self.git_diff_status.diff.needs_update(self)
|
||||
}
|
||||
|
||||
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if self.git_diff_status.update_in_progress {
|
||||
self.git_diff_status.update_requested = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(diff_base) = &self.diff_base {
|
||||
let snapshot = self.snapshot();
|
||||
let diff_base = diff_base.clone();
|
||||
|
||||
let mut diff = self.git_diff_status.diff.clone();
|
||||
let diff = cx.background().spawn(async move {
|
||||
diff.update(&diff_base, &snapshot).await;
|
||||
diff
|
||||
});
|
||||
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let buffer_diff = diff.await;
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.git_diff_status.diff = buffer_diff;
|
||||
this.git_diff_update_count += 1;
|
||||
cx.notify();
|
||||
|
||||
this.git_diff_status.update_in_progress = false;
|
||||
if this.git_diff_status.update_requested {
|
||||
this.git_diff_recalc(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&mut self, cx: &mut ModelContext<Self>) {
|
||||
cx.emit(Event::Closed);
|
||||
}
|
||||
|
@ -657,6 +735,10 @@ impl Buffer {
|
|||
self.file_update_count
|
||||
}
|
||||
|
||||
pub fn git_diff_update_count(&self) -> usize {
|
||||
self.git_diff_update_count
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn is_parsing(&self) -> bool {
|
||||
self.parsing_in_background
|
||||
|
@ -2139,6 +2221,13 @@ impl BufferSnapshot {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range<'a>(
|
||||
&'a self,
|
||||
query_row_range: Range<u32>,
|
||||
) -> impl 'a + Iterator<Item = git::diff::DiffHunk<u32>> {
|
||||
self.git_diff.hunks_in_range(query_row_range, self)
|
||||
}
|
||||
|
||||
pub fn diagnostics_in_range<'a, T, O>(
|
||||
&'a self,
|
||||
search_range: Range<T>,
|
||||
|
@ -2186,6 +2275,10 @@ impl BufferSnapshot {
|
|||
pub fn file_update_count(&self) -> usize {
|
||||
self.file_update_count
|
||||
}
|
||||
|
||||
pub fn git_diff_update_count(&self) -> usize {
|
||||
self.git_diff_update_count
|
||||
}
|
||||
}
|
||||
|
||||
pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
||||
|
@ -2212,6 +2305,7 @@ impl Clone for BufferSnapshot {
|
|||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
text: self.text.clone(),
|
||||
git_diff: self.git_diff.clone(),
|
||||
syntax: self.syntax.clone(),
|
||||
file: self.file.clone(),
|
||||
remote_selections: self.remote_selections.clone(),
|
||||
|
@ -2219,6 +2313,7 @@ impl Clone for BufferSnapshot {
|
|||
selections_update_count: self.selections_update_count,
|
||||
diagnostics_update_count: self.diagnostics_update_count,
|
||||
file_update_count: self.file_update_count,
|
||||
git_diff_update_count: self.git_diff_update_count,
|
||||
language: self.language.clone(),
|
||||
parse_count: self.parse_count,
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ collections = { path = "../collections" }
|
|||
db = { path = "../db" }
|
||||
fsevent = { path = "../fsevent" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
git = { path = "../git" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use fsevent::EventStream;
|
||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||
use git::repository::{GitRepository, LibGitRepository};
|
||||
use language::LineEnding;
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io,
|
||||
os::unix::fs::MetadataExt,
|
||||
|
@ -11,13 +14,16 @@ use std::{
|
|||
time::{Duration, SystemTime},
|
||||
};
|
||||
use text::Rope;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use collections::{btree_map, BTreeMap};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use futures::lock::Mutex;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::{Arc, Weak};
|
||||
use git::repository::FakeGitRepositoryState;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use std::sync::Weak;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Fs: Send + Sync {
|
||||
|
@ -42,6 +48,7 @@ pub trait Fs: Send + Sync {
|
|||
path: &Path,
|
||||
latency: Duration,
|
||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>>;
|
||||
fn is_fake(&self) -> bool;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_fake(&self) -> &FakeFs;
|
||||
|
@ -235,6 +242,14 @@ impl Fs for RealFs {
|
|||
})))
|
||||
}
|
||||
|
||||
fn open_repo(&self, dotgit_path: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
|
||||
LibGitRepository::open(&dotgit_path)
|
||||
.log_err()
|
||||
.and_then::<Arc<SyncMutex<dyn GitRepository>>, _>(|libgit_repository| {
|
||||
Some(Arc::new(SyncMutex::new(libgit_repository)))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -270,6 +285,7 @@ enum FakeFsEntry {
|
|||
inode: u64,
|
||||
mtime: SystemTime,
|
||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||
git_repo_state: Option<Arc<SyncMutex<git::repository::FakeGitRepositoryState>>>,
|
||||
},
|
||||
Symlink {
|
||||
target: PathBuf,
|
||||
|
@ -384,6 +400,7 @@ impl FakeFs {
|
|||
inode: 0,
|
||||
mtime: SystemTime::now(),
|
||||
entries: Default::default(),
|
||||
git_repo_state: None,
|
||||
})),
|
||||
next_inode: 1,
|
||||
event_txs: Default::default(),
|
||||
|
@ -473,6 +490,29 @@ impl FakeFs {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
pub async fn set_index_for_repo(&self, dot_git: &Path, head_state: &[(&Path, String)]) {
|
||||
let content_path = dot_git.parent().unwrap();
|
||||
let mut state = self.state.lock().await;
|
||||
let entry = state.read_path(dot_git).await.unwrap();
|
||||
let mut entry = entry.lock().await;
|
||||
|
||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||
let repo_state = git_repo_state.get_or_insert_with(Default::default);
|
||||
let mut repo_state = repo_state.lock();
|
||||
|
||||
repo_state.index_contents.clear();
|
||||
repo_state.index_contents.extend(
|
||||
head_state
|
||||
.iter()
|
||||
.map(|(path, content)| (content_path.join(path), content.clone())),
|
||||
);
|
||||
|
||||
state.emit_event([dot_git]);
|
||||
} else {
|
||||
panic!("not a directory");
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn files(&self) -> Vec<PathBuf> {
|
||||
let mut result = Vec::new();
|
||||
let mut queue = collections::VecDeque::new();
|
||||
|
@ -562,6 +602,7 @@ impl Fs for FakeFs {
|
|||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
entries: Default::default(),
|
||||
git_repo_state: None,
|
||||
}))
|
||||
});
|
||||
Ok(())
|
||||
|
@ -846,6 +887,24 @@ impl Fs for FakeFs {
|
|||
}))
|
||||
}
|
||||
|
||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<SyncMutex<dyn GitRepository>>> {
|
||||
smol::block_on(async move {
|
||||
let state = self.state.lock().await;
|
||||
let entry = state.read_path(abs_dot_git).await.unwrap();
|
||||
let mut entry = entry.lock().await;
|
||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||
let state = git_repo_state
|
||||
.get_or_insert_with(|| {
|
||||
Arc::new(SyncMutex::new(FakeGitRepositoryState::default()))
|
||||
})
|
||||
.clone();
|
||||
Some(git::repository::FakeGitRepository::open(state))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn is_fake(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
|
|
@ -8,10 +8,14 @@ pub mod worktree;
|
|||
mod project_tests;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||
use client::{
|
||||
proto::{self},
|
||||
Client, PeerId, TypedEnvelope, User, UserStore,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||
|
||||
use gpui::{
|
||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
|
||||
|
@ -420,6 +424,7 @@ impl Project {
|
|||
client.add_model_request_handler(Self::handle_open_buffer_by_id);
|
||||
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
||||
client.add_model_request_handler(Self::handle_save_buffer);
|
||||
client.add_model_message_handler(Self::handle_update_diff_base);
|
||||
}
|
||||
|
||||
pub fn local(
|
||||
|
@ -4533,8 +4538,11 @@ impl Project {
|
|||
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
|
||||
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
||||
if worktree.read(cx).is_local() {
|
||||
cx.subscribe(worktree, |this, worktree, _, cx| {
|
||||
this.update_local_worktree_buffers(worktree, cx);
|
||||
cx.subscribe(worktree, |this, worktree, event, cx| match event {
|
||||
worktree::Event::UpdatedEntries => this.update_local_worktree_buffers(worktree, cx),
|
||||
worktree::Event::UpdatedGitRepositories(updated_repos) => {
|
||||
this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -4642,6 +4650,58 @@ impl Project {
|
|||
}
|
||||
}
|
||||
|
||||
fn update_local_worktree_buffers_git_repos(
|
||||
&mut self,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
repos: &[GitRepositoryEntry],
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
for (_, buffer) in &self.opened_buffers {
|
||||
if let Some(buffer) = buffer.upgrade(cx) {
|
||||
let file = match File::from_dyn(buffer.read(cx).file()) {
|
||||
Some(file) => file,
|
||||
None => continue,
|
||||
};
|
||||
if file.worktree != worktree {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = file.path().clone();
|
||||
|
||||
let repo = match repos.iter().find(|repo| repo.manages(&path)) {
|
||||
Some(repo) => repo.clone(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let shared_remote_id = self.shared_remote_id();
|
||||
let client = self.client.clone();
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let diff_base = cx
|
||||
.background()
|
||||
.spawn(async move { repo.repo.lock().load_index(&path) })
|
||||
.await;
|
||||
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.update_diff_base(diff_base.clone(), cx);
|
||||
buffer.remote_id()
|
||||
});
|
||||
|
||||
if let Some(project_id) = shared_remote_id {
|
||||
client
|
||||
.send(proto::UpdateDiffBase {
|
||||
project_id,
|
||||
buffer_id: buffer_id as u64,
|
||||
diff_base,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
|
@ -5214,6 +5274,27 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
async fn handle_update_diff_base(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateDiffBase>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let buffer_id = envelope.payload.buffer_id;
|
||||
let diff_base = envelope.payload.diff_base;
|
||||
let buffer = this
|
||||
.opened_buffers
|
||||
.get_mut(&buffer_id)
|
||||
.and_then(|b| b.upgrade(cx))
|
||||
.ok_or_else(|| anyhow!("No such buffer {}", buffer_id))?;
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.update_diff_base(diff_base, cx));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_update_buffer_file(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateBufferFile>,
|
||||
|
@ -5780,7 +5861,7 @@ impl Project {
|
|||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||
let mut opened_buffer_rx = self.opened_buffer.1.clone();
|
||||
cx.spawn(|this, cx| async move {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let buffer = loop {
|
||||
let buffer = this.read_with(&cx, |this, cx| {
|
||||
this.opened_buffers
|
||||
|
@ -5798,6 +5879,7 @@ impl Project {
|
|||
.await
|
||||
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
|
||||
};
|
||||
buffer.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx));
|
||||
Ok(buffer)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||
|
||||
use super::{
|
||||
fs::{self, Fs},
|
||||
ignore::IgnoreStack,
|
||||
DiagnosticSummary,
|
||||
};
|
||||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{proto, Client};
|
||||
|
@ -18,6 +17,8 @@ use futures::{
|
|||
Stream, StreamExt,
|
||||
};
|
||||
use fuzzy::CharBag;
|
||||
use git::repository::GitRepository;
|
||||
use git::{DOT_GIT, GITIGNORE};
|
||||
use gpui::{
|
||||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Task,
|
||||
|
@ -26,12 +27,12 @@ use language::{
|
|||
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
||||
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use postage::{
|
||||
prelude::{Sink as _, Stream as _},
|
||||
watch,
|
||||
};
|
||||
|
||||
use smol::channel::{self, Sender};
|
||||
use std::{
|
||||
any::Any,
|
||||
|
@ -40,6 +41,7 @@ use std::{
|
|||
ffi::{OsStr, OsString},
|
||||
fmt,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::{Deref, DerefMut},
|
||||
os::unix::prelude::{OsStrExt, OsStringExt},
|
||||
path::{Path, PathBuf},
|
||||
|
@ -50,10 +52,6 @@ use std::{
|
|||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
lazy_static! {
|
||||
static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub struct WorktreeId(usize);
|
||||
|
||||
|
@ -101,15 +99,51 @@ pub struct Snapshot {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitRepositoryEntry {
|
||||
pub(crate) repo: Arc<Mutex<dyn GitRepository>>,
|
||||
|
||||
pub(crate) scan_id: usize,
|
||||
// Path to folder containing the .git file or directory
|
||||
pub(crate) content_path: Arc<Path>,
|
||||
// Path to the actual .git folder.
|
||||
// Note: if .git is a file, this points to the folder indicated by the .git file
|
||||
pub(crate) git_dir_path: Arc<Path>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GitRepositoryEntry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GitRepositoryEntry")
|
||||
.field("content_path", &self.content_path)
|
||||
.field("git_dir_path", &self.git_dir_path)
|
||||
.field("libgit_repository", &"LibGitRepository")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocalSnapshot {
|
||||
abs_path: Arc<Path>,
|
||||
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
||||
git_repositories: Vec<GitRepositoryEntry>,
|
||||
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
||||
next_entry_id: Arc<AtomicUsize>,
|
||||
snapshot: Snapshot,
|
||||
extension_counts: HashMap<OsString, usize>,
|
||||
}
|
||||
|
||||
impl Clone for LocalSnapshot {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
abs_path: self.abs_path.clone(),
|
||||
ignores_by_parent_abs_path: self.ignores_by_parent_abs_path.clone(),
|
||||
git_repositories: self.git_repositories.iter().cloned().collect(),
|
||||
removed_entry_ids: self.removed_entry_ids.clone(),
|
||||
next_entry_id: self.next_entry_id.clone(),
|
||||
snapshot: self.snapshot.clone(),
|
||||
extension_counts: self.extension_counts.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for LocalSnapshot {
|
||||
type Target = Snapshot;
|
||||
|
||||
|
@ -142,6 +176,7 @@ struct ShareState {
|
|||
|
||||
pub enum Event {
|
||||
UpdatedEntries,
|
||||
UpdatedGitRepositories(Vec<GitRepositoryEntry>),
|
||||
}
|
||||
|
||||
impl Entity for Worktree {
|
||||
|
@ -372,6 +407,7 @@ impl LocalWorktree {
|
|||
let mut snapshot = LocalSnapshot {
|
||||
abs_path,
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
git_repositories: Default::default(),
|
||||
removed_entry_ids: Default::default(),
|
||||
next_entry_id,
|
||||
snapshot: Snapshot {
|
||||
|
@ -446,10 +482,14 @@ impl LocalWorktree {
|
|||
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||
let path = Arc::from(path);
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let (file, contents) = this
|
||||
let (file, contents, diff_base) = this
|
||||
.update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
|
||||
.await?;
|
||||
Ok(cx.add_model(|cx| Buffer::from_file(0, contents, Arc::new(file), cx)))
|
||||
Ok(cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::from_file(0, contents, diff_base, Arc::new(file), cx);
|
||||
buffer.git_diff_recalc(cx);
|
||||
buffer
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -499,17 +539,37 @@ impl LocalWorktree {
|
|||
|
||||
fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
|
||||
self.poll_task.take();
|
||||
|
||||
match self.scan_state() {
|
||||
ScanState::Idle => {
|
||||
self.snapshot = self.background_snapshot.lock().clone();
|
||||
let new_snapshot = self.background_snapshot.lock().clone();
|
||||
let updated_repos = Self::changed_repos(
|
||||
&self.snapshot.git_repositories,
|
||||
&new_snapshot.git_repositories,
|
||||
);
|
||||
self.snapshot = new_snapshot;
|
||||
|
||||
if let Some(share) = self.share.as_mut() {
|
||||
*share.snapshots_tx.borrow_mut() = self.snapshot.clone();
|
||||
}
|
||||
|
||||
cx.emit(Event::UpdatedEntries);
|
||||
|
||||
if !updated_repos.is_empty() {
|
||||
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||
}
|
||||
}
|
||||
|
||||
ScanState::Initializing => {
|
||||
let is_fake_fs = self.fs.is_fake();
|
||||
self.snapshot = self.background_snapshot.lock().clone();
|
||||
|
||||
let new_snapshot = self.background_snapshot.lock().clone();
|
||||
let updated_repos = Self::changed_repos(
|
||||
&self.snapshot.git_repositories,
|
||||
&new_snapshot.git_repositories,
|
||||
);
|
||||
self.snapshot = new_snapshot;
|
||||
|
||||
self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||
if is_fake_fs {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -521,17 +581,52 @@ impl LocalWorktree {
|
|||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||
}
|
||||
}));
|
||||
|
||||
cx.emit(Event::UpdatedEntries);
|
||||
|
||||
if !updated_repos.is_empty() {
|
||||
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||
}
|
||||
}
|
||||
|
||||
_ => {
|
||||
if force {
|
||||
self.snapshot = self.background_snapshot.lock().clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn changed_repos(
|
||||
old_repos: &[GitRepositoryEntry],
|
||||
new_repos: &[GitRepositoryEntry],
|
||||
) -> Vec<GitRepositoryEntry> {
|
||||
fn diff<'a>(
|
||||
a: &'a [GitRepositoryEntry],
|
||||
b: &'a [GitRepositoryEntry],
|
||||
updated: &mut HashMap<&'a Path, GitRepositoryEntry>,
|
||||
) {
|
||||
for a_repo in a {
|
||||
let matched = b.iter().find(|b_repo| {
|
||||
a_repo.git_dir_path == b_repo.git_dir_path && a_repo.scan_id == b_repo.scan_id
|
||||
});
|
||||
|
||||
if matched.is_none() {
|
||||
updated.insert(a_repo.git_dir_path.as_ref(), a_repo.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut updated = HashMap::<&Path, GitRepositoryEntry>::default();
|
||||
|
||||
diff(old_repos, new_repos, &mut updated);
|
||||
diff(new_repos, old_repos, &mut updated);
|
||||
|
||||
updated.into_values().collect()
|
||||
}
|
||||
|
||||
pub fn scan_complete(&self) -> impl Future<Output = ()> {
|
||||
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
||||
async move {
|
||||
|
@ -558,13 +653,31 @@ impl LocalWorktree {
|
|||
}
|
||||
}
|
||||
|
||||
fn load(&self, path: &Path, cx: &mut ModelContext<Worktree>) -> Task<Result<(File, String)>> {
|
||||
fn load(
|
||||
&self,
|
||||
path: &Path,
|
||||
cx: &mut ModelContext<Worktree>,
|
||||
) -> Task<Result<(File, String, Option<String>)>> {
|
||||
let handle = cx.handle();
|
||||
let path = Arc::from(path);
|
||||
let abs_path = self.absolutize(&path);
|
||||
let fs = self.fs.clone();
|
||||
let snapshot = self.snapshot();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let text = fs.load(&abs_path).await?;
|
||||
|
||||
let diff_base = if let Some(repo) = snapshot.repo_for(&abs_path) {
|
||||
cx.background()
|
||||
.spawn({
|
||||
let path = path.clone();
|
||||
async move { repo.repo.lock().load_index(&path) }
|
||||
})
|
||||
.await
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Eagerly populate the snapshot with an updated entry for the loaded file
|
||||
let entry = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
|
@ -573,6 +686,7 @@ impl LocalWorktree {
|
|||
.refresh_entry(path, abs_path, None, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok((
|
||||
File {
|
||||
entry_id: Some(entry.id),
|
||||
|
@ -582,6 +696,7 @@ impl LocalWorktree {
|
|||
is_local: true,
|
||||
},
|
||||
text,
|
||||
diff_base,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
@ -1248,6 +1363,22 @@ impl LocalSnapshot {
|
|||
&self.extension_counts
|
||||
}
|
||||
|
||||
// Gives the most specific git repository for a given path
|
||||
pub(crate) fn repo_for(&self, path: &Path) -> Option<GitRepositoryEntry> {
|
||||
self.git_repositories
|
||||
.iter()
|
||||
.rev() //git_repository is ordered lexicographically
|
||||
.find(|repo| repo.manages(path))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn in_dot_git(&mut self, path: &Path) -> Option<&mut GitRepositoryEntry> {
|
||||
// Git repositories cannot be nested, so we don't need to reverse the order
|
||||
self.git_repositories
|
||||
.iter_mut()
|
||||
.find(|repo| repo.in_dot_git(path))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
|
||||
let root_name = self.root_name.clone();
|
||||
|
@ -1330,7 +1461,7 @@ impl LocalSnapshot {
|
|||
}
|
||||
|
||||
fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
|
||||
if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||
if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
|
||||
let abs_path = self.abs_path.join(&entry.path);
|
||||
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
||||
Ok(ignore) => {
|
||||
|
@ -1384,6 +1515,7 @@ impl LocalSnapshot {
|
|||
parent_path: Arc<Path>,
|
||||
entries: impl IntoIterator<Item = Entry>,
|
||||
ignore: Option<Arc<Gitignore>>,
|
||||
fs: &dyn Fs,
|
||||
) {
|
||||
let mut parent_entry = if let Some(parent_entry) =
|
||||
self.entries_by_path.get(&PathKey(parent_path.clone()), &())
|
||||
|
@ -1409,6 +1541,27 @@ impl LocalSnapshot {
|
|||
unreachable!();
|
||||
}
|
||||
|
||||
if parent_path.file_name() == Some(&DOT_GIT) {
|
||||
let abs_path = self.abs_path.join(&parent_path);
|
||||
let content_path: Arc<Path> = parent_path.parent().unwrap().into();
|
||||
if let Err(ix) = self
|
||||
.git_repositories
|
||||
.binary_search_by_key(&&content_path, |repo| &repo.content_path)
|
||||
{
|
||||
if let Some(repo) = fs.open_repo(abs_path.as_path()) {
|
||||
self.git_repositories.insert(
|
||||
ix,
|
||||
GitRepositoryEntry {
|
||||
repo,
|
||||
scan_id: 0,
|
||||
content_path,
|
||||
git_dir_path: parent_path,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
|
||||
let mut entries_by_id_edits = Vec::new();
|
||||
|
||||
|
@ -1493,6 +1646,14 @@ impl LocalSnapshot {
|
|||
{
|
||||
*scan_id = self.snapshot.scan_id;
|
||||
}
|
||||
} else if path.file_name() == Some(&DOT_GIT) {
|
||||
let parent_path = path.parent().unwrap();
|
||||
if let Ok(ix) = self
|
||||
.git_repositories
|
||||
.binary_search_by_key(&parent_path, |repo| repo.git_dir_path.as_ref())
|
||||
{
|
||||
self.git_repositories[ix].scan_id = self.snapshot.scan_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1532,6 +1693,22 @@ impl LocalSnapshot {
|
|||
|
||||
ignore_stack
|
||||
}
|
||||
|
||||
pub fn git_repo_entries(&self) -> &[GitRepositoryEntry] {
|
||||
&self.git_repositories
|
||||
}
|
||||
}
|
||||
|
||||
impl GitRepositoryEntry {
|
||||
// Note that these paths should be relative to the worktree root.
|
||||
pub(crate) fn manages(&self, path: &Path) -> bool {
|
||||
path.starts_with(self.content_path.as_ref())
|
||||
}
|
||||
|
||||
// Note that theis path should be relative to the worktree root.
|
||||
pub(crate) fn in_dot_git(&self, path: &Path) -> bool {
|
||||
path.starts_with(self.git_dir_path.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
|
||||
|
@ -2244,9 +2421,12 @@ impl BackgroundScanner {
|
|||
new_entries.push(child_entry);
|
||||
}
|
||||
|
||||
self.snapshot
|
||||
.lock()
|
||||
.populate_dir(job.path.clone(), new_entries, new_ignore);
|
||||
self.snapshot.lock().populate_dir(
|
||||
job.path.clone(),
|
||||
new_entries,
|
||||
new_ignore,
|
||||
self.fs.as_ref(),
|
||||
);
|
||||
for new_job in new_jobs {
|
||||
job.scan_queue.send(new_job).await.unwrap();
|
||||
}
|
||||
|
@ -2321,6 +2501,11 @@ impl BackgroundScanner {
|
|||
fs_entry.is_ignored = ignore_stack.is_all();
|
||||
snapshot.insert_entry(fs_entry, self.fs.as_ref());
|
||||
|
||||
let scan_id = snapshot.scan_id;
|
||||
if let Some(repo) = snapshot.in_dot_git(&path) {
|
||||
repo.scan_id = scan_id;
|
||||
}
|
||||
|
||||
let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
|
||||
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
||||
ancestor_inodes.insert(metadata.inode);
|
||||
|
@ -2367,6 +2552,7 @@ impl BackgroundScanner {
|
|||
self.snapshot.lock().removed_entry_ids.clear();
|
||||
|
||||
self.update_ignore_statuses().await;
|
||||
self.update_git_repositories();
|
||||
true
|
||||
}
|
||||
|
||||
|
@ -2432,6 +2618,13 @@ impl BackgroundScanner {
|
|||
.await;
|
||||
}
|
||||
|
||||
fn update_git_repositories(&self) {
|
||||
let mut snapshot = self.snapshot.lock();
|
||||
let mut git_repositories = mem::take(&mut snapshot.git_repositories);
|
||||
git_repositories.retain(|repo| snapshot.entry_for_path(&repo.git_dir_path).is_some());
|
||||
snapshot.git_repositories = git_repositories;
|
||||
}
|
||||
|
||||
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
|
||||
let mut ignore_stack = job.ignore_stack;
|
||||
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
|
||||
|
@ -2778,6 +2971,7 @@ mod tests {
|
|||
use anyhow::Result;
|
||||
use client::test::FakeHttpClient;
|
||||
use fs::RealFs;
|
||||
use git::repository::FakeGitRepository;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
|
@ -2786,6 +2980,7 @@ mod tests {
|
|||
fmt::Write,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use util::test::temp_tree;
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -3005,6 +3200,135 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
let root = temp_tree(json!({
|
||||
"dir1": {
|
||||
".git": {},
|
||||
"deps": {
|
||||
"dep1": {
|
||||
".git": {},
|
||||
"src": {
|
||||
"a.txt": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"src": {
|
||||
"b.txt": ""
|
||||
}
|
||||
},
|
||||
"c.txt": "",
|
||||
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
assert!(tree.repo_for("c.txt".as_ref()).is_none());
|
||||
|
||||
let repo = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap();
|
||||
assert_eq!(repo.content_path.as_ref(), Path::new("dir1"));
|
||||
assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/.git"));
|
||||
|
||||
let repo = tree.repo_for("dir1/deps/dep1/src/a.txt".as_ref()).unwrap();
|
||||
assert_eq!(repo.content_path.as_ref(), Path::new("dir1/deps/dep1"));
|
||||
assert_eq!(repo.git_dir_path.as_ref(), Path::new("dir1/deps/dep1/.git"),);
|
||||
});
|
||||
|
||||
let original_scan_id = tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id
|
||||
});
|
||||
|
||||
std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
let new_scan_id = tree.repo_for("dir1/src/b.txt".as_ref()).unwrap().scan_id;
|
||||
assert_ne!(
|
||||
original_scan_id, new_scan_id,
|
||||
"original {original_scan_id}, new {new_scan_id}"
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
assert!(tree.repo_for("dir1/src/b.txt".as_ref()).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_changed_repos() {
|
||||
fn fake_entry(git_dir_path: impl AsRef<Path>, scan_id: usize) -> GitRepositoryEntry {
|
||||
GitRepositoryEntry {
|
||||
repo: Arc::new(Mutex::new(FakeGitRepository::default())),
|
||||
scan_id,
|
||||
content_path: git_dir_path.as_ref().parent().unwrap().into(),
|
||||
git_dir_path: git_dir_path.as_ref().into(),
|
||||
}
|
||||
}
|
||||
|
||||
let prev_repos: Vec<GitRepositoryEntry> = vec![
|
||||
fake_entry("/.git", 0),
|
||||
fake_entry("/a/.git", 0),
|
||||
fake_entry("/a/b/.git", 0),
|
||||
];
|
||||
|
||||
let new_repos: Vec<GitRepositoryEntry> = vec![
|
||||
fake_entry("/a/.git", 1),
|
||||
fake_entry("/a/b/.git", 0),
|
||||
fake_entry("/a/c/.git", 0),
|
||||
];
|
||||
|
||||
let res = LocalWorktree::changed_repos(&prev_repos, &new_repos);
|
||||
|
||||
// Deletion retained
|
||||
assert!(res
|
||||
.iter()
|
||||
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/.git") && repo.scan_id == 0)
|
||||
.is_some());
|
||||
|
||||
// Update retained
|
||||
assert!(res
|
||||
.iter()
|
||||
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/.git") && repo.scan_id == 1)
|
||||
.is_some());
|
||||
|
||||
// Addition retained
|
||||
assert!(res
|
||||
.iter()
|
||||
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/c/.git") && repo.scan_id == 0)
|
||||
.is_some());
|
||||
|
||||
// Nochange, not retained
|
||||
assert!(res
|
||||
.iter()
|
||||
.find(|repo| repo.git_dir_path.as_ref() == Path::new("/a/b/.git") && repo.scan_id == 0)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_write_file(cx: &mut TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
|
@ -3123,6 +3447,7 @@ mod tests {
|
|||
abs_path: root_dir.path().into(),
|
||||
removed_entry_ids: Default::default(),
|
||||
ignores_by_parent_abs_path: Default::default(),
|
||||
git_repositories: Default::default(),
|
||||
next_entry_id: next_entry_id.clone(),
|
||||
snapshot: Snapshot {
|
||||
id: WorktreeId::from_usize(0),
|
||||
|
|
|
@ -108,9 +108,9 @@ message Envelope {
|
|||
FollowResponse follow_response = 93;
|
||||
UpdateFollowers update_followers = 94;
|
||||
Unfollow unfollow = 95;
|
||||
|
||||
GetPrivateUserInfo get_private_user_info = 96;
|
||||
GetPrivateUserInfoResponse get_private_user_info_response = 97;
|
||||
UpdateDiffBase update_diff_base = 98;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -831,7 +831,8 @@ message BufferState {
|
|||
uint64 id = 1;
|
||||
optional File file = 2;
|
||||
string base_text = 3;
|
||||
LineEnding line_ending = 4;
|
||||
optional string diff_base = 4;
|
||||
LineEnding line_ending = 5;
|
||||
}
|
||||
|
||||
message BufferChunk {
|
||||
|
@ -1001,3 +1002,9 @@ message WorktreeMetadata {
|
|||
string root_name = 2;
|
||||
bool visible = 3;
|
||||
}
|
||||
|
||||
message UpdateDiffBase {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
optional string diff_base = 3;
|
||||
}
|
||||
|
|
|
@ -167,6 +167,7 @@ messages!(
|
|||
(UpdateProject, Foreground),
|
||||
(UpdateWorktree, Foreground),
|
||||
(UpdateWorktreeExtensions, Background),
|
||||
(UpdateDiffBase, Background),
|
||||
(GetPrivateUserInfo, Foreground),
|
||||
(GetPrivateUserInfoResponse, Foreground),
|
||||
);
|
||||
|
@ -266,6 +267,7 @@ entity_messages!(
|
|||
UpdateProject,
|
||||
UpdateWorktree,
|
||||
UpdateWorktreeExtensions,
|
||||
UpdateDiffBase
|
||||
);
|
||||
|
||||
entity_messages!(channel_id, ChannelMessageSent);
|
||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 32;
|
||||
pub const PROTOCOL_VERSION: u32 = 34;
|
||||
|
|
|
@ -32,6 +32,8 @@ pub struct Settings {
|
|||
pub default_dock_anchor: DockAnchor,
|
||||
pub editor_defaults: EditorSettings,
|
||||
pub editor_overrides: EditorSettings,
|
||||
pub git: GitSettings,
|
||||
pub git_overrides: GitSettings,
|
||||
pub terminal_defaults: TerminalSettings,
|
||||
pub terminal_overrides: TerminalSettings,
|
||||
pub language_defaults: HashMap<Arc<str>, EditorSettings>,
|
||||
|
@ -52,6 +54,22 @@ impl FeatureFlags {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct GitSettings {
|
||||
pub git_gutter: Option<GitGutter>,
|
||||
pub gutter_debounce: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitGutter {
|
||||
#[default]
|
||||
TrackedFiles,
|
||||
Hide,
|
||||
}
|
||||
|
||||
pub struct GitGutterConfig {}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
pub struct EditorSettings {
|
||||
pub tab_size: Option<NonZeroU32>,
|
||||
|
@ -196,6 +214,8 @@ pub struct SettingsFileContent {
|
|||
#[serde(default)]
|
||||
pub terminal: TerminalSettings,
|
||||
#[serde(default)]
|
||||
pub git: Option<GitSettings>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "language_overrides")]
|
||||
pub languages: HashMap<Arc<str>, EditorSettings>,
|
||||
#[serde(default)]
|
||||
|
@ -252,6 +272,8 @@ impl Settings {
|
|||
enable_language_server: required(defaults.editor.enable_language_server),
|
||||
},
|
||||
editor_overrides: Default::default(),
|
||||
git: defaults.git.unwrap(),
|
||||
git_overrides: Default::default(),
|
||||
terminal_defaults: Default::default(),
|
||||
terminal_overrides: Default::default(),
|
||||
language_defaults: defaults.languages,
|
||||
|
@ -303,6 +325,7 @@ impl Settings {
|
|||
}
|
||||
|
||||
self.editor_overrides = data.editor;
|
||||
self.git_overrides = data.git.unwrap_or_default();
|
||||
self.terminal_defaults.font_size = data.terminal.font_size;
|
||||
self.terminal_overrides = data.terminal;
|
||||
self.language_overrides = data.languages;
|
||||
|
@ -358,6 +381,14 @@ impl Settings {
|
|||
.expect("missing default")
|
||||
}
|
||||
|
||||
pub fn git_gutter(&self) -> GitGutter {
|
||||
self.git_overrides.git_gutter.unwrap_or_else(|| {
|
||||
self.git
|
||||
.git_gutter
|
||||
.expect("git_gutter should be some by setting setup")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &gpui::AppContext) -> Settings {
|
||||
Settings {
|
||||
|
@ -382,6 +413,8 @@ impl Settings {
|
|||
editor_overrides: Default::default(),
|
||||
terminal_defaults: Default::default(),
|
||||
terminal_overrides: Default::default(),
|
||||
git: Default::default(),
|
||||
git_overrides: Default::default(),
|
||||
language_defaults: Default::default(),
|
||||
language_overrides: Default::default(),
|
||||
lsp: Default::default(),
|
||||
|
|
|
@ -101,6 +101,12 @@ pub enum Bias {
|
|||
Right,
|
||||
}
|
||||
|
||||
impl Default for Bias {
|
||||
fn default() -> Self {
|
||||
Bias::Left
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Bias {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
|
|
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
|||
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
|
||||
pub struct Anchor {
|
||||
pub timestamp: clock::Local,
|
||||
pub offset: usize,
|
||||
|
|
|
@ -54,6 +54,13 @@ impl Rope {
|
|||
cursor.slice(range.end)
|
||||
}
|
||||
|
||||
pub fn slice_rows(&self, range: Range<u32>) -> Rope {
|
||||
//This would be more efficient with a forward advance after the first, but it's fine
|
||||
let start = self.point_to_offset(Point::new(range.start, 0));
|
||||
let end = self.point_to_offset(Point::new(range.end, 0));
|
||||
self.slice(start..end)
|
||||
}
|
||||
|
||||
pub fn push(&mut self, text: &str) {
|
||||
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
||||
let mut new_chunk = ArrayString::new();
|
||||
|
|
|
@ -488,8 +488,7 @@ pub struct Editor {
|
|||
pub rename_fade: f32,
|
||||
pub document_highlight_read_background: Color,
|
||||
pub document_highlight_write_background: Color,
|
||||
pub diff_background_deleted: Color,
|
||||
pub diff_background_inserted: Color,
|
||||
pub diff: DiffStyle,
|
||||
pub line_number: Color,
|
||||
pub line_number_active: Color,
|
||||
pub guest_selections: Vec<SelectionStyle>,
|
||||
|
@ -573,6 +572,16 @@ pub struct CodeActions {
|
|||
pub vertical_scale: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct DiffStyle {
|
||||
pub inserted: Color,
|
||||
pub modified: Color,
|
||||
pub deleted: Color,
|
||||
pub removed_width_em: f32,
|
||||
pub width_em: f32,
|
||||
pub corner_radius: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Interactive<T> {
|
||||
pub default: T,
|
||||
|
|
|
@ -7,17 +7,21 @@ edition = "2021"
|
|||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["rand", "serde_json", "tempdir"]
|
||||
test-support = ["rand", "serde_json", "tempdir", "git2"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.38"
|
||||
futures = "0.3"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
lazy_static = "1.4.0"
|
||||
rand = { version = "0.8", optional = true }
|
||||
tempdir = { version = "0.3.7", optional = true }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
|
||||
git2 = { version = "0.15", default-features = false, optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
rand = { version = "0.8" }
|
||||
tempdir = { version = "0.3.7" }
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
mod assertions;
|
||||
mod marked_text;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use git2;
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tempdir::TempDir;
|
||||
|
||||
pub use assertions::*;
|
||||
|
@ -24,6 +28,11 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
|
|||
match contents {
|
||||
Value::Object(_) => {
|
||||
fs::create_dir(&path).unwrap();
|
||||
|
||||
if path.file_name() == Some(&OsStr::new(".git")) {
|
||||
git2::Repository::init(&path.parent().unwrap()).unwrap();
|
||||
}
|
||||
|
||||
write_tree(&path, contents);
|
||||
}
|
||||
Value::Null => {
|
||||
|
|
|
@ -52,7 +52,6 @@ use std::{
|
|||
cell::RefCell,
|
||||
fmt,
|
||||
future::Future,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
|
@ -318,7 +317,23 @@ pub trait Item: View {
|
|||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
_project: ModelHandle<Project>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent>;
|
||||
fn should_close_item_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn is_edit_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn act_as_type(
|
||||
&self,
|
||||
type_id: TypeId,
|
||||
|
@ -435,6 +450,57 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
|||
}
|
||||
}
|
||||
|
||||
struct DelayedDebouncedEditAction {
|
||||
task: Option<Task<()>>,
|
||||
cancel_channel: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl DelayedDebouncedEditAction {
|
||||
fn new() -> DelayedDebouncedEditAction {
|
||||
DelayedDebouncedEditAction {
|
||||
task: None,
|
||||
cancel_channel: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fire_new<F, Fut>(
|
||||
&mut self,
|
||||
delay: Duration,
|
||||
workspace: &Workspace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
f: F,
|
||||
) where
|
||||
F: FnOnce(ModelHandle<Project>, AsyncAppContext) -> Fut + 'static,
|
||||
Fut: 'static + Future<Output = ()>,
|
||||
{
|
||||
if let Some(channel) = self.cancel_channel.take() {
|
||||
_ = channel.send(());
|
||||
}
|
||||
|
||||
let project = workspace.project().downgrade();
|
||||
|
||||
let (sender, mut receiver) = oneshot::channel::<()>();
|
||||
self.cancel_channel = Some(sender);
|
||||
|
||||
let previous_task = self.task.take();
|
||||
self.task = Some(cx.spawn_weak(|_, cx| async move {
|
||||
let mut timer = cx.background().timer(delay).fuse();
|
||||
if let Some(previous_task) = previous_task {
|
||||
previous_task.await;
|
||||
}
|
||||
|
||||
futures::select_biased! {
|
||||
_ = receiver => return,
|
||||
_ = timer => {}
|
||||
}
|
||||
|
||||
if let Some(project) = project.upgrade(&cx) {
|
||||
(f)(project, cx).await;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ItemHandle: 'static + fmt::Debug {
|
||||
fn subscribe_to_item_events(
|
||||
&self,
|
||||
|
@ -473,6 +539,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
|||
) -> Task<Result<()>>;
|
||||
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
||||
-> Task<Result<()>>;
|
||||
fn git_diff_recalc(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
|
||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||
fn on_release(
|
||||
|
@ -578,8 +649,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
.insert(self.id(), pane.downgrade())
|
||||
.is_none()
|
||||
{
|
||||
let mut pending_autosave = None;
|
||||
let mut cancel_pending_autosave = oneshot::channel::<()>().0;
|
||||
let mut pending_autosave = DelayedDebouncedEditAction::new();
|
||||
let mut pending_git_update = DelayedDebouncedEditAction::new();
|
||||
let pending_update = Rc::new(RefCell::new(None));
|
||||
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
||||
|
||||
|
@ -637,45 +708,66 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
.detach_and_log_err(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
ItemEvent::UpdateTab => {
|
||||
pane.update(cx, |_, cx| {
|
||||
cx.emit(pane::Event::ChangeItemTitle);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
ItemEvent::Edit => {
|
||||
if let Autosave::AfterDelay { milliseconds } =
|
||||
cx.global::<Settings>().autosave
|
||||
{
|
||||
let prev_autosave = pending_autosave
|
||||
.take()
|
||||
.unwrap_or_else(|| Task::ready(Some(())));
|
||||
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
|
||||
let prev_cancel_tx =
|
||||
mem::replace(&mut cancel_pending_autosave, cancel_tx);
|
||||
let project = workspace.project.downgrade();
|
||||
let _ = prev_cancel_tx.send(());
|
||||
let delay = Duration::from_millis(milliseconds);
|
||||
let item = item.clone();
|
||||
pending_autosave =
|
||||
Some(cx.spawn_weak(|_, mut cx| async move {
|
||||
let mut timer = cx
|
||||
.background()
|
||||
.timer(Duration::from_millis(milliseconds))
|
||||
.fuse();
|
||||
prev_autosave.await;
|
||||
futures::select_biased! {
|
||||
_ = cancel_rx => return None,
|
||||
_ = timer => {}
|
||||
}
|
||||
|
||||
let project = project.upgrade(&cx)?;
|
||||
pending_autosave.fire_new(
|
||||
delay,
|
||||
workspace,
|
||||
cx,
|
||||
|project, mut cx| async move {
|
||||
cx.update(|cx| Pane::autosave_item(&item, project, cx))
|
||||
.await
|
||||
.log_err();
|
||||
None
|
||||
}));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let settings = cx.global::<Settings>();
|
||||
let debounce_delay = settings.git_overrides.gutter_debounce;
|
||||
|
||||
let item = item.clone();
|
||||
|
||||
if let Some(delay) = debounce_delay {
|
||||
const MIN_GIT_DELAY: u64 = 50;
|
||||
|
||||
let delay = delay.max(MIN_GIT_DELAY);
|
||||
let duration = Duration::from_millis(delay);
|
||||
|
||||
pending_git_update.fire_new(
|
||||
duration,
|
||||
workspace,
|
||||
cx,
|
||||
|project, mut cx| async move {
|
||||
cx.update(|cx| item.git_diff_recalc(project, cx))
|
||||
.await
|
||||
.log_err();
|
||||
},
|
||||
);
|
||||
} else {
|
||||
let project = workspace.project().downgrade();
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
if let Some(project) = project.upgrade(&cx) {
|
||||
cx.update(|cx| item.git_diff_recalc(project, cx))
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -755,6 +847,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
self.update(cx, |item, cx| item.reload(project, cx))
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.git_diff_recalc(project, cx))
|
||||
}
|
||||
|
||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
|
||||
self.read(cx).act_as_type(type_id, self, cx)
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
player,
|
||||
popoverShadow,
|
||||
text,
|
||||
textColor,
|
||||
TextColor,
|
||||
} from "./components";
|
||||
import hoverPopover from "./hoverPopover";
|
||||
|
@ -59,8 +60,14 @@ export default function editor(theme: Theme) {
|
|||
indicator: iconColor(theme, "secondary"),
|
||||
verticalScale: 0.618
|
||||
},
|
||||
diffBackgroundDeleted: backgroundColor(theme, "error"),
|
||||
diffBackgroundInserted: backgroundColor(theme, "ok"),
|
||||
diff: {
|
||||
deleted: theme.iconColor.error,
|
||||
inserted: theme.iconColor.ok,
|
||||
modified: theme.iconColor.warning,
|
||||
removedWidthEm: 0.275,
|
||||
widthEm: 0.16,
|
||||
cornerRadius: 0.05,
|
||||
},
|
||||
documentHighlightReadBackground: theme.editor.highlight.occurrence,
|
||||
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
|
||||
errorColor: theme.textColor.error,
|
||||
|
|
|
@ -113,6 +113,11 @@ export function createTheme(
|
|||
hovered: sample(ramps.blue, 0.1),
|
||||
active: sample(ramps.blue, 0.15),
|
||||
},
|
||||
on500Ok: {
|
||||
base: sample(ramps.green, 0.05),
|
||||
hovered: sample(ramps.green, 0.1),
|
||||
active: sample(ramps.green, 0.15)
|
||||
}
|
||||
};
|
||||
|
||||
const borderColor = {
|
||||
|
|
|
@ -78,6 +78,7 @@ export default interface Theme {
|
|||
// Hacks for elements on top of the editor
|
||||
on500: BackgroundColorSet;
|
||||
ok: BackgroundColorSet;
|
||||
on500Ok: BackgroundColorSet;
|
||||
error: BackgroundColorSet;
|
||||
on500Error: BackgroundColorSet;
|
||||
warning: BackgroundColorSet;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue