Merge pull request #948 from zed-industries/project-browser-refinements

Add commands for manipulating files in the project panel
This commit is contained in:
Antonio Scandurra
2022-05-05 15:54:59 +02:00
committed by GitHub
45 changed files with 2310 additions and 1258 deletions
Generated
+5 -2
View File
@@ -63,9 +63,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.42"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]]
name = "arrayref"
@@ -3329,7 +3329,10 @@ dependencies = [
name = "project_panel"
version = "0.1.0"
dependencies = [
"editor",
"futures",
"gpui",
"postage",
"project",
"serde_json",
"settings",
+3 -1
View File
@@ -331,7 +331,9 @@
"context": "ProjectPanel",
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry"
"right": "project_panel::ExpandSelectedEntry",
"f2": "project_panel::Rename",
"backspace": "project_panel::Delete"
}
}
]
+31 -29
View File
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
"indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#8b8792",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#8b8792",
"size": 14
},
"hover": {
"background": "#5852603d",
"text": {
"family": "Zed Mono",
"color": "#e2dfe7",
"size": 14
}
},
"active": {
"background": "#5852605c",
"text": {
"family": "Zed Mono",
"color": "#e2dfe7",
"size": 14
}
},
"active_hover": {
"background": "#5852603d",
"text": {
"family": "Zed Mono",
"color": "#efecf4",
"size": 14
}
}
},
"hovered_entry": {
"height": 24,
"background": "#5852603d",
"icon_color": "#8b8792",
"icon_size": 8,
"icon_spacing": 8,
"filename_editor": {
"background": "#26232a5c",
"text": {
"family": "Zed Mono",
"color": "#e2dfe7",
"size": 14
}
},
"selected_entry": {
"height": 24,
"icon_color": "#8b8792",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#e2dfe7",
"size": 14
}
},
"hovered_selected_entry": {
"height": 24,
"background": "#5852603d",
"icon_color": "#8b8792",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#efecf4",
"size": 14
},
"selection": {
"cursor": "#576ddb",
"selection": "#576ddb3d"
}
}
},
+31 -29
View File
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
"indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#585260",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#585260",
"size": 14
},
"hover": {
"background": "#8b87921f",
"text": {
"family": "Zed Mono",
"color": "#26232a",
"size": 14
}
},
"active": {
"background": "#8b87922e",
"text": {
"family": "Zed Mono",
"color": "#26232a",
"size": 14
}
},
"active_hover": {
"background": "#8b87921f",
"text": {
"family": "Zed Mono",
"color": "#19171c",
"size": 14
}
}
},
"hovered_entry": {
"height": 24,
"background": "#8b87921f",
"icon_color": "#585260",
"icon_size": 8,
"icon_spacing": 8,
"filename_editor": {
"background": "#e2dfe72e",
"text": {
"family": "Zed Mono",
"color": "#26232a",
"size": 14
}
},
"selected_entry": {
"height": 24,
"icon_color": "#585260",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#26232a",
"size": 14
}
},
"hovered_selected_entry": {
"height": 24,
"background": "#8b87921f",
"icon_color": "#585260",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#19171c",
"size": 14
},
"selection": {
"cursor": "#576ddb",
"selection": "#576ddb3d"
}
}
},
+31 -29
View File
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
"indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#555555",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#808080",
"size": 14
},
"hover": {
"background": "#232323",
"text": {
"family": "Zed Mono",
"color": "#f1f1f1",
"size": 14
}
},
"active": {
"background": "#2b2b2b",
"text": {
"family": "Zed Mono",
"color": "#f1f1f1",
"size": 14
}
},
"active_hover": {
"background": "#232323",
"text": {
"family": "Zed Mono",
"color": "#ffffff",
"size": 14
}
}
},
"hovered_entry": {
"height": 24,
"background": "#232323",
"icon_color": "#555555",
"icon_size": 8,
"icon_spacing": 8,
"filename_editor": {
"background": "#ffffff1f",
"text": {
"family": "Zed Mono",
"color": "#f1f1f1",
"size": 14
}
},
"selected_entry": {
"height": 24,
"icon_color": "#555555",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#f1f1f1",
"size": 14
}
},
"hovered_selected_entry": {
"height": 24,
"background": "#232323",
"icon_color": "#555555",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#ffffff",
"size": 14
},
"selection": {
"cursor": "#2472f2",
"selection": "#2472f23d"
}
}
},
+31 -29
View File
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
"indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#9c9c9c",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#636363",
"size": 14
},
"hover": {
"background": "#eaeaea",
"text": {
"family": "Zed Mono",
"color": "#2b2b2b",
"size": 14
}
},
"active": {
"background": "#e3e3e3",
"text": {
"family": "Zed Mono",
"color": "#2b2b2b",
"size": 14
}
},
"active_hover": {
"background": "#eaeaea",
"text": {
"family": "Zed Mono",
"color": "#000000",
"size": 14
}
}
},
"hovered_entry": {
"height": 24,
"background": "#eaeaea",
"icon_color": "#9c9c9c",
"icon_size": 8,
"icon_spacing": 8,
"filename_editor": {
"background": "#0000000f",
"text": {
"family": "Zed Mono",
"color": "#2b2b2b",
"size": 14
}
},
"selected_entry": {
"height": 24,
"icon_color": "#9c9c9c",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#2b2b2b",
"size": 14
}
},
"hovered_selected_entry": {
"height": 24,
"background": "#eaeaea",
"icon_color": "#9c9c9c",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#000000",
"size": 14
},
"selection": {
"cursor": "#2472f2",
"selection": "#2472f23d"
}
}
},
+31 -29
View File
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
"indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#93a1a1",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#93a1a1",
"size": 14
},
"hover": {
"background": "#586e753d",
"text": {
"family": "Zed Mono",
"color": "#eee8d5",
"size": 14
}
},
"active": {
"background": "#586e755c",
"text": {
"family": "Zed Mono",
"color": "#eee8d5",
"size": 14
}
},
"active_hover": {
"background": "#586e753d",
"text": {
"family": "Zed Mono",
"color": "#fdf6e3",
"size": 14
}
}
},
"hovered_entry": {
"height": 24,
"background": "#586e753d",
"icon_color": "#93a1a1",
"icon_size": 8,
"icon_spacing": 8,
"filename_editor": {
"background": "#0736425c",
"text": {
"family": "Zed Mono",
"color": "#eee8d5",
"size": 14
}
},
"selected_entry": {
"height": 24,
"icon_color": "#93a1a1",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#eee8d5",
"size": 14
}
},
"hovered_selected_entry": {
"height": 24,
"background": "#586e753d",
"icon_color": "#93a1a1",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#fdf6e3",
"size": 14
},
"selection": {
"cursor": "#268bd2",
"selection": "#268bd23d"
}
}
},
+31 -29
View File
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
"indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#586e75",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#586e75",
"size": 14
},
"hover": {
"background": "#93a1a11f",
"text": {
"family": "Zed Mono",
"color": "#073642",
"size": 14
}
},
"active": {
"background": "#93a1a12e",
"text": {
"family": "Zed Mono",
"color": "#073642",
"size": 14
}
},
"active_hover": {
"background": "#93a1a11f",
"text": {
"family": "Zed Mono",
"color": "#002b36",
"size": 14
}
}
},
"hovered_entry": {
"height": 24,
"background": "#93a1a11f",
"icon_color": "#586e75",
"icon_size": 8,
"icon_spacing": 8,
"filename_editor": {
"background": "#eee8d52e",
"text": {
"family": "Zed Mono",
"color": "#073642",
"size": 14
}
},
"selected_entry": {
"height": 24,
"icon_color": "#586e75",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#073642",
"size": 14
}
},
"hovered_selected_entry": {
"height": 24,
"background": "#93a1a11f",
"icon_color": "#586e75",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#002b36",
"size": 14
},
"selection": {
"cursor": "#268bd2",
"selection": "#268bd23d"
}
}
},
+31 -29
View File
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
"indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#979db4",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#979db4",
"size": 14
},
"hover": {
"background": "#5e66873d",
"text": {
"family": "Zed Mono",
"color": "#dfe2f1",
"size": 14
}
},
"active": {
"background": "#5e66875c",
"text": {
"family": "Zed Mono",
"color": "#dfe2f1",
"size": 14
}
},
"active_hover": {
"background": "#5e66873d",
"text": {
"family": "Zed Mono",
"color": "#f5f7ff",
"size": 14
}
}
},
"hovered_entry": {
"height": 24,
"background": "#5e66873d",
"icon_color": "#979db4",
"icon_size": 8,
"icon_spacing": 8,
"filename_editor": {
"background": "#2932565c",
"text": {
"family": "Zed Mono",
"color": "#dfe2f1",
"size": 14
}
},
"selected_entry": {
"height": 24,
"icon_color": "#979db4",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#dfe2f1",
"size": 14
}
},
"hovered_selected_entry": {
"height": 24,
"background": "#5e66873d",
"icon_color": "#979db4",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#f5f7ff",
"size": 14
},
"selection": {
"cursor": "#3d8fd1",
"selection": "#3d8fd13d"
}
}
},
+31 -29
View File
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
"indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#5e6687",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#5e6687",
"size": 14
},
"hover": {
"background": "#979db41f",
"text": {
"family": "Zed Mono",
"color": "#293256",
"size": 14
}
},
"active": {
"background": "#979db42e",
"text": {
"family": "Zed Mono",
"color": "#293256",
"size": 14
}
},
"active_hover": {
"background": "#979db41f",
"text": {
"family": "Zed Mono",
"color": "#202746",
"size": 14
}
}
},
"hovered_entry": {
"height": 24,
"background": "#979db41f",
"icon_color": "#5e6687",
"icon_size": 8,
"icon_spacing": 8,
"filename_editor": {
"background": "#dfe2f12e",
"text": {
"family": "Zed Mono",
"color": "#293256",
"size": 14
}
},
"selected_entry": {
"height": 24,
"icon_color": "#5e6687",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#293256",
"size": 14
}
},
"hovered_selected_entry": {
"height": 24,
"background": "#979db41f",
"icon_color": "#5e6687",
"icon_size": 8,
"icon_spacing": 8,
"text": {
"family": "Zed Mono",
"color": "#202746",
"size": 14
},
"selection": {
"cursor": "#3d8fd1",
"selection": "#3d8fd13d"
}
}
},
+1 -1
View File
@@ -270,7 +270,7 @@ impl View for AutoUpdateIndicator {
)
.boxed()
})
.on_click(|cx| cx.dispatch_action(DismissErrorMessage))
.on_click(|_, cx| cx.dispatch_action(DismissErrorMessage))
.boxed()
}
AutoUpdateStatus::Idle => Empty::new().boxed(),
+1 -1
View File
@@ -320,7 +320,7 @@ impl ChatPanel {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(move |cx| {
.on_click(move |_, cx| {
let rpc = rpc.clone();
let this = this.clone();
cx.spawn(|mut cx| async move {
+231 -14
View File
@@ -126,6 +126,9 @@ impl Server {
.add_request_handler(Server::forward_project_request::<proto::PerformRename>)
.add_request_handler(Server::forward_project_request::<proto::ReloadBuffers>)
.add_request_handler(Server::forward_project_request::<proto::FormatBuffers>)
.add_request_handler(Server::forward_project_request::<proto::CreateProjectEntry>)
.add_request_handler(Server::forward_project_request::<proto::RenameProjectEntry>)
.add_request_handler(Server::forward_project_request::<proto::DeleteProjectEntry>)
.add_request_handler(Server::update_buffer)
.add_message_handler(Server::update_buffer_file)
.add_message_handler(Server::buffer_reloaded)
@@ -157,9 +160,7 @@ impl Server {
let span = info_span!(
"handle message",
payload_type = envelope.payload_type_name(),
payload = serde_json::to_string_pretty(&envelope.payload)
.unwrap()
.as_str(),
payload = format!("{:?}", envelope.payload).as_str(),
);
let future = (handler)(server, *envelope);
async move {
@@ -447,6 +448,7 @@ impl Server {
.cloned()
.collect(),
visible: worktree.visible,
scan_id: shared_worktree.scan_id,
})
})
.collect();
@@ -577,6 +579,7 @@ impl Server {
request.payload.worktree_id,
&request.payload.removed_entries,
&request.payload.updated_entries,
request.payload.scan_id,
)?;
broadcast(request.sender_id, connection_ids, |connection_id| {
@@ -1808,6 +1811,176 @@ mod tests {
.await;
}
#[gpui::test(iterations = 10)]
async fn test_fs_operations(
executor: Arc<Deterministic>,
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
executor.forbid_parking();
let fs = FakeFs::new(cx_a.background());
// Connect to a server as 2 clients.
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
let mut client_a = server.create_client(cx_a, "user_a").await;
let mut client_b = server.create_client(cx_b, "user_b").await;
// Share a project as client A
fs.insert_tree(
"/dir",
json!({
".zed.toml": r#"collaborators = ["user_b"]"#,
"a.txt": "a-contents",
"b.txt": "b-contents",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
project_a
.update(cx_a, |project, cx| project.share(cx))
.await
.unwrap();
let project_b = client_b.build_remote_project(project_id, cx_b).await;
let worktree_a =
project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
let worktree_b =
project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
let entry = project_b
.update(cx_b, |project, cx| {
project
.create_entry((worktree_id, "c.txt"), false, cx)
.unwrap()
})
.await
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "a.txt", "b.txt", "c.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "a.txt", "b.txt", "c.txt"]
);
});
project_b
.update(cx_b, |project, cx| {
project.rename_entry(entry.id, Path::new("d.txt"), cx)
})
.unwrap()
.await
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "a.txt", "b.txt", "d.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "a.txt", "b.txt", "d.txt"]
);
});
let dir_entry = project_b
.update(cx_b, |project, cx| {
project
.create_entry((worktree_id, "DIR"), true, cx)
.unwrap()
})
.await
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"]
);
});
project_b
.update(cx_b, |project, cx| {
project.delete_entry(dir_entry.id, cx).unwrap()
})
.await
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "a.txt", "b.txt", "d.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "a.txt", "b.txt", "d.txt"]
);
});
project_b
.update(cx_b, |project, cx| {
project.delete_entry(entry.id, cx).unwrap()
})
.await
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "a.txt", "b.txt"]
);
});
worktree_b.read_with(cx_b, |worktree, _| {
assert_eq!(
worktree
.paths()
.map(|p| p.to_string_lossy())
.collect::<Vec<_>>(),
[".zed.toml", "a.txt", "b.txt"]
);
});
}
#[gpui::test(iterations = 10)]
async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
@@ -3725,7 +3898,7 @@ mod tests {
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "main.rs"), cx)
workspace.open_path((worktree_id, "main.rs"), true, cx)
})
.await
.unwrap()
@@ -3973,7 +4146,7 @@ mod tests {
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "one.rs"), cx)
workspace.open_path((worktree_id, "one.rs"), true, cx)
})
.await
.unwrap()
@@ -4725,7 +4898,7 @@ mod tests {
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), cx)
workspace.open_path((worktree_id, "1.txt"), true, cx)
})
.await
.unwrap()
@@ -4733,7 +4906,7 @@ mod tests {
.unwrap();
let editor_a2 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), cx)
workspace.open_path((worktree_id, "2.txt"), true, cx)
})
.await
.unwrap()
@@ -4744,7 +4917,7 @@ mod tests {
let workspace_b = client_b.build_workspace(&project_b, cx_b);
let editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), cx)
workspace.open_path((worktree_id, "1.txt"), true, cx)
})
.await
.unwrap()
@@ -4937,7 +5110,7 @@ mod tests {
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), cx)
workspace.open_path((worktree_id, "1.txt"), true, cx)
})
.await
.unwrap()
@@ -4949,7 +5122,7 @@ mod tests {
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
let _editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), cx)
workspace.open_path((worktree_id, "2.txt"), true, cx)
})
.await
.unwrap()
@@ -4984,7 +5157,7 @@ mod tests {
.update(cx_a, |workspace, cx| {
workspace.activate_next_pane(cx);
assert_eq!(*workspace.active_pane(), pane_a1);
workspace.open_path((worktree_id, "3.txt"), cx)
workspace.open_path((worktree_id, "3.txt"), true, cx)
})
.await
.unwrap();
@@ -4992,7 +5165,7 @@ mod tests {
.update(cx_b, |workspace, cx| {
workspace.activate_next_pane(cx);
assert_eq!(*workspace.active_pane(), pane_b1);
workspace.open_path((worktree_id, "4.txt"), cx)
workspace.open_path((worktree_id, "4.txt"), true, cx)
})
.await
.unwrap();
@@ -5081,7 +5254,7 @@ mod tests {
let workspace_a = client_a.build_workspace(&project_a, cx_a);
let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
workspace.open_path((worktree_id, "1.txt"), cx)
workspace.open_path((worktree_id, "1.txt"), true, cx)
})
.await
.unwrap()
@@ -5194,7 +5367,7 @@ mod tests {
// When client B activates a different item in the original pane, it automatically stops following client A.
workspace_b
.update(cx_b, |workspace, cx| {
workspace.open_path((worktree_id, "2.txt"), cx)
workspace.open_path((worktree_id, "2.txt"), true, cx)
})
.await
.unwrap();
@@ -5633,6 +5806,7 @@ mod tests {
guest_client.username,
id
);
assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
}
guest_client
@@ -6354,6 +6528,49 @@ mod tests {
client.buffers.extend(search.await?.into_keys());
}
}
60..=69 => {
let worktree = project
.read_with(cx, |project, cx| {
project
.worktrees(&cx)
.filter(|worktree| {
let worktree = worktree.read(cx);
worktree.is_visible()
&& worktree.entries(false).any(|e| e.is_file())
&& worktree
.root_entry()
.map_or(false, |e| e.is_dir())
})
.choose(&mut *rng.lock())
})
.unwrap();
let (worktree_id, worktree_root_name) = worktree
.read_with(cx, |worktree, _| {
(worktree.id(), worktree.root_name().to_string())
});
let mut new_name = String::new();
for _ in 0..10 {
let letter = rng.lock().gen_range('a'..='z');
new_name.push(letter);
}
let mut new_path = PathBuf::new();
new_path.push(new_name);
new_path.set_extension("rs");
log::info!(
"{}: creating {:?} in worktree {} ({})",
guest_username,
new_path,
worktree_id,
worktree_root_name,
);
project
.update(cx, |project, cx| {
project.create_entry((worktree_id, new_path), false, cx)
})
.unwrap()
.await?;
}
_ => {
buffer.update(cx, |buffer, cx| {
log::info!(
+3
View File
@@ -46,6 +46,7 @@ pub struct ProjectShare {
pub struct WorktreeShare {
pub entries: HashMap<u64, proto::Entry>,
pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
pub scan_id: u64,
}
#[derive(Default)]
@@ -561,6 +562,7 @@ impl Store {
worktree_id: u64,
removed_entries: &[u64],
updated_entries: &[proto::Entry],
scan_id: u64,
) -> Result<Vec<ConnectionId>> {
let project = self.write_project(project_id, connection_id)?;
let worktree = project
@@ -574,6 +576,7 @@ impl Store {
for entry in updated_entries {
worktree.entries.insert(entry.id, entry.clone());
}
worktree.scan_id = scan_id;
let connection_ids = project.connection_ids();
Ok(connection_ids)
}
+1 -1
View File
@@ -204,7 +204,7 @@ impl ContactsPanel {
} else {
CursorStyle::Arrow
})
.on_click(move |cx| {
.on_click(move |_, cx| {
if !is_host && !is_guest {
cx.dispatch_global_action(JoinProject {
project_id,
+2 -2
View File
@@ -161,7 +161,7 @@ impl View for DiagnosticIndicator {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(|cx| cx.dispatch_action(crate::Deploy))
.on_click(|_, cx| cx.dispatch_action(crate::Deploy))
.aligned()
.boxed(),
);
@@ -194,7 +194,7 @@ impl View for DiagnosticIndicator {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(|cx| cx.dispatch_action(GoToNextDiagnostic))
.on_click(|_, cx| cx.dispatch_action(GoToNextDiagnostic))
.boxed(),
);
}
+4 -22
View File
@@ -9354,19 +9354,10 @@ mod tests {
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", Default::default()).await;
let project = Project::test(fs, cx);
let project = Project::test(fs, ["/file.rs"], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/file.rs", true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |tree, _| tree.id());
let buffer = project
.update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
@@ -9485,19 +9476,10 @@ mod tests {
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", text).await;
let project = Project::test(fs, cx);
let project = Project::test(fs, ["/file.rs"], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/file.rs", true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |tree, _| tree.id());
let buffer = project
.update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
.update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
let mut fake_server = fake_servers.next().await.unwrap();
+7 -1
View File
@@ -875,6 +875,12 @@ impl Element for EditorElement {
.max(constraint.min_along(Axis::Vertical))
.min(line_height * max_lines as f32),
)
} else if let EditorMode::SingleLine = snapshot.mode {
size.set_y(
line_height
.min(constraint.max_along(Axis::Vertical))
.max(constraint.min_along(Axis::Vertical)),
)
} else if size.y().is_infinite() {
size.set_y(scroll_height);
}
@@ -1183,7 +1189,7 @@ impl Element for EditorElement {
click_count,
..
} => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx),
Event::LeftMouseUp { position } => self.mouse_up(*position, cx),
Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx),
Event::LeftMouseDragged { position } => {
self.mouse_dragged(*position, layout, paint, cx)
}
+1 -1
View File
@@ -102,7 +102,7 @@ impl FileFinder {
match event {
Event::Selected(project_path) => {
workspace
.open_path(project_path.clone(), cx)
.open_path(project_path.clone(), true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
}
@@ -15,7 +15,7 @@ pub struct MouseEventHandler {
child: ElementBox,
cursor_style: Option<CursorStyle>,
mouse_down_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
click_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
click_handler: Option<Box<dyn FnMut(usize, &mut EventContext)>>,
drag_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
padding: Padding,
}
@@ -57,7 +57,7 @@ impl MouseEventHandler {
self
}
pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
pub fn on_click(mut self, handler: impl FnMut(usize, &mut EventContext) + 'static) -> Self {
self.click_handler = Some(Box::new(handler));
self
}
@@ -151,14 +151,18 @@ impl Element for MouseEventHandler {
handled_in_child
}
}
Event::LeftMouseUp { position, .. } => {
Event::LeftMouseUp {
position,
click_count,
..
} => {
state.prev_drag_position = None;
if !handled_in_child && state.clicked {
state.clicked = false;
cx.notify();
if let Some(handler) = click_handler {
if hit_bounds.contains_point(*position) {
handler(cx);
handler(*click_count, cx);
}
}
true
+9 -7
View File
@@ -360,6 +360,14 @@ impl Deterministic {
self.state.lock().now = new_now;
}
pub fn forbid_parking(&self) {
use rand::prelude::*;
let mut state = self.state.lock();
state.forbid_parking = true;
state.rng = StdRng::seed_from_u64(state.seed);
}
}
impl Drop for Timer {
@@ -507,14 +515,8 @@ impl Foreground {
#[cfg(any(test, feature = "test-support"))]
pub fn forbid_parking(&self) {
use rand::prelude::*;
match self {
Self::Deterministic { executor, .. } => {
let mut state = executor.state.lock();
state.forbid_parking = true;
state.rng = StdRng::seed_from_u64(state.seed);
}
Self::Deterministic { executor, .. } => executor.forbid_parking(),
_ => panic!("this method can only be called on a deterministic executor"),
}
}
+2 -1
View File
@@ -28,6 +28,7 @@ pub enum Event {
},
LeftMouseUp {
position: Vector2F,
click_count: usize,
},
LeftMouseDragged {
position: Vector2F,
@@ -68,7 +69,7 @@ impl Event {
Event::KeyDown { .. } => None,
Event::ScrollWheel { position, .. }
| Event::LeftMouseDown { position, .. }
| Event::LeftMouseUp { position }
| Event::LeftMouseUp { position, .. }
| Event::LeftMouseDragged { position }
| Event::RightMouseDown { position, .. }
| Event::RightMouseUp { position }
+1
View File
@@ -129,6 +129,7 @@ impl Event {
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
click_count: native_event.clickCount() as usize,
}),
NSEventType::NSRightMouseDown => {
let modifiers = native_event.modifierFlags();
+2 -2
View File
@@ -119,7 +119,7 @@ impl View for Select {
.with_style(style.header)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSelect))
.on_click(move |_, cx| cx.dispatch_action(ToggleSelect))
.boxed(),
);
if self.is_open {
@@ -153,7 +153,7 @@ impl View for Select {
)
},
)
.on_click(move |cx| cx.dispatch_action(SelectItem(ix)))
.on_click(move |_, cx| cx.dispatch_action(SelectItem(ix)))
.boxed()
}))
},
+1 -1
View File
@@ -29,7 +29,7 @@ settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
util = { path = "../util" }
aho-corasick = "0.7"
anyhow = "1.0.38"
anyhow = "1.0.57"
async-trait = "0.1"
futures = "0.3"
ignore = "0.4"
+21 -12
View File
@@ -379,7 +379,7 @@ impl FakeFs {
async fn simulate_random_delay(&self) {
self.executor
.upgrade()
.expect("excecutor has been dropped")
.expect("executor has been dropped")
.simulate_random_delay()
.await;
}
@@ -493,7 +493,7 @@ impl Fs for FakeFs {
});
for (relative_path, entry) in removed {
let new_path = target.join(relative_path);
let new_path = normalize_path(&target.join(relative_path));
state.entries.insert(new_path, entry);
}
@@ -501,13 +501,15 @@ impl Fs for FakeFs {
Ok(())
}
async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
let path = normalize_path(path);
async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {
let dir_path = normalize_path(dir_path);
let mut state = self.state.lock().await;
state.validate_path(&path)?;
if let Some(entry) = state.entries.get(&path) {
state.validate_path(&dir_path)?;
if let Some(entry) = state.entries.get(&dir_path) {
if !entry.metadata.is_dir {
return Err(anyhow!("cannot remove {path:?} because it is not a dir"));
return Err(anyhow!(
"cannot remove {dir_path:?} because it is not a dir"
));
}
if !options.recursive {
@@ -517,14 +519,14 @@ impl Fs for FakeFs {
.filter(|path| path.starts_with(path))
.count();
if descendants > 1 {
return Err(anyhow!("{path:?} is not empty"));
return Err(anyhow!("{dir_path:?} is not empty"));
}
}
state.entries.retain(|path, _| !path.starts_with(path));
state.emit_event(&[path]).await;
state.entries.retain(|path, _| !path.starts_with(&dir_path));
state.emit_event(&[dir_path]).await;
} else if !options.ignore_if_not_exists {
return Err(anyhow!("{path:?} does not exist"));
return Err(anyhow!("{dir_path:?} does not exist"));
}
Ok(())
@@ -647,9 +649,16 @@ impl Fs for FakeFs {
let (tx, rx) = smol::channel::unbounded();
state.event_txs.push(tx);
let path = path.to_path_buf();
let executor = self.executor.clone();
Box::pin(futures::StreamExt::filter(rx, move |events| {
let result = events.iter().any(|event| event.path.starts_with(&path));
async move { result }
let executor = executor.clone();
async move {
if let Some(executor) = executor.clone().upgrade() {
executor.simulate_random_delay().await;
}
result
}
}))
}
File diff suppressed because it is too large Load Diff
+419 -154
View File
@@ -1,4 +1,4 @@
use crate::ProjectEntryId;
use crate::{ProjectEntryId, RemoveOptions};
use super::{
fs::{self, Fs},
@@ -42,6 +42,7 @@ use std::{
fmt,
future::Future,
ops::{Deref, DerefMut},
os::unix::prelude::{OsStrExt, OsStringExt},
path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc},
time::{Duration, SystemTime},
@@ -78,11 +79,12 @@ pub struct LocalWorktree {
}
pub struct RemoteWorktree {
pub(crate) snapshot: Snapshot,
pub snapshot: Snapshot,
pub(crate) background_snapshot: Arc<Mutex<Snapshot>>,
project_id: u64,
snapshot_rx: watch::Receiver<Snapshot>,
client: Arc<Client>,
updates_tx: UnboundedSender<proto::UpdateWorktree>,
last_scan_id_rx: watch::Receiver<usize>,
replica_id: ReplicaId,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
visible: bool,
@@ -95,12 +97,12 @@ pub struct Snapshot {
root_char_bag: CharBag,
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
scan_id: usize,
}
#[derive(Clone)]
pub struct LocalSnapshot {
abs_path: Arc<Path>,
scan_id: usize,
ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
removed_entry_ids: HashMap<u64, ProjectEntryId>,
next_entry_id: Arc<AtomicUsize>,
@@ -214,17 +216,21 @@ impl Worktree {
root_char_bag,
entries_by_path: Default::default(),
entries_by_id: Default::default(),
scan_id: worktree.scan_id as usize,
};
let (updates_tx, mut updates_rx) = mpsc::unbounded();
let (mut snapshot_tx, snapshot_rx) = watch::channel_with(snapshot.clone());
let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
let (mut last_scan_id_tx, last_scan_id_rx) = watch::channel_with(worktree.scan_id as usize);
let worktree_handle = cx.add_model(|_: &mut ModelContext<Worktree>| {
Worktree::Remote(RemoteWorktree {
project_id: project_remote_id,
replica_id,
snapshot: snapshot.clone(),
snapshot_rx: snapshot_rx.clone(),
background_snapshot: background_snapshot.clone(),
updates_tx,
last_scan_id_rx,
client: client.clone(),
diagnostic_summaries: TreeMap::from_ordered_entries(
worktree.diagnostic_summaries.into_iter().map(|summary| {
@@ -274,37 +280,42 @@ impl Worktree {
.await;
{
let mut snapshot = snapshot_tx.borrow_mut();
let mut snapshot = background_snapshot.lock();
snapshot.entries_by_path = entries_by_path;
snapshot.entries_by_id = entries_by_id;
snapshot_updated_tx.send(()).await.ok();
}
cx.background()
.spawn(async move {
while let Some(update) = updates_rx.next().await {
let mut snapshot = snapshot_tx.borrow().clone();
if let Err(error) = snapshot.apply_remote_update(update) {
if let Err(error) =
background_snapshot.lock().apply_remote_update(update)
{
log::error!("error applying worktree update: {}", error);
}
*snapshot_tx.borrow_mut() = snapshot;
snapshot_updated_tx.send(()).await.ok();
}
})
.detach();
{
let mut snapshot_rx = snapshot_rx.clone();
cx.spawn(|mut cx| {
let this = worktree_handle.downgrade();
cx.spawn(|mut cx| async move {
while let Some(_) = snapshot_rx.recv().await {
async move {
while let Some(_) = snapshot_updated_rx.recv().await {
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
this.update(&mut cx, |this, cx| {
this.poll_snapshot(cx);
let this = this.as_remote_mut().unwrap();
*last_scan_id_tx.borrow_mut() = this.snapshot.scan_id;
});
} else {
break;
}
}
})
.detach();
}
}
})
.detach();
}
});
(worktree_handle, deserialize_task)
@@ -357,6 +368,13 @@ impl Worktree {
}
}
pub fn scan_id(&self) -> usize {
match self {
Worktree::Local(worktree) => worktree.snapshot.scan_id,
Worktree::Remote(worktree) => worktree.snapshot.scan_id,
}
}
pub fn is_visible(&self) -> bool {
match self {
Worktree::Local(worktree) => worktree.visible,
@@ -410,7 +428,7 @@ impl Worktree {
}
}
Self::Remote(worktree) => {
worktree.snapshot = worktree.snapshot_rx.borrow().clone();
worktree.snapshot = worktree.background_snapshot.lock().clone();
cx.emit(Event::UpdatedEntries);
}
};
@@ -454,7 +472,6 @@ impl LocalWorktree {
let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
let mut snapshot = LocalSnapshot {
abs_path,
scan_id: 0,
ignores: Default::default(),
removed_entry_ids: Default::default(),
next_entry_id,
@@ -464,6 +481,7 @@ impl LocalWorktree {
root_char_bag,
entries_by_path: Default::default(),
entries_by_id: Default::default(),
scan_id: 0,
},
};
if let Some(metadata) = metadata {
@@ -494,24 +512,13 @@ impl LocalWorktree {
cx.spawn_weak(|this, mut cx| async move {
while let Some(scan_state) = scan_states_rx.next().await {
if let Some(handle) = this.upgrade(&cx) {
let to_send = handle.update(&mut cx, |this, cx| {
last_scan_state_tx.blocking_send(scan_state).ok();
if let Some(this) = this.upgrade(&cx) {
last_scan_state_tx.blocking_send(scan_state).ok();
this.update(&mut cx, |this, cx| {
this.poll_snapshot(cx);
let tree = this.as_local_mut().unwrap();
if !tree.is_scanning() {
if let Some(share) = tree.share.as_ref() {
return Some((tree.snapshot(), share.snapshots_tx.clone()));
}
}
None
});
if let Some((snapshot, snapshots_to_send_tx)) = to_send {
if let Err(err) = snapshots_to_send_tx.send(snapshot).await {
log::error!("error submitting snapshot to send {}", err);
}
}
this.as_local().unwrap().broadcast_snapshot()
})
.await;
} else {
break;
}
@@ -623,12 +630,15 @@ impl LocalWorktree {
let handle = cx.handle();
let path = Arc::from(path);
let abs_path = self.absolutize(&path);
let background_snapshot = self.background_snapshot.clone();
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
let text = fs.load(&abs_path).await?;
// Eagerly populate the snapshot with an updated entry for the loaded file
let entry = refresh_entry(fs.as_ref(), &background_snapshot, path, &abs_path).await?;
let entry = this
.update(&mut cx, |this, _| {
this.as_local().unwrap().refresh_entry(path, abs_path, None)
})
.await?;
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
Ok((
File {
@@ -652,7 +662,7 @@ impl LocalWorktree {
let buffer = buffer_handle.read(cx);
let text = buffer.as_rope().clone();
let version = buffer.version();
let save = self.save(path, text, cx);
let save = self.write_file(path, text, cx);
let handle = cx.handle();
cx.as_mut().spawn(|mut cx| async move {
let entry = save.await?;
@@ -672,28 +682,188 @@ impl LocalWorktree {
})
}
fn save(
pub fn create_entry(
&self,
path: impl Into<Arc<Path>>,
is_dir: bool,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
self.write_entry_internal(
path,
if is_dir {
None
} else {
Some(Default::default())
},
cx,
)
}
pub fn write_file(
&self,
path: impl Into<Arc<Path>>,
text: Rope,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
self.write_entry_internal(path, Some(text), cx)
}
pub fn delete_entry(
&self,
entry_id: ProjectEntryId,
cx: &mut ModelContext<Worktree>,
) -> Option<Task<Result<()>>> {
let entry = self.entry_for_id(entry_id)?.clone();
let abs_path = self.absolutize(&entry.path);
let delete = cx.background().spawn({
let fs = self.fs.clone();
let abs_path = abs_path.clone();
async move {
if entry.is_file() {
fs.remove_file(&abs_path, Default::default()).await
} else {
fs.remove_dir(
&abs_path,
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
},
)
.await
}
}
});
Some(cx.spawn(|this, mut cx| async move {
delete.await?;
this.update(&mut cx, |this, _| {
let this = this.as_local_mut().unwrap();
let mut snapshot = this.background_snapshot.lock();
snapshot.delete_entry(entry_id);
});
this.update(&mut cx, |this, cx| {
this.poll_snapshot(cx);
this.as_local().unwrap().broadcast_snapshot()
})
.await;
Ok(())
}))
}
pub fn rename_entry(
&self,
entry_id: ProjectEntryId,
new_path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
) -> Option<Task<Result<Entry>>> {
let old_path = self.entry_for_id(entry_id)?.path.clone();
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
let rename = cx.background().spawn({
let fs = self.fs.clone();
let abs_new_path = abs_new_path.clone();
async move {
fs.rename(&abs_old_path, &abs_new_path, Default::default())
.await
}
});
Some(cx.spawn(|this, mut cx| async move {
rename.await?;
let entry = this
.update(&mut cx, |this, _| {
this.as_local_mut().unwrap().refresh_entry(
new_path.clone(),
abs_new_path,
Some(old_path),
)
})
.await?;
this.update(&mut cx, |this, cx| {
this.poll_snapshot(cx);
this.as_local().unwrap().broadcast_snapshot()
})
.await;
Ok(entry)
}))
}
fn write_entry_internal(
&self,
path: impl Into<Arc<Path>>,
text_if_file: Option<Rope>,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
let path = path.into();
let abs_path = self.absolutize(&path);
let background_snapshot = self.background_snapshot.clone();
let fs = self.fs.clone();
let save = cx.background().spawn(async move {
fs.save(&abs_path, &text).await?;
refresh_entry(fs.as_ref(), &background_snapshot, path.clone(), &abs_path).await
let write = cx.background().spawn({
let fs = self.fs.clone();
let abs_path = abs_path.clone();
async move {
if let Some(text) = text_if_file {
fs.save(&abs_path, &text).await
} else {
fs.create_dir(&abs_path).await
}
}
});
cx.spawn(|this, mut cx| async move {
let entry = save.await?;
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
write.await?;
let entry = this
.update(&mut cx, |this, _| {
this.as_local_mut()
.unwrap()
.refresh_entry(path, abs_path, None)
})
.await?;
this.update(&mut cx, |this, cx| {
this.poll_snapshot(cx);
this.as_local().unwrap().broadcast_snapshot()
})
.await;
Ok(entry)
})
}
fn refresh_entry(
&self,
path: Arc<Path>,
abs_path: PathBuf,
old_path: Option<Arc<Path>>,
) -> impl Future<Output = Result<Entry>> {
let root_char_bag;
let next_entry_id;
let fs = self.fs.clone();
let shared_snapshots_tx = self.share.as_ref().map(|share| share.snapshots_tx.clone());
let snapshot = self.background_snapshot.clone();
{
let snapshot = snapshot.lock();
root_char_bag = snapshot.root_char_bag;
next_entry_id = snapshot.next_entry_id.clone();
}
async move {
let entry = Entry::new(
path,
&fs.metadata(&abs_path)
.await?
.ok_or_else(|| anyhow!("could not read saved file metadata"))?,
&next_entry_id,
root_char_bag,
);
let mut snapshot = snapshot.lock();
if let Some(old_path) = old_path {
snapshot.remove_path(&old_path);
}
let entry = snapshot.insert_entry(entry, fs.as_ref());
if let Some(tx) = shared_snapshots_tx {
tx.send(snapshot.clone()).await.ok();
}
Ok(entry)
}
}
pub fn register(
&mut self,
project_id: u64,
@@ -761,6 +931,7 @@ impl LocalWorktree {
.map(Into::into)
.collect(),
removed_entries: Default::default(),
scan_id: snapshot.scan_id as u64,
})
.await
{
@@ -829,6 +1000,23 @@ impl LocalWorktree {
pub fn is_shared(&self) -> bool {
self.share.is_some()
}
fn broadcast_snapshot(&self) -> impl Future<Output = ()> {
let mut to_send = None;
if !self.is_scanning() {
if let Some(share) = self.share.as_ref() {
to_send = Some((self.snapshot(), share.snapshots_tx.clone()));
}
}
async move {
if let Some((snapshot, snapshots_to_send_tx)) = to_send {
if let Err(err) = snapshots_to_send_tx.send(snapshot).await {
log::error!("error submitting snapshot to send {}", err);
}
}
}
}
}
impl RemoteWorktree {
@@ -843,10 +1031,20 @@ impl RemoteWorktree {
self.updates_tx
.unbounded_send(envelope.payload)
.expect("consumer runs to completion");
Ok(())
}
fn wait_for_snapshot(&self, scan_id: usize) -> impl Future<Output = ()> {
let mut rx = self.last_scan_id_rx.clone();
async move {
while let Some(applied_scan_id) = rx.next().await {
if applied_scan_id >= scan_id {
return;
}
}
}
}
pub fn update_diagnostic_summary(
&mut self,
path: Arc<Path>,
@@ -863,6 +1061,44 @@ impl RemoteWorktree {
.insert(PathKey(path.clone()), summary);
}
}
pub fn insert_entry(
&self,
entry: proto::Entry,
scan_id: usize,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
let wait_for_snapshot = self.wait_for_snapshot(scan_id);
cx.spawn(|this, mut cx| async move {
wait_for_snapshot.await;
this.update(&mut cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
let mut snapshot = worktree.background_snapshot.lock();
let entry = snapshot.insert_entry(entry);
worktree.snapshot = snapshot.clone();
entry
})
})
}
pub(crate) fn delete_entry(
&self,
id: ProjectEntryId,
scan_id: usize,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<()>> {
let wait_for_snapshot = self.wait_for_snapshot(scan_id);
cx.spawn(|this, mut cx| async move {
wait_for_snapshot.await;
this.update(&mut cx, |worktree, _| {
let worktree = worktree.as_remote_mut().unwrap();
let mut snapshot = worktree.background_snapshot.lock();
snapshot.delete_entry(id);
worktree.snapshot = snapshot.clone();
});
Ok(())
})
}
}
impl Snapshot {
@@ -874,6 +1110,33 @@ impl Snapshot {
self.entries_by_id.get(&entry_id, &()).is_some()
}
pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
let entry = Entry::try_from((&self.root_char_bag, entry))?;
let old_entry = self.entries_by_id.insert_or_replace(
PathEntry {
id: entry.id,
path: entry.path.clone(),
is_ignored: entry.is_ignored,
scan_id: 0,
},
&(),
);
if let Some(old_entry) = old_entry {
self.entries_by_path.remove(&PathKey(old_entry.path), &());
}
self.entries_by_path.insert_or_replace(entry.clone(), &());
Ok(entry)
}
fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool {
if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) {
self.entries_by_path.remove(&PathKey(entry.path), &());
true
} else {
false
}
}
pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
let mut entries_by_path_edits = Vec::new();
let mut entries_by_id_edits = Vec::new();
@@ -901,6 +1164,7 @@ impl Snapshot {
self.entries_by_path.edit(entries_by_path_edits, &());
self.entries_by_id.edit(entries_by_id_edits, &());
self.scan_id = update.scan_id as usize;
Ok(())
}
@@ -989,6 +1253,10 @@ impl Snapshot {
&self.root_name
}
pub fn scan_id(&self) -> usize {
self.scan_id
}
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
let path = path.as_ref();
self.traverse_from_path(true, true, path)
@@ -1038,6 +1306,7 @@ impl LocalSnapshot {
.map(|(path, summary)| summary.to_proto(&path.0))
.collect(),
visible,
scan_id: self.scan_id as u64,
}
}
@@ -1103,6 +1372,7 @@ impl LocalSnapshot {
root_name: self.root_name().to_string(),
updated_entries,
removed_entries,
scan_id: self.scan_id as u64,
}
}
@@ -1146,11 +1416,18 @@ impl LocalSnapshot {
entries: impl IntoIterator<Item = Entry>,
ignore: Option<Arc<Gitignore>>,
) {
let mut parent_entry = self
.entries_by_path
.get(&PathKey(parent_path.clone()), &())
.unwrap()
.clone();
let mut parent_entry = if let Some(parent_entry) =
self.entries_by_path.get(&PathKey(parent_path.clone()), &())
{
parent_entry.clone()
} else {
log::warn!(
"populating a directory {:?} that has been removed",
parent_path
);
return;
};
if let Some(ignore) = ignore {
self.ignores.insert(parent_path, (ignore, self.scan_id));
}
@@ -1210,7 +1487,7 @@ impl LocalSnapshot {
if path.file_name() == Some(&GITIGNORE) {
if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) {
*scan_id = self.scan_id;
*scan_id = self.snapshot.scan_id;
}
}
}
@@ -1397,7 +1674,7 @@ impl language::File for File {
Worktree::Local(worktree) => {
let rpc = worktree.client.clone();
let project_id = worktree.share.as_ref().map(|share| share.project_id);
let save = worktree.save(self.path.clone(), text, cx);
let save = worktree.write_file(self.path.clone(), text, cx);
cx.background().spawn(async move {
let entry = save.await?;
if let Some(project_id) = project_id {
@@ -1536,7 +1813,7 @@ pub struct Entry {
pub is_ignored: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EntryKind {
PendingDir,
Dir,
@@ -1777,14 +2054,14 @@ impl BackgroundScanner {
let path: Arc<Path> = Arc::from(Path::new(""));
let abs_path = self.abs_path();
let (tx, rx) = channel::unbounded();
tx.send(ScanJob {
abs_path: abs_path.to_path_buf(),
path,
ignore_stack: IgnoreStack::none(),
scan_queue: tx.clone(),
})
.await
.unwrap();
self.executor
.block(tx.send(ScanJob {
abs_path: abs_path.to_path_buf(),
path,
ignore_stack: IgnoreStack::none(),
scan_queue: tx.clone(),
}))
.unwrap();
drop(tx);
self.executor
@@ -1907,83 +2184,91 @@ impl BackgroundScanner {
}
async fn process_events(&mut self, mut events: Vec<fsevent::Event>) -> bool {
let mut snapshot = self.snapshot();
snapshot.scan_id += 1;
events.sort_unstable_by(|a, b| a.path.cmp(&b.path));
events.dedup_by(|a, b| a.path.starts_with(&b.path));
let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&snapshot.abs_path).await {
let root_char_bag;
let root_abs_path;
let next_entry_id;
{
let mut snapshot = self.snapshot.lock();
snapshot.scan_id += 1;
root_char_bag = snapshot.root_char_bag;
root_abs_path = snapshot.abs_path.clone();
next_entry_id = snapshot.next_entry_id.clone();
}
let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&root_abs_path).await {
abs_path
} else {
return false;
};
let root_char_bag = snapshot.root_char_bag;
let next_entry_id = snapshot.next_entry_id.clone();
let metadata = futures::future::join_all(
events
.iter()
.map(|event| self.fs.metadata(&event.path))
.collect::<Vec<_>>(),
)
.await;
events.sort_unstable_by(|a, b| a.path.cmp(&b.path));
events.dedup_by(|a, b| a.path.starts_with(&b.path));
for event in &events {
match event.path.strip_prefix(&root_abs_path) {
Ok(path) => snapshot.remove_path(&path),
Err(_) => {
log::error!(
"unexpected event {:?} for root path {:?}",
event.path,
root_abs_path
);
continue;
// Hold the snapshot lock while clearing and re-inserting the root entries
// for each event. This way, the snapshot is not observable to the foreground
// thread while this operation is in-progress.
let (scan_queue_tx, scan_queue_rx) = channel::unbounded();
{
let mut snapshot = self.snapshot.lock();
for event in &events {
if let Ok(path) = event.path.strip_prefix(&root_abs_path) {
snapshot.remove_path(&path);
}
}
}
let (scan_queue_tx, scan_queue_rx) = channel::unbounded();
for event in events {
let path: Arc<Path> = match event.path.strip_prefix(&root_abs_path) {
Ok(path) => Arc::from(path.to_path_buf()),
Err(_) => {
log::error!(
"unexpected event {:?} for root path {:?}",
event.path,
root_abs_path
);
continue;
}
};
for (event, metadata) in events.into_iter().zip(metadata.into_iter()) {
let path: Arc<Path> = match event.path.strip_prefix(&root_abs_path) {
Ok(path) => Arc::from(path.to_path_buf()),
Err(_) => {
log::error!(
"unexpected event {:?} for root path {:?}",
event.path,
root_abs_path
);
continue;
}
};
match self.fs.metadata(&event.path).await {
Ok(Some(metadata)) => {
let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir);
let mut fs_entry = Entry::new(
path.clone(),
&metadata,
snapshot.next_entry_id.as_ref(),
snapshot.root_char_bag,
);
fs_entry.is_ignored = ignore_stack.is_all();
snapshot.insert_entry(fs_entry, self.fs.as_ref());
if metadata.is_dir {
scan_queue_tx
.send(ScanJob {
abs_path: event.path,
path,
ignore_stack,
scan_queue: scan_queue_tx.clone(),
})
.await
.unwrap();
match metadata {
Ok(Some(metadata)) => {
let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir);
let mut fs_entry = Entry::new(
path.clone(),
&metadata,
snapshot.next_entry_id.as_ref(),
snapshot.root_char_bag,
);
fs_entry.is_ignored = ignore_stack.is_all();
snapshot.insert_entry(fs_entry, self.fs.as_ref());
if metadata.is_dir {
self.executor
.block(scan_queue_tx.send(ScanJob {
abs_path: event.path,
path,
ignore_stack,
scan_queue: scan_queue_tx.clone(),
}))
.unwrap();
}
}
Ok(None) => {}
Err(err) => {
// TODO - create a special 'error' entry in the entries tree to mark this
log::error!("error reading file on event {:?}", err);
}
}
Ok(None) => {}
Err(err) => {
// TODO - create a special 'error' entry in the entries tree to mark this
log::error!("error reading file on event {:?}", err);
}
}
drop(scan_queue_tx);
}
*self.snapshot.lock() = snapshot;
// Scan any directories that were created as part of this event batch.
drop(scan_queue_tx);
self.executor
.scoped(|scope| {
for _ in 0..self.executor.num_cpus() {
@@ -2107,30 +2392,6 @@ impl BackgroundScanner {
}
}
async fn refresh_entry(
fs: &dyn Fs,
snapshot: &Mutex<LocalSnapshot>,
path: Arc<Path>,
abs_path: &Path,
) -> Result<Entry> {
let root_char_bag;
let next_entry_id;
{
let snapshot = snapshot.lock();
root_char_bag = snapshot.root_char_bag;
next_entry_id = snapshot.next_entry_id.clone();
}
let entry = Entry::new(
path,
&fs.metadata(abs_path)
.await?
.ok_or_else(|| anyhow!("could not read saved file metadata"))?,
&next_entry_id,
root_char_bag,
);
Ok(snapshot.lock().insert_entry(entry, fs))
}
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
let mut result = root_char_bag;
result.extend(
@@ -2368,7 +2629,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
Self {
id: entry.id.to_proto(),
is_dir: entry.is_dir(),
path: entry.path.to_string_lossy().to_string(),
path: entry.path.as_os_str().as_bytes().to_vec(),
inode: entry.inode,
mtime: Some(entry.mtime.into()),
is_symlink: entry.is_symlink,
@@ -2386,10 +2647,14 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
EntryKind::Dir
} else {
let mut char_bag = root_char_bag.clone();
char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase()));
char_bag.extend(
String::from_utf8_lossy(&entry.path)
.chars()
.map(|c| c.to_ascii_lowercase()),
);
EntryKind::File(char_bag)
};
let path: Arc<Path> = Arc::from(Path::new(&entry.path));
let path: Arc<Path> = PathBuf::from(OsString::from_vec(entry.path)).into();
Ok(Entry {
id: ProjectEntryId::from_proto(entry.id),
kind,
@@ -2541,7 +2806,6 @@ mod tests {
let next_entry_id = Arc::new(AtomicUsize::new(0));
let mut initial_snapshot = LocalSnapshot {
abs_path: root_dir.path().into(),
scan_id: 0,
removed_entry_ids: Default::default(),
ignores: Default::default(),
next_entry_id: next_entry_id.clone(),
@@ -2551,6 +2815,7 @@ mod tests {
entries_by_id: Default::default(),
root_name: Default::default(),
root_char_bag: Default::default(),
scan_id: 0,
},
};
initial_snapshot.insert_entry(
+4
View File
@@ -8,15 +8,19 @@ path = "src/project_panel.rs"
doctest = false
[dependencies]
editor = { path = "../editor" }
gpui = { path = "../gpui" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
postage = { version = "0.4.1", features = ["futures-traits"] }
futures = "0.3"
unicase = "2.6"
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
File diff suppressed because it is too large Load Diff
+3 -14
View File
@@ -297,23 +297,12 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
let project = Project::test(fs.clone(), cx);
project.update(cx, |project, _| {
project.languages().add(Arc::new(language));
});
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/dir", true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |tree, _| tree.id());
let project = Project::test(fs.clone(), ["/dir"], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let _buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "test.rs"), cx)
project.open_local_buffer("/dir/test.rs", cx)
})
.await
.unwrap();
+78 -47
View File
@@ -36,57 +36,63 @@ message Envelope {
RegisterWorktree register_worktree = 28;
UnregisterWorktree unregister_worktree = 29;
UpdateWorktree update_worktree = 31;
UpdateDiagnosticSummary update_diagnostic_summary = 32;
StartLanguageServer start_language_server = 33;
UpdateLanguageServer update_language_server = 34;
OpenBufferById open_buffer_by_id = 35;
OpenBufferByPath open_buffer_by_path = 36;
OpenBufferResponse open_buffer_response = 37;
UpdateBuffer update_buffer = 38;
UpdateBufferFile update_buffer_file = 39;
SaveBuffer save_buffer = 40;
BufferSaved buffer_saved = 41;
BufferReloaded buffer_reloaded = 42;
ReloadBuffers reload_buffers = 43;
ReloadBuffersResponse reload_buffers_response = 44;
FormatBuffers format_buffers = 45;
FormatBuffersResponse format_buffers_response = 46;
GetCompletions get_completions = 47;
GetCompletionsResponse get_completions_response = 48;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50;
GetCodeActions get_code_actions = 51;
GetCodeActionsResponse get_code_actions_response = 52;
ApplyCodeAction apply_code_action = 53;
ApplyCodeActionResponse apply_code_action_response = 54;
PrepareRename prepare_rename = 55;
PrepareRenameResponse prepare_rename_response = 56;
PerformRename perform_rename = 57;
PerformRenameResponse perform_rename_response = 58;
SearchProject search_project = 59;
SearchProjectResponse search_project_response = 60;
CreateProjectEntry create_project_entry = 32;
RenameProjectEntry rename_project_entry = 33;
DeleteProjectEntry delete_project_entry = 34;
ProjectEntryResponse project_entry_response = 35;
GetChannels get_channels = 61;
GetChannelsResponse get_channels_response = 62;
JoinChannel join_channel = 63;
JoinChannelResponse join_channel_response = 64;
LeaveChannel leave_channel = 65;
SendChannelMessage send_channel_message = 66;
SendChannelMessageResponse send_channel_message_response = 67;
ChannelMessageSent channel_message_sent = 68;
GetChannelMessages get_channel_messages = 69;
GetChannelMessagesResponse get_channel_messages_response = 70;
UpdateDiagnosticSummary update_diagnostic_summary = 36;
StartLanguageServer start_language_server = 37;
UpdateLanguageServer update_language_server = 38;
UpdateContacts update_contacts = 71;
OpenBufferById open_buffer_by_id = 39;
OpenBufferByPath open_buffer_by_path = 40;
OpenBufferResponse open_buffer_response = 41;
UpdateBuffer update_buffer = 42;
UpdateBufferFile update_buffer_file = 43;
SaveBuffer save_buffer = 44;
BufferSaved buffer_saved = 45;
BufferReloaded buffer_reloaded = 46;
ReloadBuffers reload_buffers = 47;
ReloadBuffersResponse reload_buffers_response = 48;
FormatBuffers format_buffers = 49;
FormatBuffersResponse format_buffers_response = 50;
GetCompletions get_completions = 51;
GetCompletionsResponse get_completions_response = 52;
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53;
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54;
GetCodeActions get_code_actions = 55;
GetCodeActionsResponse get_code_actions_response = 56;
ApplyCodeAction apply_code_action = 57;
ApplyCodeActionResponse apply_code_action_response = 58;
PrepareRename prepare_rename = 59;
PrepareRenameResponse prepare_rename_response = 60;
PerformRename perform_rename = 61;
PerformRenameResponse perform_rename_response = 62;
SearchProject search_project = 63;
SearchProjectResponse search_project_response = 64;
GetUsers get_users = 72;
GetUsersResponse get_users_response = 73;
GetChannels get_channels = 65;
GetChannelsResponse get_channels_response = 66;
JoinChannel join_channel = 67;
JoinChannelResponse join_channel_response = 68;
LeaveChannel leave_channel = 69;
SendChannelMessage send_channel_message = 70;
SendChannelMessageResponse send_channel_message_response = 71;
ChannelMessageSent channel_message_sent = 72;
GetChannelMessages get_channel_messages = 73;
GetChannelMessagesResponse get_channel_messages_response = 74;
Follow follow = 74;
FollowResponse follow_response = 75;
UpdateFollowers update_followers = 76;
Unfollow unfollow = 77;
UpdateContacts update_contacts = 75;
GetUsers get_users = 76;
GetUsersResponse get_users_response = 77;
Follow follow = 78;
FollowResponse follow_response = 79;
UpdateFollowers update_followers = 80;
Unfollow unfollow = 81;
}
}
@@ -156,6 +162,30 @@ message UpdateWorktree {
string root_name = 3;
repeated Entry updated_entries = 4;
repeated uint64 removed_entries = 5;
uint64 scan_id = 6;
}
message CreateProjectEntry {
uint64 project_id = 1;
uint64 worktree_id = 2;
bytes path = 3;
bool is_directory = 4;
}
message RenameProjectEntry {
uint64 project_id = 1;
uint64 entry_id = 2;
bytes new_path = 3;
}
message DeleteProjectEntry {
uint64 project_id = 1;
uint64 entry_id = 2;
}
message ProjectEntryResponse {
Entry entry = 1;
uint64 worktree_scan_id = 2;
}
message AddProjectCollaborator {
@@ -630,6 +660,7 @@ message Worktree {
repeated Entry entries = 3;
repeated DiagnosticSummary diagnostic_summaries = 4;
bool visible = 5;
uint64 scan_id = 6;
}
message File {
@@ -642,7 +673,7 @@ message File {
message Entry {
uint64 id = 1;
bool is_dir = 2;
string path = 3;
bytes path = 3;
uint64 inode = 4;
Timestamp mtime = 5;
bool is_symlink = 6;
+14 -3
View File
@@ -6,13 +6,14 @@ use prost::Message as _;
use serde::Serialize;
use std::any::{Any, TypeId};
use std::{
fmt::Debug,
io,
time::{Duration, SystemTime, UNIX_EPOCH},
};
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
pub trait EnvelopedMessage: Clone + Serialize + Sized + Send + Sync + 'static {
pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static {
const NAME: &'static str;
const PRIORITY: MessagePriority;
fn into_envelope(
@@ -147,6 +148,8 @@ messages!(
(BufferReloaded, Foreground),
(BufferSaved, Foreground),
(ChannelMessageSent, Foreground),
(CreateProjectEntry, Foreground),
(DeleteProjectEntry, Foreground),
(Error, Foreground),
(Follow, Foreground),
(FollowResponse, Foreground),
@@ -174,8 +177,6 @@ messages!(
(JoinChannelResponse, Foreground),
(JoinProject, Foreground),
(JoinProjectResponse, Foreground),
(StartLanguageServer, Foreground),
(UpdateLanguageServer, Foreground),
(LeaveChannel, Foreground),
(LeaveProject, Foreground),
(OpenBufferById, Background),
@@ -187,6 +188,7 @@ messages!(
(PerformRenameResponse, Background),
(PrepareRename, Background),
(PrepareRenameResponse, Background),
(ProjectEntryResponse, Foreground),
(RegisterProjectResponse, Foreground),
(Ping, Foreground),
(RegisterProject, Foreground),
@@ -194,12 +196,14 @@ messages!(
(ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground),
(RemoveProjectCollaborator, Foreground),
(RenameProjectEntry, Foreground),
(SaveBuffer, Foreground),
(SearchProject, Background),
(SearchProjectResponse, Background),
(SendChannelMessage, Foreground),
(SendChannelMessageResponse, Foreground),
(ShareProject, Foreground),
(StartLanguageServer, Foreground),
(Test, Foreground),
(Unfollow, Foreground),
(UnregisterProject, Foreground),
@@ -210,6 +214,7 @@ messages!(
(UpdateContacts, Foreground),
(UpdateDiagnosticSummary, Foreground),
(UpdateFollowers, Foreground),
(UpdateLanguageServer, Foreground),
(UpdateWorktree, Foreground),
);
@@ -219,6 +224,8 @@ request_messages!(
ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse
),
(CreateProjectEntry, ProjectEntryResponse),
(DeleteProjectEntry, ProjectEntryResponse),
(Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse),
(GetChannelMessages, GetChannelMessagesResponse),
@@ -241,6 +248,7 @@ request_messages!(
(RegisterProject, RegisterProjectResponse),
(RegisterWorktree, Ack),
(ReloadBuffers, ReloadBuffersResponse),
(RenameProjectEntry, ProjectEntryResponse),
(SaveBuffer, BufferSaved),
(SearchProject, SearchProjectResponse),
(SendChannelMessage, SendChannelMessageResponse),
@@ -257,6 +265,9 @@ entity_messages!(
ApplyCompletionAdditionalEdits,
BufferReloaded,
BufferSaved,
CreateProjectEntry,
RenameProjectEntry,
DeleteProjectEntry,
Follow,
FormatBuffers,
GetCodeActions,
+1 -1
View File
@@ -5,4 +5,4 @@ pub mod proto;
pub use conn::Connection;
pub use peer::*;
pub const PROTOCOL_VERSION: u32 = 15;
pub const PROTOCOL_VERSION: u32 = 16;
+2 -2
View File
@@ -292,7 +292,7 @@ impl BufferSearchBar {
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option)))
.on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(search_option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@@ -316,7 +316,7 @@ impl BufferSearchBar {
.with_style(style.container)
.boxed()
})
.on_click(move |cx| match direction {
.on_click(move |_, cx| match direction {
Direction::Prev => cx.dispatch_action(SelectPrevMatch),
Direction::Next => cx.dispatch_action(SelectNextMatch),
})
+3 -12
View File
@@ -666,7 +666,7 @@ impl ProjectSearchBar {
.with_style(style.container)
.boxed()
})
.on_click(move |cx| match direction {
.on_click(move |_, cx| match direction {
Direction::Prev => cx.dispatch_action(SelectPrevMatch),
Direction::Next => cx.dispatch_action(SelectNextMatch),
})
@@ -693,7 +693,7 @@ impl ProjectSearchBar {
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
.on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@@ -844,16 +844,7 @@ mod tests {
}),
)
.await;
let project = Project::test(fs.clone(), cx);
let (tree, _) = project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/dir", true, cx)
})
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
let project = Project::test(fs.clone(), ["/dir"], cx).await;
let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
let search_view = cx.add_view(Default::default(), |cx| {
ProjectSearchView::new(search.clone(), cx)
+28 -8
View File
@@ -483,17 +483,20 @@ impl<T: Item + PartialEq> PartialEq for SumTree<T> {
impl<T: Item + Eq> Eq for SumTree<T> {}
impl<T: KeyedItem> SumTree<T> {
pub fn insert_or_replace(&mut self, item: T, cx: &<T::Summary as Summary>::Context) -> bool {
let mut replaced = false;
pub fn insert_or_replace(
&mut self,
item: T,
cx: &<T::Summary as Summary>::Context,
) -> Option<T> {
let mut replaced = None;
*self = {
let mut cursor = self.cursor::<T::Key>();
let mut new_tree = cursor.slice(&item.key(), Bias::Left, cx);
if cursor
.item()
.map_or(false, |cursor_item| cursor_item.key() == item.key())
{
cursor.next(cx);
replaced = true;
if let Some(cursor_item) = cursor.item() {
if cursor_item.key() == item.key() {
replaced = Some(cursor_item.clone());
cursor.next(cx);
}
}
new_tree.push(item, cx);
new_tree.push_tree(cursor.suffix(cx), cx);
@@ -502,6 +505,23 @@ impl<T: KeyedItem> SumTree<T> {
replaced
}
pub fn remove(&mut self, key: &T::Key, cx: &<T::Summary as Summary>::Context) -> Option<T> {
let mut removed = None;
*self = {
let mut cursor = self.cursor::<T::Key>();
let mut new_tree = cursor.slice(key, Bias::Left, cx);
if let Some(item) = cursor.item() {
if item.key() == *key {
removed = Some(item.clone());
cursor.next(cx);
}
}
new_tree.push_tree(cursor.suffix(cx), cx);
new_tree
};
removed
}
pub fn edit(
&mut self,
mut edits: Vec<Edit<T>>,
+4 -5
View File
@@ -204,14 +204,13 @@ pub struct ChatPanel {
pub hovered_sign_in_prompt: TextStyle,
}
#[derive(Debug, Deserialize, Default)]
#[derive(Deserialize, Default)]
pub struct ProjectPanel {
#[serde(flatten)]
pub container: ContainerStyle,
pub entry: ProjectPanelEntry,
pub hovered_entry: ProjectPanelEntry,
pub selected_entry: ProjectPanelEntry,
pub hovered_selected_entry: ProjectPanelEntry,
pub entry: Interactive<ProjectPanelEntry>,
pub filename_editor: FieldEditor,
pub indent_width: f32,
}
#[derive(Debug, Deserialize, Default)]
+1 -1
View File
@@ -50,7 +50,7 @@ impl<'a> VimTestContext<'a> {
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| workspace.open_path(file, cx))
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
+1 -1
View File
@@ -168,7 +168,7 @@ impl View for LspStatus {
self.failed.join(", "),
if self.failed.len() > 1 { "s" } else { "" }
);
handler = Some(|cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
handler = Some(|_, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
} else {
return Empty::new().boxed();
}
+32 -17
View File
@@ -59,7 +59,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
pane.activate_item(action.0, true, cx);
pane.activate_item(action.0, true, true, cx);
});
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
pane.activate_prev_item(cx);
@@ -213,7 +213,7 @@ impl Pane {
{
let prev_active_item_index = pane.active_item_index;
pane.nav_history.borrow_mut().set_mode(mode);
pane.activate_item(index, true, cx);
pane.activate_item(index, true, true, cx);
pane.nav_history
.borrow_mut()
.set_mode(NavigationMode::Normal);
@@ -257,6 +257,7 @@ impl Pane {
workspace,
pane.clone(),
project_entry_id,
true,
cx,
build_item,
)
@@ -287,6 +288,7 @@ impl Pane {
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
project_entry_id: ProjectEntryId,
focus_item: bool,
cx: &mut ViewContext<Workspace>,
build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
@@ -294,7 +296,7 @@ impl Pane {
for (ix, item) in pane.items.iter().enumerate() {
if item.project_entry_id(cx) == Some(project_entry_id) {
let item = item.boxed_clone();
pane.activate_item(ix, true, cx);
pane.activate_item(ix, true, focus_item, cx);
return Some(item);
}
}
@@ -304,7 +306,7 @@ impl Pane {
existing_item
} else {
let item = build_item(cx);
Self::add_item(workspace, pane, item.boxed_clone(), true, cx);
Self::add_item(workspace, pane, item.boxed_clone(), true, focus_item, cx);
item
}
}
@@ -313,12 +315,15 @@ impl Pane {
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
item: Box<dyn ItemHandle>,
local: bool,
activate_pane: bool,
focus_item: bool,
cx: &mut ViewContext<Workspace>,
) {
// Prevent adding the same item to the pane more than once.
if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, activate_pane, focus_item, cx)
});
return;
}
@@ -327,7 +332,7 @@ impl Pane {
pane.update(cx, |pane, cx| {
let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
pane.items.insert(item_idx, item);
pane.activate_item(item_idx, local, cx);
pane.activate_item(item_idx, activate_pane, focus_item, cx);
cx.notify();
});
}
@@ -378,7 +383,13 @@ impl Pane {
self.items.iter().position(|i| i.id() == item.id())
}
pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
pub fn activate_item(
&mut self,
index: usize,
activate_pane: bool,
focus_item: bool,
cx: &mut ViewContext<Self>,
) {
use NavigationMode::{GoingBack, GoingForward};
if index < self.items.len() {
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
@@ -387,11 +398,15 @@ impl Pane {
&& prev_active_item_ix < self.items.len())
{
self.items[prev_active_item_ix].deactivated(cx);
cx.emit(Event::ActivateItem { local });
cx.emit(Event::ActivateItem {
local: activate_pane,
});
}
self.update_toolbar(cx);
if local {
if focus_item {
self.focus_active_item(cx);
}
if activate_pane {
self.activate(cx);
}
self.autoscroll = true;
@@ -406,7 +421,7 @@ impl Pane {
} else if self.items.len() > 0 {
index = self.items.len() - 1;
}
self.activate_item(index, true, cx);
self.activate_item(index, true, true, cx);
}
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
@@ -416,7 +431,7 @@ impl Pane {
} else {
index = 0;
}
self.activate_item(index, true, cx);
self.activate_item(index, true, true, cx);
}
fn close_active_item(
@@ -498,7 +513,7 @@ impl Pane {
if is_last_item_for_entry {
if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) {
let mut answer = pane.update(&mut cx, |pane, cx| {
pane.activate_item(item_to_close_ix, true, cx);
pane.activate_item(item_to_close_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
@@ -518,7 +533,7 @@ impl Pane {
} else if cx.read(|cx| item.is_dirty(cx)) {
if cx.read(|cx| item.can_save(cx)) {
let mut answer = pane.update(&mut cx, |pane, cx| {
pane.activate_item(item_to_close_ix, true, cx);
pane.activate_item(item_to_close_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
DIRTY_MESSAGE,
@@ -535,7 +550,7 @@ impl Pane {
}
} else if cx.read(|cx| item.can_save_as(cx)) {
let mut answer = pane.update(&mut cx, |pane, cx| {
pane.activate_item(item_to_close_ix, true, cx);
pane.activate_item(item_to_close_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
DIRTY_MESSAGE,
@@ -737,7 +752,7 @@ impl Pane {
.with_cursor_style(CursorStyle::PointingHand)
.on_click({
let pane = pane.clone();
move |cx| {
move |_, cx| {
cx.dispatch_action(CloseItem {
item_id,
pane: pane.clone(),
@@ -949,7 +964,7 @@ mod tests {
let close_items = workspace.update(cx, |workspace, cx| {
pane.update(cx, |pane, cx| {
pane.activate_item(1, true, cx);
pane.activate_item(1, true, true, cx);
assert_eq!(pane.active_item().unwrap().id(), item2.id());
});
+1 -1
View File
@@ -203,7 +203,7 @@ impl View for SidebarButtons {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(move |cx| {
.on_click(move |_, cx| {
cx.dispatch_action(ToggleSidebarItem {
side,
item_index: ix,
+12 -10
View File
@@ -493,7 +493,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
if T::should_activate_item_on_event(event) {
pane.update(cx, |pane, cx| {
if let Some(ix) = pane.index_for_item(&item) {
pane.activate_item(ix, true, cx);
pane.activate_item(ix, true, true, cx);
pane.activate(cx);
}
});
@@ -898,7 +898,7 @@ impl Workspace {
if fs.is_file(&abs_path).await {
Some(
this.update(&mut cx, |this, cx| {
this.open_path(project_path, cx)
this.open_path(project_path, true, cx)
})
.await,
)
@@ -1065,7 +1065,7 @@ impl Workspace {
Side::Right => &mut self.right_sidebar,
};
let active_item = sidebar.update(cx, |sidebar, cx| {
sidebar.toggle_item(action.item_index, cx);
sidebar.activate_item(action.item_index, cx);
sidebar.active_item().cloned()
});
if let Some(active_item) = active_item {
@@ -1099,12 +1099,13 @@ impl Workspace {
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
let pane = self.active_pane().clone();
Pane::add_item(self, pane, item, true, cx);
Pane::add_item(self, pane, item, true, true, cx);
}
pub fn open_path(
&mut self,
path: impl Into<ProjectPath>,
focus_item: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
let pane = self.active_pane().downgrade();
@@ -1119,6 +1120,7 @@ impl Workspace {
this,
pane,
project_entry_id,
focus_item,
cx,
build_item,
))
@@ -1187,7 +1189,7 @@ impl Workspace {
});
if let Some((pane, ix)) = result {
self.activate_pane(pane.clone(), cx);
pane.update(cx, |pane, cx| pane.activate_item(ix, true, cx));
pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
true
} else {
false
@@ -1277,7 +1279,7 @@ impl Workspace {
self.activate_pane(new_pane.clone(), cx);
if let Some(item) = pane.read(cx).active_item() {
if let Some(clone) = item.clone_on_split(cx.as_mut()) {
Pane::add_item(self, new_pane.clone(), clone, true, cx);
Pane::add_item(self, new_pane.clone(), clone, true, true, cx);
}
}
self.center.split(&pane, &new_pane, direction).unwrap();
@@ -1584,7 +1586,7 @@ impl Workspace {
.with_style(style.container)
.boxed()
})
.on_click(|cx| cx.dispatch_action(Authenticate))
.on_click(|_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.boxed(),
@@ -1635,7 +1637,7 @@ impl Workspace {
if let Some(peer_id) = peer_id {
MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
.on_click(move |cx| cx.dispatch_action(ToggleFollow(peer_id)))
.on_click(move |_, cx| cx.dispatch_action(ToggleFollow(peer_id)))
.boxed()
} else {
content
@@ -1667,7 +1669,7 @@ impl Workspace {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
.on_click(|cx| cx.dispatch_action(ToggleShare))
.on_click(|_, cx| cx.dispatch_action(ToggleShare))
.boxed(),
)
} else {
@@ -1961,7 +1963,7 @@ impl Workspace {
}
for (pane, item) in items_to_add {
Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx);
Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx);
if pane == self.active_pane {
pane.update(cx, |pane, cx| pane.focus_active_item(cx));
}
+11 -10
View File
@@ -446,7 +446,7 @@ mod tests {
// Open the first entry
let entry_1 = workspace
.update(cx, |w, cx| w.open_path(file1.clone(), cx))
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.await
.unwrap();
cx.read(|cx| {
@@ -460,7 +460,7 @@ mod tests {
// Open the second entry
workspace
.update(cx, |w, cx| w.open_path(file2.clone(), cx))
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
.await
.unwrap();
cx.read(|cx| {
@@ -474,7 +474,7 @@ mod tests {
// Open the first entry again. The existing pane item is activated.
let entry_1b = workspace
.update(cx, |w, cx| w.open_path(file1.clone(), cx))
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.await
.unwrap();
assert_eq!(entry_1.id(), entry_1b.id());
@@ -492,7 +492,7 @@ mod tests {
workspace
.update(cx, |w, cx| {
w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
w.open_path(file2.clone(), cx)
w.open_path(file2.clone(), true, cx)
})
.await
.unwrap();
@@ -511,8 +511,8 @@ mod tests {
// Open the third entry twice concurrently. Only one pane item is added.
let (t1, t2) = workspace.update(cx, |w, cx| {
(
w.open_path(file3.clone(), cx),
w.open_path(file3.clone(), cx),
w.open_path(file3.clone(), true, cx),
w.open_path(file3.clone(), true, cx),
)
});
t1.await.unwrap();
@@ -780,6 +780,7 @@ mod tests {
worktree_id: worktree.read(cx).id(),
path: Path::new("the-new-name.rs").into(),
},
true,
cx,
)
})
@@ -875,7 +876,7 @@ mod tests {
let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
workspace
.update(cx, |w, cx| w.open_path(file1.clone(), cx))
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.await
.unwrap();
@@ -955,7 +956,7 @@ mod tests {
let file3 = entries[2].clone();
let editor1 = workspace
.update(cx, |w, cx| w.open_path(file1.clone(), cx))
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.await
.unwrap()
.downcast::<Editor>()
@@ -964,13 +965,13 @@ mod tests {
editor.select_display_ranges(&[DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], cx);
});
let editor2 = workspace
.update(cx, |w, cx| w.open_path(file2.clone(), cx))
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let editor3 = workspace
.update(cx, |w, cx| w.open_path(file3.clone(), cx))
.update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
.await
.unwrap()
.downcast::<Editor>()
+25 -24
View File
@@ -1,34 +1,35 @@
import Theme from "../themes/theme";
import { Color } from "../utils/color";
import { panel } from "./app";
import { backgroundColor, iconColor, text, TextColor } from "./components";
import { backgroundColor, iconColor, player, text } from "./components";
export default function projectPanel(theme: Theme) {
function entry(theme: Theme, textColor: TextColor, background?: Color) {
return {
return {
...panel,
padding: { left: 12, right: 12, top: 6, bottom: 6 },
indentWidth: 20,
entry: {
height: 24,
background,
iconColor: iconColor(theme, "muted"),
iconSize: 8,
iconSpacing: 8,
text: text(theme, "mono", textColor, { size: "sm" }),
};
}
return {
...panel,
entry: entry(theme, "muted"),
hoveredEntry: entry(
theme,
"primary",
backgroundColor(theme, 300, "hovered")
),
selectedEntry: entry(theme, "primary"),
hoveredSelectedEntry: entry(
theme,
"active",
backgroundColor(theme, 300, "hovered")
),
padding: { left: 12, right: 12, top: 6, bottom: 6 },
text: text(theme, "mono", "muted", { size: "sm" }),
hover: {
background: backgroundColor(theme, 300, "hovered"),
text: text(theme, "mono", "primary", { size: "sm" }),
},
active: {
background: backgroundColor(theme, 300, "active"),
text: text(theme, "mono", "primary", { size: "sm" }),
},
activeHover: {
background: backgroundColor(theme, 300, "hovered"),
text: text(theme, "mono", "active", { size: "sm" }),
}
},
filenameEditor: {
background: backgroundColor(theme, 500, "active"),
text: text(theme, "mono", "primary", { size: "sm" }),
selection: player(theme, 1).selection,
},
};
}