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",
|
"env_logger",
|
||||||
"envy",
|
"envy",
|
||||||
"futures",
|
"futures",
|
||||||
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"hyper",
|
"hyper",
|
||||||
"language",
|
"language",
|
||||||
|
@ -1061,6 +1062,7 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"unindent",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
@ -1697,6 +1699,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
@ -2224,6 +2227,39 @@ dependencies = [
|
||||||
"stable_deref_trait",
|
"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]]
|
[[package]]
|
||||||
name = "glob"
|
name = "glob"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
@ -2840,6 +2876,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
@ -2894,6 +2931,18 @@ version = "0.2.126"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
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]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
|
@ -3970,6 +4019,7 @@ dependencies = [
|
||||||
"fsevent",
|
"fsevent",
|
||||||
"futures",
|
"futures",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
"git",
|
||||||
"gpui",
|
"gpui",
|
||||||
"ignore",
|
"ignore",
|
||||||
"language",
|
"language",
|
||||||
|
@ -6332,6 +6382,8 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"futures",
|
"futures",
|
||||||
|
"git2",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -74,6 +74,15 @@
|
||||||
"hard_tabs": false,
|
"hard_tabs": false,
|
||||||
// How many columns a tab should occupy.
|
// How many columns a tab should occupy.
|
||||||
"tab_size": 4,
|
"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
|
// Settings specific to the terminal
|
||||||
"terminal": {
|
"terminal": {
|
||||||
// What shell to use when opening a terminal. May take 3 values:
|
// What shell to use when opening a terminal. May take 3 values:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
authors = ["Nathan Sobo <nathan@warp.dev>"]
|
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
|
@ -26,6 +26,7 @@ base64 = "0.13"
|
||||||
clap = { version = "3.1", features = ["derive"], optional = true }
|
clap = { version = "3.1", features = ["derive"], optional = true }
|
||||||
envy = "0.4.2"
|
envy = "0.4.2"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
git = { path = "../git" }
|
||||||
hyper = "0.14"
|
hyper = "0.14"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
lipsum = { version = "0.8", optional = true }
|
lipsum = { version = "0.8", optional = true }
|
||||||
|
@ -65,11 +66,13 @@ project = { path = "../project", features = ["test-support"] }
|
||||||
settings = { path = "../settings", features = ["test-support"] }
|
settings = { path = "../settings", features = ["test-support"] }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace", features = ["test-support"] }
|
workspace = { path = "../workspace", features = ["test-support"] }
|
||||||
|
git = { path = "../git", features = ["test-support"] }
|
||||||
ctor = "0.1"
|
ctor = "0.1"
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||||
|
unindent = "0.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
seed-support = ["clap", "lipsum", "reqwest"]
|
seed-support = ["clap", "lipsum", "reqwest"]
|
||||||
|
|
|
@ -51,6 +51,7 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use theme::ThemeRegistry;
|
use theme::ThemeRegistry;
|
||||||
|
use unindent::Unindent as _;
|
||||||
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
|
||||||
|
|
||||||
#[ctor::ctor]
|
#[ctor::ctor]
|
||||||
|
@ -946,6 +947,136 @@ async fn test_propagate_saves_and_fs_changes(
|
||||||
.await;
|
.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)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_fs_operations(
|
async fn test_fs_operations(
|
||||||
executor: Arc<Deterministic>,
|
executor: Arc<Deterministic>,
|
||||||
|
|
|
@ -206,6 +206,7 @@ impl Server {
|
||||||
.add_message_handler(Server::unfollow)
|
.add_message_handler(Server::unfollow)
|
||||||
.add_message_handler(Server::update_followers)
|
.add_message_handler(Server::update_followers)
|
||||||
.add_request_handler(Server::get_channel_messages)
|
.add_request_handler(Server::get_channel_messages)
|
||||||
|
.add_message_handler(Server::update_diff_base)
|
||||||
.add_request_handler(Server::get_private_user_info);
|
.add_request_handler(Server::get_private_user_info);
|
||||||
|
|
||||||
Arc::new(server)
|
Arc::new(server)
|
||||||
|
@ -1728,6 +1729,21 @@ impl Server {
|
||||||
Ok(())
|
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(
|
async fn get_private_user_info(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
request: TypedEnvelope<proto::GetPrivateUserInfo>,
|
request: TypedEnvelope<proto::GetPrivateUserInfo>,
|
||||||
|
|
|
@ -25,6 +25,7 @@ clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
context_menu = { path = "../context_menu" }
|
context_menu = { path = "../context_menu" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
git = { path = "../git" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
|
|
|
@ -274,6 +274,7 @@ impl FoldMap {
|
||||||
if buffer.edit_count() != new_buffer.edit_count()
|
if buffer.edit_count() != new_buffer.edit_count()
|
||||||
|| buffer.parse_count() != new_buffer.parse_count()
|
|| buffer.parse_count() != new_buffer.parse_count()
|
||||||
|| buffer.diagnostics_update_count() != new_buffer.diagnostics_update_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()
|
|| buffer.trailing_excerpt_update_count()
|
||||||
!= new_buffer.trailing_excerpt_update_count()
|
!= new_buffer.trailing_excerpt_update_count()
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,6 +16,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, HashMap};
|
use collections::{BTreeMap, HashMap};
|
||||||
|
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::Color,
|
color::Color,
|
||||||
elements::*,
|
elements::*,
|
||||||
|
@ -36,7 +37,7 @@ use gpui::{
|
||||||
use json::json;
|
use json::json;
|
||||||
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection};
|
||||||
use project::ProjectPath;
|
use project::ProjectPath;
|
||||||
use settings::Settings;
|
use settings::{GitGutter, Settings};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
|
@ -45,6 +46,7 @@ use std::{
|
||||||
ops::Range,
|
ops::Range,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
use theme::DiffStyle;
|
||||||
|
|
||||||
struct SelectionLayout {
|
struct SelectionLayout {
|
||||||
head: DisplayPoint,
|
head: DisplayPoint,
|
||||||
|
@ -524,30 +526,143 @@ impl EditorElement {
|
||||||
layout: &mut LayoutState,
|
layout: &mut LayoutState,
|
||||||
cx: &mut PaintContext,
|
cx: &mut PaintContext,
|
||||||
) {
|
) {
|
||||||
let scroll_top =
|
struct GutterLayout {
|
||||||
layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
|
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() {
|
for (ix, line) in layout.line_number_layouts.iter().enumerate() {
|
||||||
if let Some(line) = line {
|
if let Some(line) = line {
|
||||||
let line_origin = bounds.origin()
|
let line_origin = bounds.origin()
|
||||||
+ vec2f(
|
+ vec2f(
|
||||||
bounds.width() - line.width() - layout.gutter_padding,
|
bounds.width() - line.width() - layout.gutter_padding,
|
||||||
ix as f32 * layout.position_map.line_height
|
ix as f32 * gutter_layout.line_height
|
||||||
- (scroll_top % layout.position_map.line_height),
|
- (gutter_layout.scroll_top % gutter_layout.line_height),
|
||||||
);
|
|
||||||
line.paint(
|
|
||||||
line_origin,
|
|
||||||
visible_bounds,
|
|
||||||
layout.position_map.line_height,
|
|
||||||
cx,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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() {
|
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
||||||
let mut x = bounds.width() - layout.gutter_padding;
|
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.;
|
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);
|
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 {
|
impl Element for EditorElement {
|
||||||
type LayoutState = LayoutState;
|
type LayoutState = LayoutState;
|
||||||
type PaintState = ();
|
type PaintState = ();
|
||||||
|
@ -1425,6 +1561,11 @@ impl Element for EditorElement {
|
||||||
let line_number_layouts =
|
let line_number_layouts =
|
||||||
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
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 mut max_visible_line_width = 0.0;
|
||||||
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx);
|
||||||
for line in &line_layouts {
|
for line in &line_layouts {
|
||||||
|
@ -1573,6 +1714,7 @@ impl Element for EditorElement {
|
||||||
highlighted_rows,
|
highlighted_rows,
|
||||||
highlighted_ranges,
|
highlighted_ranges,
|
||||||
line_number_layouts,
|
line_number_layouts,
|
||||||
|
diff_hunks,
|
||||||
blocks,
|
blocks,
|
||||||
selections,
|
selections,
|
||||||
context_menu,
|
context_menu,
|
||||||
|
@ -1710,6 +1852,7 @@ pub struct LayoutState {
|
||||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||||
|
diff_hunks: Vec<DiffHunk<u32>>,
|
||||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||||
hover_popovers: Option<(DisplayPoint, Vec<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> {
|
fn to_item_events(event: &Self::Event) -> Vec<workspace::ItemEvent> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
match event {
|
match event {
|
||||||
|
|
|
@ -4,6 +4,7 @@ pub use anchor::{Anchor, AnchorRangeExt};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||||
|
use git::diff::DiffHunk;
|
||||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||||
pub use language::Completion;
|
pub use language::Completion;
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -90,6 +91,7 @@ struct BufferState {
|
||||||
last_selections_update_count: usize,
|
last_selections_update_count: usize,
|
||||||
last_diagnostics_update_count: usize,
|
last_diagnostics_update_count: usize,
|
||||||
last_file_update_count: usize,
|
last_file_update_count: usize,
|
||||||
|
last_git_diff_update_count: usize,
|
||||||
excerpts: Vec<ExcerptId>,
|
excerpts: Vec<ExcerptId>,
|
||||||
_subscriptions: [gpui::Subscription; 2],
|
_subscriptions: [gpui::Subscription; 2],
|
||||||
}
|
}
|
||||||
|
@ -101,6 +103,7 @@ pub struct MultiBufferSnapshot {
|
||||||
parse_count: usize,
|
parse_count: usize,
|
||||||
diagnostics_update_count: usize,
|
diagnostics_update_count: usize,
|
||||||
trailing_excerpt_update_count: usize,
|
trailing_excerpt_update_count: usize,
|
||||||
|
git_diff_update_count: usize,
|
||||||
edit_count: usize,
|
edit_count: usize,
|
||||||
is_dirty: bool,
|
is_dirty: bool,
|
||||||
has_conflict: bool,
|
has_conflict: bool,
|
||||||
|
@ -202,6 +205,7 @@ impl MultiBuffer {
|
||||||
last_selections_update_count: buffer_state.last_selections_update_count,
|
last_selections_update_count: buffer_state.last_selections_update_count,
|
||||||
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
|
last_diagnostics_update_count: buffer_state.last_diagnostics_update_count,
|
||||||
last_file_update_count: buffer_state.last_file_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(),
|
excerpts: buffer_state.excerpts.clone(),
|
||||||
_subscriptions: [
|
_subscriptions: [
|
||||||
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()),
|
||||||
|
@ -308,6 +312,17 @@ impl MultiBuffer {
|
||||||
self.read(cx).symbols_containing(offset, theme)
|
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>(
|
pub fn edit<I, S, T>(
|
||||||
&mut self,
|
&mut self,
|
||||||
edits: I,
|
edits: I,
|
||||||
|
@ -827,6 +842,7 @@ impl MultiBuffer {
|
||||||
last_selections_update_count: buffer_snapshot.selections_update_count(),
|
last_selections_update_count: buffer_snapshot.selections_update_count(),
|
||||||
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
|
last_diagnostics_update_count: buffer_snapshot.diagnostics_update_count(),
|
||||||
last_file_update_count: buffer_snapshot.file_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(),
|
excerpts: Default::default(),
|
||||||
_subscriptions: [
|
_subscriptions: [
|
||||||
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
cx.observe(&buffer, |_, _, cx| cx.notify()),
|
||||||
|
@ -1249,6 +1265,7 @@ impl MultiBuffer {
|
||||||
let mut excerpts_to_edit = Vec::new();
|
let mut excerpts_to_edit = Vec::new();
|
||||||
let mut reparsed = false;
|
let mut reparsed = false;
|
||||||
let mut diagnostics_updated = false;
|
let mut diagnostics_updated = false;
|
||||||
|
let mut git_diff_updated = false;
|
||||||
let mut is_dirty = false;
|
let mut is_dirty = false;
|
||||||
let mut has_conflict = false;
|
let mut has_conflict = false;
|
||||||
let mut edited = false;
|
let mut edited = false;
|
||||||
|
@ -1260,6 +1277,7 @@ impl MultiBuffer {
|
||||||
let selections_update_count = buffer.selections_update_count();
|
let selections_update_count = buffer.selections_update_count();
|
||||||
let diagnostics_update_count = buffer.diagnostics_update_count();
|
let diagnostics_update_count = buffer.diagnostics_update_count();
|
||||||
let file_update_count = buffer.file_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_edited = version.changed_since(&buffer_state.last_version);
|
||||||
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
|
let buffer_reparsed = parse_count > buffer_state.last_parse_count;
|
||||||
|
@ -1268,17 +1286,21 @@ impl MultiBuffer {
|
||||||
let buffer_diagnostics_updated =
|
let buffer_diagnostics_updated =
|
||||||
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
|
diagnostics_update_count > buffer_state.last_diagnostics_update_count;
|
||||||
let buffer_file_updated = file_update_count > buffer_state.last_file_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
|
if buffer_edited
|
||||||
|| buffer_reparsed
|
|| buffer_reparsed
|
||||||
|| buffer_selections_updated
|
|| buffer_selections_updated
|
||||||
|| buffer_diagnostics_updated
|
|| buffer_diagnostics_updated
|
||||||
|| buffer_file_updated
|
|| buffer_file_updated
|
||||||
|
|| buffer_git_diff_updated
|
||||||
{
|
{
|
||||||
buffer_state.last_version = version;
|
buffer_state.last_version = version;
|
||||||
buffer_state.last_parse_count = parse_count;
|
buffer_state.last_parse_count = parse_count;
|
||||||
buffer_state.last_selections_update_count = selections_update_count;
|
buffer_state.last_selections_update_count = selections_update_count;
|
||||||
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
|
buffer_state.last_diagnostics_update_count = diagnostics_update_count;
|
||||||
buffer_state.last_file_update_count = file_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(
|
excerpts_to_edit.extend(
|
||||||
buffer_state
|
buffer_state
|
||||||
.excerpts
|
.excerpts
|
||||||
|
@ -1290,6 +1312,7 @@ impl MultiBuffer {
|
||||||
edited |= buffer_edited;
|
edited |= buffer_edited;
|
||||||
reparsed |= buffer_reparsed;
|
reparsed |= buffer_reparsed;
|
||||||
diagnostics_updated |= buffer_diagnostics_updated;
|
diagnostics_updated |= buffer_diagnostics_updated;
|
||||||
|
git_diff_updated |= buffer_git_diff_updated;
|
||||||
is_dirty |= buffer.is_dirty();
|
is_dirty |= buffer.is_dirty();
|
||||||
has_conflict |= buffer.has_conflict();
|
has_conflict |= buffer.has_conflict();
|
||||||
}
|
}
|
||||||
|
@ -1302,6 +1325,9 @@ impl MultiBuffer {
|
||||||
if diagnostics_updated {
|
if diagnostics_updated {
|
||||||
snapshot.diagnostics_update_count += 1;
|
snapshot.diagnostics_update_count += 1;
|
||||||
}
|
}
|
||||||
|
if git_diff_updated {
|
||||||
|
snapshot.git_diff_update_count += 1;
|
||||||
|
}
|
||||||
snapshot.is_dirty = is_dirty;
|
snapshot.is_dirty = is_dirty;
|
||||||
snapshot.has_conflict = has_conflict;
|
snapshot.has_conflict = has_conflict;
|
||||||
|
|
||||||
|
@ -2479,6 +2505,10 @@ impl MultiBufferSnapshot {
|
||||||
self.diagnostics_update_count
|
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 {
|
pub fn trailing_excerpt_update_count(&self) -> usize {
|
||||||
self.trailing_excerpt_update_count
|
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>> {
|
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);
|
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" }
|
clock = { path = "../clock" }
|
||||||
collections = { path = "../collections" }
|
collections = { path = "../collections" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
git = { path = "../git" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
|
|
|
@ -45,8 +45,16 @@ pub use {tree_sitter_rust, tree_sitter_typescript};
|
||||||
|
|
||||||
pub use lsp::DiagnosticSeverity;
|
pub use lsp::DiagnosticSeverity;
|
||||||
|
|
||||||
|
struct GitDiffStatus {
|
||||||
|
diff: git::diff::BufferDiff,
|
||||||
|
update_in_progress: bool,
|
||||||
|
update_requested: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
text: TextBuffer,
|
text: TextBuffer,
|
||||||
|
diff_base: Option<String>,
|
||||||
|
git_diff_status: GitDiffStatus,
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
saved_version: clock::Global,
|
saved_version: clock::Global,
|
||||||
saved_version_fingerprint: String,
|
saved_version_fingerprint: String,
|
||||||
|
@ -66,6 +74,7 @@ pub struct Buffer {
|
||||||
diagnostics_update_count: usize,
|
diagnostics_update_count: usize,
|
||||||
diagnostics_timestamp: clock::Lamport,
|
diagnostics_timestamp: clock::Lamport,
|
||||||
file_update_count: usize,
|
file_update_count: usize,
|
||||||
|
git_diff_update_count: usize,
|
||||||
completion_triggers: Vec<String>,
|
completion_triggers: Vec<String>,
|
||||||
completion_triggers_timestamp: clock::Lamport,
|
completion_triggers_timestamp: clock::Lamport,
|
||||||
deferred_ops: OperationQueue<Operation>,
|
deferred_ops: OperationQueue<Operation>,
|
||||||
|
@ -73,11 +82,13 @@ pub struct Buffer {
|
||||||
|
|
||||||
pub struct BufferSnapshot {
|
pub struct BufferSnapshot {
|
||||||
text: text::BufferSnapshot,
|
text: text::BufferSnapshot,
|
||||||
|
pub git_diff: git::diff::BufferDiff,
|
||||||
pub(crate) syntax: SyntaxSnapshot,
|
pub(crate) syntax: SyntaxSnapshot,
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
diagnostics: DiagnosticSet,
|
diagnostics: DiagnosticSet,
|
||||||
diagnostics_update_count: usize,
|
diagnostics_update_count: usize,
|
||||||
file_update_count: usize,
|
file_update_count: usize,
|
||||||
|
git_diff_update_count: usize,
|
||||||
remote_selections: TreeMap<ReplicaId, SelectionSet>,
|
remote_selections: TreeMap<ReplicaId, SelectionSet>,
|
||||||
selections_update_count: usize,
|
selections_update_count: usize,
|
||||||
language: Option<Arc<Language>>,
|
language: Option<Arc<Language>>,
|
||||||
|
@ -328,17 +339,20 @@ impl Buffer {
|
||||||
Self::build(
|
Self::build(
|
||||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_file<T: Into<String>>(
|
pub fn from_file<T: Into<String>>(
|
||||||
replica_id: ReplicaId,
|
replica_id: ReplicaId,
|
||||||
base_text: T,
|
base_text: T,
|
||||||
|
diff_base: Option<T>,
|
||||||
file: Arc<dyn File>,
|
file: Arc<dyn File>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::build(
|
Self::build(
|
||||||
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()),
|
||||||
|
diff_base.map(|h| h.into().into_boxed_str().into()),
|
||||||
Some(file),
|
Some(file),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -349,7 +363,11 @@ impl Buffer {
|
||||||
file: Option<Arc<dyn File>>,
|
file: Option<Arc<dyn File>>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let buffer = TextBuffer::new(replica_id, message.id, message.base_text);
|
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(
|
this.text.set_line_ending(proto::deserialize_line_ending(
|
||||||
proto::LineEnding::from_i32(message.line_ending)
|
proto::LineEnding::from_i32(message.line_ending)
|
||||||
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
.ok_or_else(|| anyhow!("missing line_ending"))?,
|
||||||
|
@ -362,6 +380,7 @@ impl Buffer {
|
||||||
id: self.remote_id(),
|
id: self.remote_id(),
|
||||||
file: self.file.as_ref().map(|f| f.to_proto()),
|
file: self.file.as_ref().map(|f| f.to_proto()),
|
||||||
base_text: self.base_text().to_string(),
|
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,
|
line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -404,7 +423,7 @@ impl Buffer {
|
||||||
self
|
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() {
|
let saved_mtime = if let Some(file) = file.as_ref() {
|
||||||
file.mtime()
|
file.mtime()
|
||||||
} else {
|
} else {
|
||||||
|
@ -418,6 +437,12 @@ impl Buffer {
|
||||||
transaction_depth: 0,
|
transaction_depth: 0,
|
||||||
was_dirty_before_starting_transaction: None,
|
was_dirty_before_starting_transaction: None,
|
||||||
text: buffer,
|
text: buffer,
|
||||||
|
diff_base,
|
||||||
|
git_diff_status: GitDiffStatus {
|
||||||
|
diff: git::diff::BufferDiff::new(),
|
||||||
|
update_in_progress: false,
|
||||||
|
update_requested: false,
|
||||||
|
},
|
||||||
file,
|
file,
|
||||||
syntax_map: Mutex::new(SyntaxMap::new()),
|
syntax_map: Mutex::new(SyntaxMap::new()),
|
||||||
parsing_in_background: false,
|
parsing_in_background: false,
|
||||||
|
@ -432,6 +457,7 @@ impl Buffer {
|
||||||
diagnostics_update_count: 0,
|
diagnostics_update_count: 0,
|
||||||
diagnostics_timestamp: Default::default(),
|
diagnostics_timestamp: Default::default(),
|
||||||
file_update_count: 0,
|
file_update_count: 0,
|
||||||
|
git_diff_update_count: 0,
|
||||||
completion_triggers: Default::default(),
|
completion_triggers: Default::default(),
|
||||||
completion_triggers_timestamp: Default::default(),
|
completion_triggers_timestamp: Default::default(),
|
||||||
deferred_ops: OperationQueue::new(),
|
deferred_ops: OperationQueue::new(),
|
||||||
|
@ -447,11 +473,13 @@ impl Buffer {
|
||||||
BufferSnapshot {
|
BufferSnapshot {
|
||||||
text,
|
text,
|
||||||
syntax,
|
syntax,
|
||||||
|
git_diff: self.git_diff_status.diff.clone(),
|
||||||
file: self.file.clone(),
|
file: self.file.clone(),
|
||||||
remote_selections: self.remote_selections.clone(),
|
remote_selections: self.remote_selections.clone(),
|
||||||
diagnostics: self.diagnostics.clone(),
|
diagnostics: self.diagnostics.clone(),
|
||||||
diagnostics_update_count: self.diagnostics_update_count,
|
diagnostics_update_count: self.diagnostics_update_count,
|
||||||
file_update_count: self.file_update_count,
|
file_update_count: self.file_update_count,
|
||||||
|
git_diff_update_count: self.git_diff_update_count,
|
||||||
language: self.language.clone(),
|
language: self.language.clone(),
|
||||||
parse_count: self.parse_count,
|
parse_count: self.parse_count,
|
||||||
selections_update_count: self.selections_update_count,
|
selections_update_count: self.selections_update_count,
|
||||||
|
@ -584,6 +612,7 @@ impl Buffer {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
self.git_diff_recalc(cx);
|
||||||
cx.emit(Event::Reloaded);
|
cx.emit(Event::Reloaded);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -633,6 +662,55 @@ impl Buffer {
|
||||||
task
|
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>) {
|
pub fn close(&mut self, cx: &mut ModelContext<Self>) {
|
||||||
cx.emit(Event::Closed);
|
cx.emit(Event::Closed);
|
||||||
}
|
}
|
||||||
|
@ -657,6 +735,10 @@ impl Buffer {
|
||||||
self.file_update_count
|
self.file_update_count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn git_diff_update_count(&self) -> usize {
|
||||||
|
self.git_diff_update_count
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn is_parsing(&self) -> bool {
|
pub fn is_parsing(&self) -> bool {
|
||||||
self.parsing_in_background
|
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>(
|
pub fn diagnostics_in_range<'a, T, O>(
|
||||||
&'a self,
|
&'a self,
|
||||||
search_range: Range<T>,
|
search_range: Range<T>,
|
||||||
|
@ -2186,6 +2275,10 @@ impl BufferSnapshot {
|
||||||
pub fn file_update_count(&self) -> usize {
|
pub fn file_update_count(&self) -> usize {
|
||||||
self.file_update_count
|
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 {
|
pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize {
|
||||||
|
@ -2212,6 +2305,7 @@ impl Clone for BufferSnapshot {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: self.text.clone(),
|
text: self.text.clone(),
|
||||||
|
git_diff: self.git_diff.clone(),
|
||||||
syntax: self.syntax.clone(),
|
syntax: self.syntax.clone(),
|
||||||
file: self.file.clone(),
|
file: self.file.clone(),
|
||||||
remote_selections: self.remote_selections.clone(),
|
remote_selections: self.remote_selections.clone(),
|
||||||
|
@ -2219,6 +2313,7 @@ impl Clone for BufferSnapshot {
|
||||||
selections_update_count: self.selections_update_count,
|
selections_update_count: self.selections_update_count,
|
||||||
diagnostics_update_count: self.diagnostics_update_count,
|
diagnostics_update_count: self.diagnostics_update_count,
|
||||||
file_update_count: self.file_update_count,
|
file_update_count: self.file_update_count,
|
||||||
|
git_diff_update_count: self.git_diff_update_count,
|
||||||
language: self.language.clone(),
|
language: self.language.clone(),
|
||||||
parse_count: self.parse_count,
|
parse_count: self.parse_count,
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ collections = { path = "../collections" }
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
fsevent = { path = "../fsevent" }
|
fsevent = { path = "../fsevent" }
|
||||||
fuzzy = { path = "../fuzzy" }
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
git = { path = "../git" }
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
language = { path = "../language" }
|
language = { path = "../language" }
|
||||||
lsp = { path = "../lsp" }
|
lsp = { path = "../lsp" }
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use fsevent::EventStream;
|
use fsevent::EventStream;
|
||||||
use futures::{future::BoxFuture, Stream, StreamExt};
|
use futures::{future::BoxFuture, Stream, StreamExt};
|
||||||
|
use git::repository::{GitRepository, LibGitRepository};
|
||||||
use language::LineEnding;
|
use language::LineEnding;
|
||||||
|
use parking_lot::Mutex as SyncMutex;
|
||||||
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
use smol::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{
|
use std::{
|
||||||
io,
|
io,
|
||||||
os::unix::fs::MetadataExt,
|
os::unix::fs::MetadataExt,
|
||||||
|
@ -11,13 +14,16 @@ use std::{
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use text::Rope;
|
use text::Rope;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use collections::{btree_map, BTreeMap};
|
use collections::{btree_map, BTreeMap};
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
use futures::lock::Mutex;
|
use futures::lock::Mutex;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[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]
|
#[async_trait::async_trait]
|
||||||
pub trait Fs: Send + Sync {
|
pub trait Fs: Send + Sync {
|
||||||
|
@ -42,6 +48,7 @@ pub trait Fs: Send + Sync {
|
||||||
path: &Path,
|
path: &Path,
|
||||||
latency: Duration,
|
latency: Duration,
|
||||||
) -> Pin<Box<dyn Send + Stream<Item = Vec<fsevent::Event>>>>;
|
) -> 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;
|
fn is_fake(&self) -> bool;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fn as_fake(&self) -> &FakeFs;
|
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 {
|
fn is_fake(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
@ -270,6 +285,7 @@ enum FakeFsEntry {
|
||||||
inode: u64,
|
inode: u64,
|
||||||
mtime: SystemTime,
|
mtime: SystemTime,
|
||||||
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
entries: BTreeMap<String, Arc<Mutex<FakeFsEntry>>>,
|
||||||
|
git_repo_state: Option<Arc<SyncMutex<git::repository::FakeGitRepositoryState>>>,
|
||||||
},
|
},
|
||||||
Symlink {
|
Symlink {
|
||||||
target: PathBuf,
|
target: PathBuf,
|
||||||
|
@ -384,6 +400,7 @@ impl FakeFs {
|
||||||
inode: 0,
|
inode: 0,
|
||||||
mtime: SystemTime::now(),
|
mtime: SystemTime::now(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
|
git_repo_state: None,
|
||||||
})),
|
})),
|
||||||
next_inode: 1,
|
next_inode: 1,
|
||||||
event_txs: Default::default(),
|
event_txs: Default::default(),
|
||||||
|
@ -473,6 +490,29 @@ impl FakeFs {
|
||||||
.boxed()
|
.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> {
|
pub async fn files(&self) -> Vec<PathBuf> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut queue = collections::VecDeque::new();
|
let mut queue = collections::VecDeque::new();
|
||||||
|
@ -562,6 +602,7 @@ impl Fs for FakeFs {
|
||||||
inode,
|
inode,
|
||||||
mtime: SystemTime::now(),
|
mtime: SystemTime::now(),
|
||||||
entries: Default::default(),
|
entries: Default::default(),
|
||||||
|
git_repo_state: None,
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
Ok(())
|
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 {
|
fn is_fake(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,14 @@ pub mod worktree;
|
||||||
mod project_tests;
|
mod project_tests;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
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 clock::ReplicaId;
|
||||||
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
use collections::{hash_map, BTreeMap, HashMap, HashSet};
|
||||||
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle,
|
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_id);
|
||||||
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
client.add_model_request_handler(Self::handle_open_buffer_by_path);
|
||||||
client.add_model_request_handler(Self::handle_save_buffer);
|
client.add_model_request_handler(Self::handle_save_buffer);
|
||||||
|
client.add_model_message_handler(Self::handle_update_diff_base);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local(
|
pub fn local(
|
||||||
|
@ -4533,8 +4538,11 @@ impl Project {
|
||||||
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
|
fn add_worktree(&mut self, worktree: &ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
|
||||||
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
cx.observe(worktree, |_, _, cx| cx.notify()).detach();
|
||||||
if worktree.read(cx).is_local() {
|
if worktree.read(cx).is_local() {
|
||||||
cx.subscribe(worktree, |this, worktree, _, cx| {
|
cx.subscribe(worktree, |this, worktree, event, cx| match event {
|
||||||
this.update_local_worktree_buffers(worktree, cx);
|
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();
|
.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>) {
|
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||||
let new_active_entry = entry.and_then(|project_path| {
|
let new_active_entry = entry.and_then(|project_path| {
|
||||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
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(
|
async fn handle_update_buffer_file(
|
||||||
this: ModelHandle<Self>,
|
this: ModelHandle<Self>,
|
||||||
envelope: TypedEnvelope<proto::UpdateBufferFile>,
|
envelope: TypedEnvelope<proto::UpdateBufferFile>,
|
||||||
|
@ -5780,7 +5861,7 @@ impl Project {
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||||
let mut opened_buffer_rx = self.opened_buffer.1.clone();
|
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 = loop {
|
||||||
let buffer = this.read_with(&cx, |this, cx| {
|
let buffer = this.read_with(&cx, |this, cx| {
|
||||||
this.opened_buffers
|
this.opened_buffers
|
||||||
|
@ -5798,6 +5879,7 @@ impl Project {
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
|
.ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?;
|
||||||
};
|
};
|
||||||
|
buffer.update(&mut cx, |buffer, cx| buffer.git_diff_recalc(cx));
|
||||||
Ok(buffer)
|
Ok(buffer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
fs::{self, Fs},
|
fs::{self, Fs},
|
||||||
ignore::IgnoreStack,
|
ignore::IgnoreStack,
|
||||||
DiagnosticSummary,
|
DiagnosticSummary,
|
||||||
};
|
};
|
||||||
|
use crate::{copy_recursive, ProjectEntryId, RemoveOptions};
|
||||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use client::{proto, Client};
|
use client::{proto, Client};
|
||||||
|
@ -18,6 +17,8 @@ use futures::{
|
||||||
Stream, StreamExt,
|
Stream, StreamExt,
|
||||||
};
|
};
|
||||||
use fuzzy::CharBag;
|
use fuzzy::CharBag;
|
||||||
|
use git::repository::GitRepository;
|
||||||
|
use git::{DOT_GIT, GITIGNORE};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||||
Task,
|
Task,
|
||||||
|
@ -26,12 +27,12 @@ use language::{
|
||||||
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
proto::{deserialize_version, serialize_line_ending, serialize_version},
|
||||||
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope,
|
||||||
};
|
};
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::{
|
use postage::{
|
||||||
prelude::{Sink as _, Stream as _},
|
prelude::{Sink as _, Stream as _},
|
||||||
watch,
|
watch,
|
||||||
};
|
};
|
||||||
|
|
||||||
use smol::channel::{self, Sender};
|
use smol::channel::{self, Sender};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
|
@ -40,6 +41,7 @@ use std::{
|
||||||
ffi::{OsStr, OsString},
|
ffi::{OsStr, OsString},
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
|
mem,
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
os::unix::prelude::{OsStrExt, OsStringExt},
|
os::unix::prelude::{OsStrExt, OsStringExt},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -50,10 +52,6 @@ use std::{
|
||||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||||
pub struct WorktreeId(usize);
|
pub struct WorktreeId(usize);
|
||||||
|
|
||||||
|
@ -101,15 +99,51 @@ pub struct Snapshot {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[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 {
|
pub struct LocalSnapshot {
|
||||||
abs_path: Arc<Path>,
|
abs_path: Arc<Path>,
|
||||||
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
|
||||||
|
git_repositories: Vec<GitRepositoryEntry>,
|
||||||
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
removed_entry_ids: HashMap<u64, ProjectEntryId>,
|
||||||
next_entry_id: Arc<AtomicUsize>,
|
next_entry_id: Arc<AtomicUsize>,
|
||||||
snapshot: Snapshot,
|
snapshot: Snapshot,
|
||||||
extension_counts: HashMap<OsString, usize>,
|
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 {
|
impl Deref for LocalSnapshot {
|
||||||
type Target = Snapshot;
|
type Target = Snapshot;
|
||||||
|
|
||||||
|
@ -142,6 +176,7 @@ struct ShareState {
|
||||||
|
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
UpdatedEntries,
|
UpdatedEntries,
|
||||||
|
UpdatedGitRepositories(Vec<GitRepositoryEntry>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for Worktree {
|
impl Entity for Worktree {
|
||||||
|
@ -372,6 +407,7 @@ impl LocalWorktree {
|
||||||
let mut snapshot = LocalSnapshot {
|
let mut snapshot = LocalSnapshot {
|
||||||
abs_path,
|
abs_path,
|
||||||
ignores_by_parent_abs_path: Default::default(),
|
ignores_by_parent_abs_path: Default::default(),
|
||||||
|
git_repositories: Default::default(),
|
||||||
removed_entry_ids: Default::default(),
|
removed_entry_ids: Default::default(),
|
||||||
next_entry_id,
|
next_entry_id,
|
||||||
snapshot: Snapshot {
|
snapshot: Snapshot {
|
||||||
|
@ -446,10 +482,14 @@ impl LocalWorktree {
|
||||||
) -> Task<Result<ModelHandle<Buffer>>> {
|
) -> Task<Result<ModelHandle<Buffer>>> {
|
||||||
let path = Arc::from(path);
|
let path = Arc::from(path);
|
||||||
cx.spawn(move |this, mut cx| async move {
|
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))
|
.update(&mut cx, |t, cx| t.as_local().unwrap().load(&path, cx))
|
||||||
.await?;
|
.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>) {
|
fn poll_snapshot(&mut self, force: bool, cx: &mut ModelContext<Worktree>) {
|
||||||
self.poll_task.take();
|
self.poll_task.take();
|
||||||
|
|
||||||
match self.scan_state() {
|
match self.scan_state() {
|
||||||
ScanState::Idle => {
|
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() {
|
if let Some(share) = self.share.as_mut() {
|
||||||
*share.snapshots_tx.borrow_mut() = self.snapshot.clone();
|
*share.snapshots_tx.borrow_mut() = self.snapshot.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(Event::UpdatedEntries);
|
cx.emit(Event::UpdatedEntries);
|
||||||
|
|
||||||
|
if !updated_repos.is_empty() {
|
||||||
|
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ScanState::Initializing => {
|
ScanState::Initializing => {
|
||||||
let is_fake_fs = self.fs.is_fake();
|
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 {
|
self.poll_task = Some(cx.spawn_weak(|this, mut cx| async move {
|
||||||
if is_fake_fs {
|
if is_fake_fs {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
@ -521,17 +581,52 @@ impl LocalWorktree {
|
||||||
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
cx.emit(Event::UpdatedEntries);
|
cx.emit(Event::UpdatedEntries);
|
||||||
|
|
||||||
|
if !updated_repos.is_empty() {
|
||||||
|
cx.emit(Event::UpdatedGitRepositories(updated_repos));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
if force {
|
if force {
|
||||||
self.snapshot = self.background_snapshot.lock().clone();
|
self.snapshot = self.background_snapshot.lock().clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.notify();
|
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 = ()> {
|
pub fn scan_complete(&self) -> impl Future<Output = ()> {
|
||||||
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
let mut scan_state_rx = self.last_scan_state_rx.clone();
|
||||||
async move {
|
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 handle = cx.handle();
|
||||||
let path = Arc::from(path);
|
let path = Arc::from(path);
|
||||||
let abs_path = self.absolutize(&path);
|
let abs_path = self.absolutize(&path);
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
|
let snapshot = self.snapshot();
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let text = fs.load(&abs_path).await?;
|
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
|
// Eagerly populate the snapshot with an updated entry for the loaded file
|
||||||
let entry = this
|
let entry = this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
|
@ -573,6 +686,7 @@ impl LocalWorktree {
|
||||||
.refresh_entry(path, abs_path, None, cx)
|
.refresh_entry(path, abs_path, None, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
File {
|
File {
|
||||||
entry_id: Some(entry.id),
|
entry_id: Some(entry.id),
|
||||||
|
@ -582,6 +696,7 @@ impl LocalWorktree {
|
||||||
is_local: true,
|
is_local: true,
|
||||||
},
|
},
|
||||||
text,
|
text,
|
||||||
|
diff_base,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1248,6 +1363,22 @@ impl LocalSnapshot {
|
||||||
&self.extension_counts
|
&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)]
|
#[cfg(test)]
|
||||||
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
|
pub(crate) fn build_initial_update(&self, project_id: u64) -> proto::UpdateWorktree {
|
||||||
let root_name = self.root_name.clone();
|
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 {
|
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);
|
let abs_path = self.abs_path.join(&entry.path);
|
||||||
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
match smol::block_on(build_gitignore(&abs_path, fs)) {
|
||||||
Ok(ignore) => {
|
Ok(ignore) => {
|
||||||
|
@ -1384,6 +1515,7 @@ impl LocalSnapshot {
|
||||||
parent_path: Arc<Path>,
|
parent_path: Arc<Path>,
|
||||||
entries: impl IntoIterator<Item = Entry>,
|
entries: impl IntoIterator<Item = Entry>,
|
||||||
ignore: Option<Arc<Gitignore>>,
|
ignore: Option<Arc<Gitignore>>,
|
||||||
|
fs: &dyn Fs,
|
||||||
) {
|
) {
|
||||||
let mut parent_entry = if let Some(parent_entry) =
|
let mut parent_entry = if let Some(parent_entry) =
|
||||||
self.entries_by_path.get(&PathKey(parent_path.clone()), &())
|
self.entries_by_path.get(&PathKey(parent_path.clone()), &())
|
||||||
|
@ -1409,6 +1541,27 @@ impl LocalSnapshot {
|
||||||
unreachable!();
|
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_path_edits = vec![Edit::Insert(parent_entry)];
|
||||||
let mut entries_by_id_edits = Vec::new();
|
let mut entries_by_id_edits = Vec::new();
|
||||||
|
|
||||||
|
@ -1493,6 +1646,14 @@ impl LocalSnapshot {
|
||||||
{
|
{
|
||||||
*scan_id = self.snapshot.scan_id;
|
*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
|
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> {
|
async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
|
||||||
|
@ -2244,9 +2421,12 @@ impl BackgroundScanner {
|
||||||
new_entries.push(child_entry);
|
new_entries.push(child_entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.snapshot
|
self.snapshot.lock().populate_dir(
|
||||||
.lock()
|
job.path.clone(),
|
||||||
.populate_dir(job.path.clone(), new_entries, new_ignore);
|
new_entries,
|
||||||
|
new_ignore,
|
||||||
|
self.fs.as_ref(),
|
||||||
|
);
|
||||||
for new_job in new_jobs {
|
for new_job in new_jobs {
|
||||||
job.scan_queue.send(new_job).await.unwrap();
|
job.scan_queue.send(new_job).await.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -2321,6 +2501,11 @@ impl BackgroundScanner {
|
||||||
fs_entry.is_ignored = ignore_stack.is_all();
|
fs_entry.is_ignored = ignore_stack.is_all();
|
||||||
snapshot.insert_entry(fs_entry, self.fs.as_ref());
|
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);
|
let mut ancestor_inodes = snapshot.ancestor_inodes_for_path(&path);
|
||||||
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
if metadata.is_dir && !ancestor_inodes.contains(&metadata.inode) {
|
||||||
ancestor_inodes.insert(metadata.inode);
|
ancestor_inodes.insert(metadata.inode);
|
||||||
|
@ -2367,6 +2552,7 @@ impl BackgroundScanner {
|
||||||
self.snapshot.lock().removed_entry_ids.clear();
|
self.snapshot.lock().removed_entry_ids.clear();
|
||||||
|
|
||||||
self.update_ignore_statuses().await;
|
self.update_ignore_statuses().await;
|
||||||
|
self.update_git_repositories();
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2432,6 +2618,13 @@ impl BackgroundScanner {
|
||||||
.await;
|
.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) {
|
async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
|
||||||
let mut ignore_stack = job.ignore_stack;
|
let mut ignore_stack = job.ignore_stack;
|
||||||
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
|
if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
|
||||||
|
@ -2778,6 +2971,7 @@ mod tests {
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::test::FakeHttpClient;
|
use client::test::FakeHttpClient;
|
||||||
use fs::RealFs;
|
use fs::RealFs;
|
||||||
|
use git::repository::FakeGitRepository;
|
||||||
use gpui::{executor::Deterministic, TestAppContext};
|
use gpui::{executor::Deterministic, TestAppContext};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
@ -2786,6 +2980,7 @@ mod tests {
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use util::test::temp_tree;
|
use util::test::temp_tree;
|
||||||
|
|
||||||
#[gpui::test]
|
#[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]
|
#[gpui::test]
|
||||||
async fn test_write_file(cx: &mut TestAppContext) {
|
async fn test_write_file(cx: &mut TestAppContext) {
|
||||||
let dir = temp_tree(json!({
|
let dir = temp_tree(json!({
|
||||||
|
@ -3123,6 +3447,7 @@ mod tests {
|
||||||
abs_path: root_dir.path().into(),
|
abs_path: root_dir.path().into(),
|
||||||
removed_entry_ids: Default::default(),
|
removed_entry_ids: Default::default(),
|
||||||
ignores_by_parent_abs_path: Default::default(),
|
ignores_by_parent_abs_path: Default::default(),
|
||||||
|
git_repositories: Default::default(),
|
||||||
next_entry_id: next_entry_id.clone(),
|
next_entry_id: next_entry_id.clone(),
|
||||||
snapshot: Snapshot {
|
snapshot: Snapshot {
|
||||||
id: WorktreeId::from_usize(0),
|
id: WorktreeId::from_usize(0),
|
||||||
|
|
|
@ -108,9 +108,9 @@ message Envelope {
|
||||||
FollowResponse follow_response = 93;
|
FollowResponse follow_response = 93;
|
||||||
UpdateFollowers update_followers = 94;
|
UpdateFollowers update_followers = 94;
|
||||||
Unfollow unfollow = 95;
|
Unfollow unfollow = 95;
|
||||||
|
|
||||||
GetPrivateUserInfo get_private_user_info = 96;
|
GetPrivateUserInfo get_private_user_info = 96;
|
||||||
GetPrivateUserInfoResponse get_private_user_info_response = 97;
|
GetPrivateUserInfoResponse get_private_user_info_response = 97;
|
||||||
|
UpdateDiffBase update_diff_base = 98;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -831,7 +831,8 @@ message BufferState {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
optional File file = 2;
|
optional File file = 2;
|
||||||
string base_text = 3;
|
string base_text = 3;
|
||||||
LineEnding line_ending = 4;
|
optional string diff_base = 4;
|
||||||
|
LineEnding line_ending = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BufferChunk {
|
message BufferChunk {
|
||||||
|
@ -1001,3 +1002,9 @@ message WorktreeMetadata {
|
||||||
string root_name = 2;
|
string root_name = 2;
|
||||||
bool visible = 3;
|
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),
|
(UpdateProject, Foreground),
|
||||||
(UpdateWorktree, Foreground),
|
(UpdateWorktree, Foreground),
|
||||||
(UpdateWorktreeExtensions, Background),
|
(UpdateWorktreeExtensions, Background),
|
||||||
|
(UpdateDiffBase, Background),
|
||||||
(GetPrivateUserInfo, Foreground),
|
(GetPrivateUserInfo, Foreground),
|
||||||
(GetPrivateUserInfoResponse, Foreground),
|
(GetPrivateUserInfoResponse, Foreground),
|
||||||
);
|
);
|
||||||
|
@ -266,6 +267,7 @@ entity_messages!(
|
||||||
UpdateProject,
|
UpdateProject,
|
||||||
UpdateWorktree,
|
UpdateWorktree,
|
||||||
UpdateWorktreeExtensions,
|
UpdateWorktreeExtensions,
|
||||||
|
UpdateDiffBase
|
||||||
);
|
);
|
||||||
|
|
||||||
entity_messages!(channel_id, ChannelMessageSent);
|
entity_messages!(channel_id, ChannelMessageSent);
|
||||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
||||||
pub use peer::*;
|
pub use peer::*;
|
||||||
mod macros;
|
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 default_dock_anchor: DockAnchor,
|
||||||
pub editor_defaults: EditorSettings,
|
pub editor_defaults: EditorSettings,
|
||||||
pub editor_overrides: EditorSettings,
|
pub editor_overrides: EditorSettings,
|
||||||
|
pub git: GitSettings,
|
||||||
|
pub git_overrides: GitSettings,
|
||||||
pub terminal_defaults: TerminalSettings,
|
pub terminal_defaults: TerminalSettings,
|
||||||
pub terminal_overrides: TerminalSettings,
|
pub terminal_overrides: TerminalSettings,
|
||||||
pub language_defaults: HashMap<Arc<str>, EditorSettings>,
|
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)]
|
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||||
pub struct EditorSettings {
|
pub struct EditorSettings {
|
||||||
pub tab_size: Option<NonZeroU32>,
|
pub tab_size: Option<NonZeroU32>,
|
||||||
|
@ -196,6 +214,8 @@ pub struct SettingsFileContent {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub terminal: TerminalSettings,
|
pub terminal: TerminalSettings,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub git: Option<GitSettings>,
|
||||||
|
#[serde(default)]
|
||||||
#[serde(alias = "language_overrides")]
|
#[serde(alias = "language_overrides")]
|
||||||
pub languages: HashMap<Arc<str>, EditorSettings>,
|
pub languages: HashMap<Arc<str>, EditorSettings>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -252,6 +272,8 @@ impl Settings {
|
||||||
enable_language_server: required(defaults.editor.enable_language_server),
|
enable_language_server: required(defaults.editor.enable_language_server),
|
||||||
},
|
},
|
||||||
editor_overrides: Default::default(),
|
editor_overrides: Default::default(),
|
||||||
|
git: defaults.git.unwrap(),
|
||||||
|
git_overrides: Default::default(),
|
||||||
terminal_defaults: Default::default(),
|
terminal_defaults: Default::default(),
|
||||||
terminal_overrides: Default::default(),
|
terminal_overrides: Default::default(),
|
||||||
language_defaults: defaults.languages,
|
language_defaults: defaults.languages,
|
||||||
|
@ -303,6 +325,7 @@ impl Settings {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.editor_overrides = data.editor;
|
self.editor_overrides = data.editor;
|
||||||
|
self.git_overrides = data.git.unwrap_or_default();
|
||||||
self.terminal_defaults.font_size = data.terminal.font_size;
|
self.terminal_defaults.font_size = data.terminal.font_size;
|
||||||
self.terminal_overrides = data.terminal;
|
self.terminal_overrides = data.terminal;
|
||||||
self.language_overrides = data.languages;
|
self.language_overrides = data.languages;
|
||||||
|
@ -358,6 +381,14 @@ impl Settings {
|
||||||
.expect("missing default")
|
.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"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub fn test(cx: &gpui::AppContext) -> Settings {
|
pub fn test(cx: &gpui::AppContext) -> Settings {
|
||||||
Settings {
|
Settings {
|
||||||
|
@ -382,6 +413,8 @@ impl Settings {
|
||||||
editor_overrides: Default::default(),
|
editor_overrides: Default::default(),
|
||||||
terminal_defaults: Default::default(),
|
terminal_defaults: Default::default(),
|
||||||
terminal_overrides: Default::default(),
|
terminal_overrides: Default::default(),
|
||||||
|
git: Default::default(),
|
||||||
|
git_overrides: Default::default(),
|
||||||
language_defaults: Default::default(),
|
language_defaults: Default::default(),
|
||||||
language_overrides: Default::default(),
|
language_overrides: Default::default(),
|
||||||
lsp: Default::default(),
|
lsp: Default::default(),
|
||||||
|
|
|
@ -101,6 +101,12 @@ pub enum Bias {
|
||||||
Right,
|
Right,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Bias {
|
||||||
|
fn default() -> Self {
|
||||||
|
Bias::Left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl PartialOrd for Bias {
|
impl PartialOrd for Bias {
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
Some(self.cmp(other))
|
Some(self.cmp(other))
|
||||||
|
|
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
||||||
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
|
||||||
pub struct Anchor {
|
pub struct Anchor {
|
||||||
pub timestamp: clock::Local,
|
pub timestamp: clock::Local,
|
||||||
pub offset: usize,
|
pub offset: usize,
|
||||||
|
|
|
@ -54,6 +54,13 @@ impl Rope {
|
||||||
cursor.slice(range.end)
|
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) {
|
pub fn push(&mut self, text: &str) {
|
||||||
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
let mut new_chunks = SmallVec::<[_; 16]>::new();
|
||||||
let mut new_chunk = ArrayString::new();
|
let mut new_chunk = ArrayString::new();
|
||||||
|
|
|
@ -488,8 +488,7 @@ pub struct Editor {
|
||||||
pub rename_fade: f32,
|
pub rename_fade: f32,
|
||||||
pub document_highlight_read_background: Color,
|
pub document_highlight_read_background: Color,
|
||||||
pub document_highlight_write_background: Color,
|
pub document_highlight_write_background: Color,
|
||||||
pub diff_background_deleted: Color,
|
pub diff: DiffStyle,
|
||||||
pub diff_background_inserted: Color,
|
|
||||||
pub line_number: Color,
|
pub line_number: Color,
|
||||||
pub line_number_active: Color,
|
pub line_number_active: Color,
|
||||||
pub guest_selections: Vec<SelectionStyle>,
|
pub guest_selections: Vec<SelectionStyle>,
|
||||||
|
@ -573,6 +572,16 @@ pub struct CodeActions {
|
||||||
pub vertical_scale: f32,
|
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)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct Interactive<T> {
|
pub struct Interactive<T> {
|
||||||
pub default: T,
|
pub default: T,
|
||||||
|
|
|
@ -7,17 +7,21 @@ edition = "2021"
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = ["rand", "serde_json", "tempdir"]
|
test-support = ["rand", "serde_json", "tempdir", "git2"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.38"
|
anyhow = "1.0.38"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
tempdir = { version = "0.3.7", optional = true }
|
tempdir = { version = "0.3.7", optional = true }
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
|
serde_json = { version = "1.0", features = ["preserve_order"], optional = true }
|
||||||
|
git2 = { version = "0.15", default-features = false, optional = true }
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = { version = "0.8" }
|
rand = { version = "0.8" }
|
||||||
tempdir = { version = "0.3.7" }
|
tempdir = { version = "0.3.7" }
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||||
|
git2 = { version = "0.15", default-features = false }
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
mod assertions;
|
mod assertions;
|
||||||
mod marked_text;
|
mod marked_text;
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use git2;
|
||||||
|
use std::{
|
||||||
|
ffi::OsStr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
use tempdir::TempDir;
|
use tempdir::TempDir;
|
||||||
|
|
||||||
pub use assertions::*;
|
pub use assertions::*;
|
||||||
|
@ -24,6 +28,11 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
|
||||||
match contents {
|
match contents {
|
||||||
Value::Object(_) => {
|
Value::Object(_) => {
|
||||||
fs::create_dir(&path).unwrap();
|
fs::create_dir(&path).unwrap();
|
||||||
|
|
||||||
|
if path.file_name() == Some(&OsStr::new(".git")) {
|
||||||
|
git2::Repository::init(&path.parent().unwrap()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
write_tree(&path, contents);
|
write_tree(&path, contents);
|
||||||
}
|
}
|
||||||
Value::Null => {
|
Value::Null => {
|
||||||
|
|
|
@ -52,7 +52,6 @@ use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
fmt,
|
fmt,
|
||||||
future::Future,
|
future::Future,
|
||||||
mem,
|
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
@ -318,7 +317,23 @@ pub trait Item: View {
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>>;
|
) -> 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 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(
|
fn act_as_type(
|
||||||
&self,
|
&self,
|
||||||
type_id: TypeId,
|
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 {
|
pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
fn subscribe_to_item_events(
|
fn subscribe_to_item_events(
|
||||||
&self,
|
&self,
|
||||||
|
@ -473,6 +539,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||||
) -> Task<Result<()>>;
|
) -> Task<Result<()>>;
|
||||||
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
||||||
-> Task<Result<()>>;
|
-> 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 act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
|
||||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||||
fn on_release(
|
fn on_release(
|
||||||
|
@ -578,8 +649,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||||
.insert(self.id(), pane.downgrade())
|
.insert(self.id(), pane.downgrade())
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
let mut pending_autosave = None;
|
let mut pending_autosave = DelayedDebouncedEditAction::new();
|
||||||
let mut cancel_pending_autosave = oneshot::channel::<()>().0;
|
let mut pending_git_update = DelayedDebouncedEditAction::new();
|
||||||
let pending_update = Rc::new(RefCell::new(None));
|
let pending_update = Rc::new(RefCell::new(None));
|
||||||
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
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);
|
.detach_and_log_err(cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemEvent::UpdateTab => {
|
ItemEvent::UpdateTab => {
|
||||||
pane.update(cx, |_, cx| {
|
pane.update(cx, |_, cx| {
|
||||||
cx.emit(pane::Event::ChangeItemTitle);
|
cx.emit(pane::Event::ChangeItemTitle);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemEvent::Edit => {
|
ItemEvent::Edit => {
|
||||||
if let Autosave::AfterDelay { milliseconds } =
|
if let Autosave::AfterDelay { milliseconds } =
|
||||||
cx.global::<Settings>().autosave
|
cx.global::<Settings>().autosave
|
||||||
{
|
{
|
||||||
let prev_autosave = pending_autosave
|
let delay = Duration::from_millis(milliseconds);
|
||||||
.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 item = item.clone();
|
let item = item.clone();
|
||||||
pending_autosave =
|
pending_autosave.fire_new(
|
||||||
Some(cx.spawn_weak(|_, mut cx| async move {
|
delay,
|
||||||
let mut timer = cx
|
workspace,
|
||||||
.background()
|
cx,
|
||||||
.timer(Duration::from_millis(milliseconds))
|
|project, mut cx| async move {
|
||||||
.fuse();
|
|
||||||
prev_autosave.await;
|
|
||||||
futures::select_biased! {
|
|
||||||
_ = cancel_rx => return None,
|
|
||||||
_ = timer => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let project = project.upgrade(&cx)?;
|
|
||||||
cx.update(|cx| Pane::autosave_item(&item, project, cx))
|
cx.update(|cx| Pane::autosave_item(&item, project, cx))
|
||||||
.await
|
.await
|
||||||
.log_err();
|
.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))
|
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> {
|
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle> {
|
||||||
self.read(cx).act_as_type(type_id, self, cx)
|
self.read(cx).act_as_type(type_id, self, cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
player,
|
player,
|
||||||
popoverShadow,
|
popoverShadow,
|
||||||
text,
|
text,
|
||||||
|
textColor,
|
||||||
TextColor,
|
TextColor,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
import hoverPopover from "./hoverPopover";
|
import hoverPopover from "./hoverPopover";
|
||||||
|
@ -59,8 +60,14 @@ export default function editor(theme: Theme) {
|
||||||
indicator: iconColor(theme, "secondary"),
|
indicator: iconColor(theme, "secondary"),
|
||||||
verticalScale: 0.618
|
verticalScale: 0.618
|
||||||
},
|
},
|
||||||
diffBackgroundDeleted: backgroundColor(theme, "error"),
|
diff: {
|
||||||
diffBackgroundInserted: backgroundColor(theme, "ok"),
|
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,
|
documentHighlightReadBackground: theme.editor.highlight.occurrence,
|
||||||
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
|
documentHighlightWriteBackground: theme.editor.highlight.activeOccurrence,
|
||||||
errorColor: theme.textColor.error,
|
errorColor: theme.textColor.error,
|
||||||
|
|
|
@ -113,6 +113,11 @@ export function createTheme(
|
||||||
hovered: sample(ramps.blue, 0.1),
|
hovered: sample(ramps.blue, 0.1),
|
||||||
active: sample(ramps.blue, 0.15),
|
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 = {
|
const borderColor = {
|
||||||
|
|
|
@ -78,6 +78,7 @@ export default interface Theme {
|
||||||
// Hacks for elements on top of the editor
|
// Hacks for elements on top of the editor
|
||||||
on500: BackgroundColorSet;
|
on500: BackgroundColorSet;
|
||||||
ok: BackgroundColorSet;
|
ok: BackgroundColorSet;
|
||||||
|
on500Ok: BackgroundColorSet;
|
||||||
error: BackgroundColorSet;
|
error: BackgroundColorSet;
|
||||||
on500Error: BackgroundColorSet;
|
on500Error: BackgroundColorSet;
|
||||||
warning: BackgroundColorSet;
|
warning: BackgroundColorSet;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue