diff --git a/Cargo.lock b/Cargo.lock index c5bbeac616..5e18671305 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1068,6 +1068,7 @@ dependencies = [ "editor", "env_logger", "envy", + "fs", "futures 0.3.24", "git", "gpui", @@ -1083,6 +1084,7 @@ dependencies = [ "prometheus", "rand 0.8.5", "reqwest", + "rope", "rpc", "scrypt", "serde", @@ -1574,6 +1576,7 @@ dependencies = [ "language", "postage", "project", + "rope", "serde_json", "settings", "smallvec", @@ -1727,6 +1730,7 @@ dependencies = [ "postage", "project", "rand 0.8.5", + "rope", "rpc", "serde", "settings", @@ -2007,6 +2011,31 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "fs" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collections", + "fsevent", + "futures 0.3.24", + "git2", + "gpui", + "lazy_static", + "libc", + "log", + "lsp", + "parking_lot 0.11.2", + "regex", + "rope", + "serde", + "serde_json", + "smol", + "tempfile", + "util", +] + [[package]] name = "fs-set-times" version = "0.15.0" @@ -2065,6 +2094,13 @@ version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" +[[package]] +name = "futures" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.24" @@ -2265,6 +2301,7 @@ dependencies = [ "lazy_static", "log", "parking_lot 0.11.2", + "rope", "smol", "sum_tree", "text", @@ -2312,6 +2349,7 @@ dependencies = [ "gpui", "menu", "postage", + "rope", "settings", "text", "workspace", @@ -2900,6 +2938,7 @@ dependencies = [ "collections", "ctor", "env_logger", + "fs", "futures 0.3.24", "fuzzy", "git", @@ -2911,6 +2950,7 @@ dependencies = [ "postage", "rand 0.8.5", "regex", + "rope", "rpc", "serde", "serde_json", @@ -4073,6 +4113,7 @@ dependencies = [ "clock", "collections", "db", + "fs", "fsevent", "futures 0.3.24", "fuzzy", @@ -4081,7 +4122,6 @@ dependencies = [ "ignore", "language", "lazy_static", - "libc", "log", "lsp", "parking_lot 0.11.2", @@ -4090,6 +4130,7 @@ dependencies = [ "rand 0.8.5", "regex", "rocksdb", + "rope", "rpc", "serde", "serde_json", @@ -4597,6 +4638,20 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "rope" +version = "0.1.0" +dependencies = [ + "arrayvec 0.7.2", + "bromberg_sl2", + "gpui", + "log", + "rand 0.8.5", + "smallvec", + "sum_tree", + "util", +] + [[package]] name = "roxmltree" version = "0.14.1" @@ -5095,14 +5150,21 @@ dependencies = [ "anyhow", "assets", "collections", + "fs", + "futures 0.3.24", "gpui", "json_comments", + "postage", + "rope", "schemars", "serde", "serde_json", "serde_path_to_error", "theme", "toml", + "tree-sitter", + "tree-sitter-json 0.19.0", + "unindent", "util", ] @@ -5651,13 +5713,12 @@ name = "text" version = "0.1.0" dependencies = [ "anyhow", - "arrayvec 0.7.2", - "bromberg_sl2", "clock", "collections", "ctor", "digest 0.9.0", "env_logger", + "fs", "gpui", "lazy_static", "log", @@ -5665,6 +5726,7 @@ dependencies = [ "postage", "rand 0.8.5", "regex", + "rope", "smallvec", "sum_tree", "util", @@ -6584,6 +6646,7 @@ dependencies = [ "nvim-rs", "parking_lot 0.11.2", "project", + "rope", "search", "serde", "serde_json", @@ -7274,6 +7337,7 @@ dependencies = [ "collections", "context_menu", "drag_and_drop", + "fs", "futures 0.3.24", "gpui", "language", @@ -7329,7 +7393,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.59.0" +version = "0.60.0" dependencies = [ "activity_indicator", "anyhow", @@ -7357,6 +7421,7 @@ dependencies = [ "editor", "env_logger", "file_finder", + "fs", "fsevent", "futures 0.3.24", "fuzzy", diff --git a/Cargo.toml b/Cargo.toml index 31a9118a1a..8d2a3fcc40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,11 @@ members = ["crates/*"] default-members = ["crates/zed"] resolver = "2" +[workspace.dependencies] +serde = { version = "1.0", features = ["derive", "rc"] } +serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] } +rand = { version = "0.8" } + [patch.crates-io] tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } @@ -21,3 +26,4 @@ split-debuginfo = "unpacked" [profile.release] debug = true + diff --git a/Dockerfile b/Dockerfile index 6bfd49be55..122600bf94 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.62-bullseye as builder +FROM rust:1.64-bullseye as builder WORKDIR app COPY . . diff --git a/Dockerfile.migrator b/Dockerfile.migrator index b6393ed1b5..482228a2eb 100644 --- a/Dockerfile.migrator +++ b/Dockerfile.migrator @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.62-bullseye as builder +FROM rust:1.64-bullseye as builder WORKDIR app RUN --mount=type=cache,target=/usr/local/cargo/registry \ --mount=type=cache,target=./target \ diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index e2adfc0f81..841d9ab7c8 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -376,6 +376,7 @@ { "bindings": { "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", + "cmd-shift-c": "collab::ToggleCollaborationMenu", "cmd-alt-i": "zed::DebugElements" } }, diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index e8f6b80173..cc6bdf6279 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -53,6 +53,8 @@ lazy_static! { } pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894"; +pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100); +pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5); actions!(client, [Authenticate]); @@ -330,7 +332,7 @@ impl Client { let reconnect_interval = state.reconnect_interval; state._reconnect_task = Some(cx.spawn(|cx| async move { let mut rng = StdRng::from_entropy(); - let mut delay = Duration::from_millis(100); + let mut delay = INITIAL_RECONNECTION_DELAY; while let Err(error) = this.authenticate_and_connect(true, &cx).await { log::error!("failed to connect {}", error); if matches!(*this.status().borrow(), Status::ConnectionError) { @@ -661,44 +663,51 @@ impl Client { self.set_status(Status::Reconnecting, cx); } - match self.establish_connection(&credentials, cx).await { - Ok(conn) => { - self.state.write().credentials = Some(credentials.clone()); - if !read_from_keychain && IMPERSONATE_LOGIN.is_none() { - write_credentials_to_keychain(&credentials, cx).log_err(); - } - self.set_connection(conn, cx).await; - Ok(()) - } - Err(EstablishConnectionError::Unauthorized) => { - self.state.write().credentials.take(); - if read_from_keychain { - cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); - self.set_status(Status::SignedOut, cx); - self.authenticate_and_connect(false, cx).await - } else { - self.set_status(Status::ConnectionError, cx); - Err(EstablishConnectionError::Unauthorized)? + futures::select_biased! { + connection = self.establish_connection(&credentials, cx).fuse() => { + match connection { + Ok(conn) => { + self.state.write().credentials = Some(credentials.clone()); + if !read_from_keychain && IMPERSONATE_LOGIN.is_none() { + write_credentials_to_keychain(&credentials, cx).log_err(); + } + self.set_connection(conn, cx); + Ok(()) + } + Err(EstablishConnectionError::Unauthorized) => { + self.state.write().credentials.take(); + if read_from_keychain { + cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); + self.set_status(Status::SignedOut, cx); + self.authenticate_and_connect(false, cx).await + } else { + self.set_status(Status::ConnectionError, cx); + Err(EstablishConnectionError::Unauthorized)? + } + } + Err(EstablishConnectionError::UpgradeRequired) => { + self.set_status(Status::UpgradeRequired, cx); + Err(EstablishConnectionError::UpgradeRequired)? + } + Err(error) => { + self.set_status(Status::ConnectionError, cx); + Err(error)? + } } } - Err(EstablishConnectionError::UpgradeRequired) => { - self.set_status(Status::UpgradeRequired, cx); - Err(EstablishConnectionError::UpgradeRequired)? - } - Err(error) => { + _ = cx.background().timer(CONNECTION_TIMEOUT).fuse() => { self.set_status(Status::ConnectionError, cx); - Err(error)? + Err(anyhow!("timed out trying to establish connection")) } } } - async fn set_connection(self: &Arc, conn: Connection, cx: &AsyncAppContext) { + fn set_connection(self: &Arc, conn: Connection, cx: &AsyncAppContext) { let executor = cx.background(); log::info!("add connection to peer"); let (connection_id, handle_io, mut incoming) = self .peer - .add_connection(conn, move |duration| executor.timer(duration)) - .await; + .add_connection(conn, move |duration| executor.timer(duration)); log::info!("set status to connected {}", connection_id); self.set_status(Status::Connected { connection_id }, cx); cx.foreground() @@ -1169,6 +1178,76 @@ mod tests { assert_eq!(server.auth_count(), 2); // Client re-authenticated due to an invalid token } + #[gpui::test(iterations = 10)] + async fn test_connection_timeout(deterministic: Arc, cx: &mut TestAppContext) { + deterministic.forbid_parking(); + + let user_id = 5; + let client = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let mut status = client.status(); + + // Time out when client tries to connect. + client.override_authenticate(move |cx| { + cx.foreground().spawn(async move { + Ok(Credentials { + user_id, + access_token: "token".into(), + }) + }) + }); + client.override_establish_connection(|_, cx| { + cx.foreground().spawn(async move { + future::pending::<()>().await; + unreachable!() + }) + }); + let auth_and_connect = cx.spawn({ + let client = client.clone(); + |cx| async move { client.authenticate_and_connect(false, &cx).await } + }); + deterministic.run_until_parked(); + assert!(matches!(status.next().await, Some(Status::Connecting))); + + deterministic.advance_clock(CONNECTION_TIMEOUT); + assert!(matches!( + status.next().await, + Some(Status::ConnectionError { .. }) + )); + auth_and_connect.await.unwrap_err(); + + // Allow the connection to be established. + let server = FakeServer::for_client(user_id, &client, cx).await; + assert!(matches!( + status.next().await, + Some(Status::Connected { .. }) + )); + + // Disconnect client. + server.forbid_connections(); + server.disconnect(); + while !matches!(status.next().await, Some(Status::ReconnectionError { .. })) {} + + // Time out when re-establishing the connection. + server.allow_connections(); + client.override_establish_connection(|_, cx| { + cx.foreground().spawn(async move { + future::pending::<()>().await; + unreachable!() + }) + }); + deterministic.advance_clock(2 * INITIAL_RECONNECTION_DELAY); + assert!(matches!( + status.next().await, + Some(Status::Reconnecting { .. }) + )); + + deterministic.advance_clock(CONNECTION_TIMEOUT); + assert!(matches!( + status.next().await, + Some(Status::ReconnectionError { .. }) + )); + } + #[gpui::test(iterations = 10)] async fn test_authenticating_more_than_once( cx: &mut TestAppContext, diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 288c9a31fa..ade21f02f4 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -82,7 +82,7 @@ impl FakeServer { let (client_conn, server_conn, _) = Connection::in_memory(cx.background()); let (connection_id, io, incoming) = - peer.add_test_connection(server_conn, cx.background()).await; + peer.add_test_connection(server_conn, cx.background()); cx.background().spawn(io).detach(); let mut state = state.lock(); state.connection_id = Some(connection_id); @@ -101,10 +101,12 @@ impl FakeServer { } pub fn disconnect(&self) { - self.peer.disconnect(self.connection_id()); - let mut state = self.state.lock(); - state.connection_id.take(); - state.incoming.take(); + if self.state.lock().connection_id.is_some() { + self.peer.disconnect(self.connection_id()); + let mut state = self.state.lock(); + state.connection_id.take(); + state.incoming.take(); + } } pub fn auth_count(&self) -> usize { diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 840199c2bb..cf6b7f8b68 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -16,7 +16,8 @@ required-features = ["seed-support"] collections = { path = "../collections" } rpc = { path = "../rpc" } util = { path = "../util" } - +fs = { path = "../fs" } +rope = { path = "../rope" } anyhow = "1.0.40" async-trait = "0.1.50" async-tungstenite = "0.16" diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index f289dd9d27..bac5cc040e 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -15,6 +15,7 @@ use editor::{ self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; +use fs::{FakeFs, Fs as _, LineEnding}; use futures::{channel::mpsc, Future, StreamExt as _}; use gpui::{ executor::{self, Deterministic}, @@ -24,17 +25,16 @@ use gpui::{ }; use language::{ range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, - LanguageConfig, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope, + LanguageConfig, LanguageRegistry, OffsetRangeExt, Rope, }; use lsp::{self, FakeLanguageServer}; use parking_lot::Mutex; use project::{ - fs::{FakeFs, Fs as _}, - search::SearchQuery, - worktree::WorktreeHandle, - DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId, + search::SearchQuery, worktree::WorktreeHandle, DiagnosticSummary, Project, ProjectPath, + ProjectStore, WorktreeId, }; use rand::prelude::*; +use rope::point::Point; use rpc::PeerId; use serde_json::json; use settings::{Formatter, Settings}; @@ -2017,7 +2017,7 @@ async fn test_leaving_project( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); - let project_b = client_b.build_remote_project(project_id, cx_b).await; + let project_b1 = client_b.build_remote_project(project_id, cx_b).await; let project_c = client_c.build_remote_project(project_id, cx_c).await; // Client A sees that a guest has joined. @@ -2025,20 +2025,62 @@ async fn test_leaving_project( project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 2); }); - project_b.read_with(cx_b, |project, _| { + project_b1.read_with(cx_b, |project, _| { assert_eq!(project.collaborators().len(), 2); }); project_c.read_with(cx_c, |project, _| { assert_eq!(project.collaborators().len(), 2); }); - // Drop client B's connection and ensure client A and client C observe client B leaving the project. + // Client B opens a buffer. + let buffer_b1 = project_b1 + .update(cx_b, |project, cx| { + let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id(); + project.open_buffer((worktree_id, "a.txt"), cx) + }) + .await + .unwrap(); + buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents")); + + // Drop client B's project and ensure client A and client C observe client B leaving. + cx_b.update(|_| drop(project_b1)); + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 1); + }); + project_c.read_with(cx_c, |project, _| { + assert_eq!(project.collaborators().len(), 1); + }); + + // Client B re-joins the project and can open buffers as before. + let project_b2 = client_b.build_remote_project(project_id, cx_b).await; + deterministic.run_until_parked(); + project_a.read_with(cx_a, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + project_b2.read_with(cx_b, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + project_c.read_with(cx_c, |project, _| { + assert_eq!(project.collaborators().len(), 2); + }); + + let buffer_b2 = project_b2 + .update(cx_b, |project, cx| { + let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id(); + project.open_buffer((worktree_id, "a.txt"), cx) + }) + .await + .unwrap(); + buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents")); + + // Drop client B's connection and ensure client A and client C observe client B leaving. client_b.disconnect(&cx_b.to_async()).unwrap(); deterministic.run_until_parked(); project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 1); }); - project_b.read_with(cx_b, |project, _| { + project_b2.read_with(cx_b, |project, _| { assert!(project.is_read_only()); }); project_c.read_with(cx_c, |project, _| { @@ -2068,7 +2110,7 @@ async fn test_leaving_project( project_a.read_with(cx_a, |project, _| { assert_eq!(project.collaborators().len(), 0); }); - project_b.read_with(cx_b, |project, _| { + project_b2.read_with(cx_b, |project, _| { assert!(project.is_read_only()); }); project_c.read_with(cx_c, |project, _| { @@ -2117,15 +2159,10 @@ async fn test_collaborating_with_diagnostics( ) .await; let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); // Cause the language server to start. - let _buffer = cx_a - .background() - .spawn(project_a.update(cx_a, |project, cx| { + let _buffer = project_a + .update(cx_a, |project, cx| { project.open_buffer( ProjectPath { worktree_id, @@ -2133,18 +2170,35 @@ async fn test_collaborating_with_diagnostics( }, cx, ) - })) + }) .await .unwrap(); - // Join the worktree as client B. - let project_b = client_b.build_remote_project(project_id, cx_b).await; - // Simulate a language server reporting errors for a file. let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server .receive_notification::() .await; + fake_language_server.notify::( + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), + version: None, + diagnostics: vec![lsp::Diagnostic { + severity: Some(lsp::DiagnosticSeverity::WARNING), + range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)), + message: "message 0".to_string(), + ..Default::default() + }], + }, + ); + + // Client A shares the project and, simultaneously, the language server + // publishes a diagnostic. This is done to ensure that the server always + // observes the latest diagnostics for a worktree. + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); fake_language_server.notify::( lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/a/a.rs").unwrap(), @@ -2158,6 +2212,9 @@ async fn test_collaborating_with_diagnostics( }, ); + // Join the worktree as client B. + let project_b = client_b.build_remote_project(project_id, cx_b).await; + // Wait for server to see the diagnostics update. deterministic.run_until_parked(); { @@ -2187,24 +2244,35 @@ async fn test_collaborating_with_diagnostics( // Join project as client C and observe the diagnostics. let project_c = client_c.build_remote_project(project_id, cx_c).await; - deterministic.run_until_parked(); - project_c.read_with(cx_c, |project, cx| { - assert_eq!( - project.diagnostic_summaries(cx).collect::>(), - &[( - ProjectPath { - worktree_id, - path: Arc::from(Path::new("a.rs")), - }, - DiagnosticSummary { - error_count: 1, - warning_count: 0, - ..Default::default() - }, - )] - ) + let project_c_diagnostic_summaries = Rc::new(RefCell::new(Vec::new())); + project_c.update(cx_c, |_, cx| { + let summaries = project_c_diagnostic_summaries.clone(); + cx.subscribe(&project_c, { + move |p, _, event, cx| { + if let project::Event::DiskBasedDiagnosticsFinished { .. } = event { + *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect(); + } + } + }) + .detach(); }); + deterministic.run_until_parked(); + assert_eq!( + project_c_diagnostic_summaries.borrow().as_slice(), + &[( + ProjectPath { + worktree_id, + path: Arc::from(Path::new("a.rs")), + }, + DiagnosticSummary { + error_count: 1, + warning_count: 0, + ..Default::default() + }, + )] + ); + // Simulate a language server reporting more errors for a file. fake_language_server.notify::( lsp::PublishDiagnosticsParams { @@ -2279,7 +2347,7 @@ async fn test_collaborating_with_diagnostics( DiagnosticEntry { range: Point::new(0, 4)..Point::new(0, 7), diagnostic: Diagnostic { - group_id: 1, + group_id: 2, message: "message 1".to_string(), severity: lsp::DiagnosticSeverity::ERROR, is_primary: true, @@ -2289,7 +2357,7 @@ async fn test_collaborating_with_diagnostics( DiagnosticEntry { range: Point::new(0, 10)..Point::new(0, 13), diagnostic: Diagnostic { - group_id: 2, + group_id: 3, severity: lsp::DiagnosticSeverity::WARNING, message: "message 2".to_string(), is_primary: true, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 84449e79d5..564e173fec 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -365,8 +365,7 @@ impl Server { timer.await; } } - }) - .await; + }); tracing::info!(%user_id, %login, %connection_id, %address, "connection opened"); @@ -1013,6 +1012,21 @@ impl Server { } } + for language_server in &project.language_servers { + self.peer.send( + request.sender_id, + proto::UpdateLanguageServer { + project_id: project_id.to_proto(), + language_server_id: language_server.id, + variant: Some( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ), + }, + )?; + } + Ok(()) } diff --git a/crates/collab/src/rpc/store.rs b/crates/collab/src/rpc/store.rs index cc34094782..b7dd39cff1 100644 --- a/crates/collab/src/rpc/store.rs +++ b/crates/collab/src/rpc/store.rs @@ -1205,7 +1205,7 @@ impl Store { let guest_connection = self.connections.get(guest_connection_id).unwrap(); assert!(guest_connection.projects.contains(project_id)); } - assert_eq!(project.active_replica_ids.len(), project.guests.len(),); + assert_eq!(project.active_replica_ids.len(), project.guests.len()); assert_eq!( project.active_replica_ids, project diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 9faea76a10..928cf3273b 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -17,10 +17,7 @@ use std::ops::Range; use theme::Theme; use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace}; -actions!( - contacts_titlebar_item, - [ToggleContactsPopover, ShareProject] -); +actions!(collab, [ToggleCollaborationMenu, ShareProject]); pub fn init(cx: &mut MutableAppContext) { cx.add_action(CollabTitlebarItem::toggle_contacts_popover); @@ -143,7 +140,11 @@ impl CollabTitlebarItem { } } - fn toggle_contacts_popover(&mut self, _: &ToggleContactsPopover, cx: &mut ViewContext) { + pub fn toggle_contacts_popover( + &mut self, + _: &ToggleCollaborationMenu, + cx: &mut ViewContext, + ) { match self.contacts_popover.take() { Some(_) => {} None => { @@ -197,7 +198,7 @@ impl CollabTitlebarItem { }; Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |state, _| { + MouseEventHandler::::new(0, cx, |state, _| { let style = titlebar .toggle_contacts_button .style_for(state, self.contacts_popover.is_some()); @@ -214,8 +215,8 @@ impl CollabTitlebarItem { .boxed() }) .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(ToggleContactsPopover); + .on_click(MouseButton::Left, move |_, cx| { + cx.dispatch_action(ToggleCollaborationMenu); }) .aligned() .boxed(), diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 4b7e3dae01..f5f508ce5b 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -8,7 +8,7 @@ mod notifications; mod project_shared_notification; use call::ActiveCall; -pub use collab_titlebar_item::CollabTitlebarItem; +pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu}; use gpui::MutableAppContext; use project::Project; use std::sync::Arc; diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 616f69117f..c4b851917e 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -15,6 +15,7 @@ editor = { path = "../editor" } language = { path = "../language" } gpui = { path = "../gpui" } project = { path = "../project" } +rope = { path = "../rope" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 3111d7a9f1..608b333d0d 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -14,10 +14,10 @@ use gpui::{ ViewHandle, WeakViewHandle, }; use language::{ - Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, - SelectionGoal, + Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Selection, SelectionGoal, }; use project::{DiagnosticSummary, Project, ProjectPath}; +use rope::point::Point; use serde_json::json; use settings::Settings; use smallvec::SmallVec; @@ -738,7 +738,8 @@ mod tests { DisplayPoint, }; use gpui::TestAppContext; - use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16}; + use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity}; + use rope::point_utf16::PointUtf16; use serde_json::json; use unindent::Unindent as _; use workspace::AppState; diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index e8695e5ddc..4e0a10b70d 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -30,6 +30,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } project = { path = "../project" } +rope = { path = "../rope" } rpc = { path = "../rpc" } settings = { path = "../settings" } snippet = { path = "../snippet" } @@ -48,7 +49,7 @@ ordered-float = "2.1.1" parking_lot = "0.11" postage = { version = "0.4", features = ["futures-traits"] } rand = { version = "0.8.3", optional = true } -serde = { version = "1.0", features = ["derive", "rc"] } +serde = { workspace = true } smallvec = { version = "1.6", features = ["union"] } smol = "1.2" tree-sitter-rust = { version = "*", optional = true } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index ac85bf68ee..91a3a30267 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -11,7 +11,8 @@ use gpui::{ fonts::{FontId, HighlightStyle}, Entity, ModelContext, ModelHandle, }; -use language::{OffsetUtf16, Point, Subscription as BufferSubscription}; +use language::Subscription as BufferSubscription; +use rope::{offset_utf16::OffsetUtf16, point::Point}; use settings::Settings; use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; @@ -622,7 +623,7 @@ pub mod tests { use super::*; use crate::{movement, test::marked_display_snapshot}; use gpui::{color::Color, elements::*, test::observe, MutableAppContext}; - use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal}; + use language::{Buffer, Language, LanguageConfig, SelectionGoal}; use rand::{prelude::*, Rng}; use smol::stream::StreamExt; use std::{env, sync::Arc}; @@ -666,7 +667,9 @@ pub mod tests { let buffer = cx.update(|cx| { if rng.gen() { let len = rng.gen_range(0..10); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); MultiBuffer::build_simple(&text, cx) } else { MultiBuffer::build_random(&mut rng, cx) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 210daccac2..f0f2720f1a 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -7,6 +7,7 @@ use collections::{Bound, HashMap, HashSet}; use gpui::{ElementBox, RenderContext}; use language::{BufferSnapshot, Chunk, Patch}; use parking_lot::Mutex; +use rope::point::Point; use std::{ cell::RefCell, cmp::{self, Ordering}, @@ -18,7 +19,7 @@ use std::{ }, }; use sum_tree::{Bias, SumTree}; -use text::{Edit, Point}; +use text::Edit; const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; @@ -42,7 +43,7 @@ pub struct BlockSnapshot { pub struct BlockId(usize); #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct BlockPoint(pub super::Point); +pub struct BlockPoint(pub Point); #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] struct BlockRow(u32); @@ -994,7 +995,7 @@ mod tests { use rand::prelude::*; use settings::Settings; use std::env; - use text::RandomCharIter; + use util::RandomCharIter; #[gpui::test] fn test_offset_for_row() { diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index c17cfa39f2..5bd1670542 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -5,8 +5,9 @@ use crate::{ }; use collections::BTreeMap; use gpui::fonts::HighlightStyle; -use language::{Chunk, Edit, Point, TextSummary}; +use language::{Chunk, Edit, TextSummary}; use parking_lot::Mutex; +use rope::point::Point; use std::{ any::TypeId, cmp::{self, Ordering}, @@ -18,11 +19,11 @@ use std::{ use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct FoldPoint(pub super::Point); +pub struct FoldPoint(pub Point); impl FoldPoint { pub fn new(row: u32, column: u32) -> Self { - Self(super::Point::new(row, column)) + Self(Point::new(row, column)) } pub fn row(self) -> u32 { @@ -1196,8 +1197,8 @@ mod tests { use settings::Settings; use std::{cmp::Reverse, env, mem, sync::Arc}; use sum_tree::TreeMap; - use text::RandomCharIter; use util::test::sample_text; + use util::RandomCharIter; use Bias::{Left, Right}; #[gpui::test] diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 4d89767a19..b7d8fac770 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -3,11 +3,12 @@ use super::{ TextHighlights, }; use crate::MultiBufferSnapshot; -use language::{rope, Chunk}; +use language::Chunk; use parking_lot::Mutex; +use rope; +use rope::point::Point; use std::{cmp, mem, num::NonZeroU32, ops::Range}; use sum_tree::Bias; -use text::Point; pub struct TabMap(Mutex); @@ -332,11 +333,11 @@ impl TabSnapshot { } #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct TabPoint(pub super::Point); +pub struct TabPoint(pub Point); impl TabPoint { pub fn new(row: u32, column: u32) -> Self { - Self(super::Point::new(row, column)) + Self(Point::new(row, column)) } pub fn zero() -> Self { @@ -352,8 +353,8 @@ impl TabPoint { } } -impl From for TabPoint { - fn from(point: super::Point) -> Self { +impl From for TabPoint { + fn from(point: Point) -> Self { Self(point) } } @@ -362,7 +363,7 @@ pub type TabEdit = text::Edit; #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TextSummary { - pub lines: super::Point, + pub lines: Point, pub first_line_chars: u32, pub last_line_chars: u32, pub longest_row: u32, @@ -485,7 +486,6 @@ mod tests { use super::*; use crate::{display_map::fold_map::FoldMap, MultiBuffer}; use rand::{prelude::StdRng, Rng}; - use text::{RandomCharIter, Rope}; #[test] fn test_expand_tabs() { @@ -508,7 +508,9 @@ mod tests { let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap(); let len = rng.gen_range(0..30); let buffer = if rng.gen() { - let text = RandomCharIter::new(&mut rng).take(len).collect::(); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); MultiBuffer::build_simple(&text, cx) } else { MultiBuffer::build_random(&mut rng, cx) @@ -522,7 +524,7 @@ mod tests { log::info!("FoldMap text: {:?}", folds_snapshot.text()); let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size); - let text = Rope::from(tabs_snapshot.text().as_str()); + let text = rope::Rope::from(tabs_snapshot.text().as_str()); log::info!( "TabMap text (tab size: {}): {:?}", tab_size, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index ee6ce2860d..42156b905f 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -3,13 +3,14 @@ use super::{ tab_map::{self, TabEdit, TabPoint, TabSnapshot}, TextHighlights, }; -use crate::{MultiBufferSnapshot, Point}; +use crate::MultiBufferSnapshot; use gpui::{ fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext, Task, }; use language::Chunk; use lazy_static::lazy_static; +use rope::point::Point; use smol::future::yield_now; use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration}; use sum_tree::{Bias, Cursor, SumTree}; @@ -52,7 +53,7 @@ struct TransformSummary { } #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct WrapPoint(pub super::Point); +pub struct WrapPoint(pub Point); pub struct WrapChunks<'a> { input_chunks: tab_map::TabChunks<'a>, @@ -959,7 +960,7 @@ impl SumTreeExt for SumTree { impl WrapPoint { pub fn new(row: u32, column: u32) -> Self { - Self(super::Point::new(row, column)) + Self(Point::new(row, column)) } pub fn row(self) -> u32 { @@ -1029,7 +1030,6 @@ mod tests { MultiBuffer, }; use gpui::test::observe; - use language::RandomCharIter; use rand::prelude::*; use settings::Settings; use smol::stream::StreamExt; @@ -1067,7 +1067,9 @@ mod tests { MultiBuffer::build_random(&mut rng, cx) } else { let len = rng.gen_range(0..10); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); MultiBuffer::build_simple(&text, cx) } }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4be6d72017..c8bb16ee00 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -43,8 +43,8 @@ pub use items::MAX_TAB_TITLE_LEN; pub use language::{char_kind, CharKind}; use language::{ AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, - DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, - Selection, SelectionGoal, TransactionId, + DiagnosticSeverity, IndentKind, IndentSize, Language, OffsetRangeExt, Selection, SelectionGoal, + TransactionId, }; use link_go_to_definition::{hide_link_definition, LinkGoToDefinitionState}; pub use multi_buffer::{ @@ -54,6 +54,7 @@ pub use multi_buffer::{ use multi_buffer::{MultiBufferChunks, ToOffsetUtf16}; use ordered_float::OrderedFloat; use project::{FormatTrigger, LocationLink, Project, ProjectPath, ProjectTransaction}; +use rope::{offset_utf16::OffsetUtf16, point::Point}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; use settings::Settings; @@ -1271,7 +1272,7 @@ impl Editor { let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) { (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.) } else { - display_map.max_point().row().saturating_sub(1) as f32 + display_map.max_point().row() as f32 }; if scroll_position.y() > max_scroll_top { scroll_position.set_y(max_scroll_top); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 24260a497e..8b41990574 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -15,8 +15,8 @@ use gpui::{ }; use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry}; use project::FakeFs; +use rope::point::Point; use settings::EditorSettings; -use text::Point; use util::{ assert_set_eq, test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ec39bf1eb2..f29eb52804 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -35,8 +35,9 @@ use gpui::{ WeakViewHandle, }; use json::json; -use language::{Bias, DiagnosticSeverity, OffsetUtf16, Selection}; +use language::{Bias, DiagnosticSeverity, Selection}; use project::ProjectPath; +use rope::offset_utf16::OffsetUtf16; use settings::{GitGutter, Settings}; use smallvec::SmallVec; use std::{ @@ -916,36 +917,30 @@ impl EditorElement { let view = self.view.clone(); let style = &self.style.theme.scrollbar; - let min_thumb_height = - style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size); let top = bounds.min_y(); let bottom = bounds.max_y(); let right = bounds.max_x(); let left = right - style.width; - let height = bounds.height(); let row_range = &layout.scrollbar_row_range; - let max_row = layout.max_row + ((row_range.end - row_range.start) as u32); - let scrollbar_start = row_range.start as f32 / max_row as f32; - let scrollbar_end = row_range.end as f32 / max_row as f32; + let max_row = layout.max_row as f32 + (row_range.end - row_range.start); - let mut thumb_top = top + scrollbar_start * height; - let mut thumb_bottom = top + scrollbar_end * height; - let thumb_center = (thumb_top + thumb_bottom) / 2.0; + let mut height = bounds.height(); + let mut first_row_y_offset = 0.0; - if thumb_bottom - thumb_top < min_thumb_height { - thumb_top = thumb_center - min_thumb_height / 2.0; - thumb_bottom = thumb_center + min_thumb_height / 2.0; - if thumb_top < top { - thumb_top = top; - thumb_bottom = top + min_thumb_height; - } - if thumb_bottom > bottom { - thumb_bottom = bottom; - thumb_top = bottom - min_thumb_height; - } + // Impose a minimum height on the scrollbar thumb + let min_thumb_height = + style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size); + let thumb_height = (row_range.end - row_range.start) * height / max_row; + if thumb_height < min_thumb_height { + first_row_y_offset = (min_thumb_height - thumb_height) / 2.0; + height -= min_thumb_height - thumb_height; } + let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * height / max_row }; + + let thumb_top = y_for_row(row_range.start) - first_row_y_offset; + let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset; let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom)); let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom)); @@ -1587,11 +1582,14 @@ impl Element for EditorElement { // The scroll position is a fractional point, the whole number of which represents // the top of the window in terms of display rows. let start_row = scroll_position.y() as u32; - let visible_row_count = (size.y() / line_height).ceil() as u32; + let height_in_lines = size.y() / line_height; let max_row = snapshot.max_point().row(); // Add 1 to ensure selections bleed off screen - let end_row = 1 + cmp::min(start_row + visible_row_count, max_row); + let end_row = 1 + cmp::min( + (scroll_position.y() + height_in_lines).ceil() as u32, + max_row, + ); let start_anchor = if start_row == 0 { Anchor::min() @@ -1685,8 +1683,7 @@ impl Element for EditorElement { .git_diff_hunks_in_range(start_row..end_row) .collect(); - let scrollbar_row_range = - scroll_position.y()..(scroll_position.y() + visible_row_count as f32); + let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines); let mut max_visible_line_width = 0.0; let line_layouts = self.layout_lines(start_row..end_row, &snapshot, cx); @@ -1723,7 +1720,7 @@ impl Element for EditorElement { let scroll_max = vec2f( ((scroll_width - text_size.x()) / em_width).max(0.0), - max_row.saturating_sub(1) as f32, + max_row as f32, ); self.update_view(cx.app, |view, cx| { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index c1082020e5..727ee1f094 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -11,6 +11,7 @@ use gpui::{ }; use language::{Bias, Buffer, File as _, OffsetRangeExt, SelectionGoal}; use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath}; +use rope::point::Point; use rpc::proto::{self, update_view}; use settings::Settings; use smallvec::SmallVec; @@ -21,7 +22,7 @@ use std::{ ops::Range, path::{Path, PathBuf}, }; -use text::{Point, Selection}; +use text::Selection; use util::TryFutureExt; use workspace::{ searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 9261a6d445..e5dcf94841 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,6 +1,7 @@ +use rope::point::Point; + use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, ToPoint}; -use language::Point; use std::ops::Range; pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { @@ -336,7 +337,7 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range< mod tests { use super::*; use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer}; - use language::Point; + use rope::point::Point; use settings::Settings; #[gpui::test] diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a0eedb850c..23ee7df657 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -12,6 +12,7 @@ use language::{ DiagnosticEntry, Event, File, IndentSize, Language, OffsetRangeExt, Outline, OutlineItem, Selection, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, }; +use rope::{offset_utf16::OffsetUtf16, point::Point, point_utf16::PointUtf16, TextDimension}; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -27,9 +28,8 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{ locator::Locator, - rope::TextDimension, subscription::{Subscription, Topic}, - Edit, OffsetUtf16, Point, PointUtf16, TextSummary, + Edit, TextSummary, }; use theme::SyntaxTheme; use util::post_inc; @@ -168,7 +168,7 @@ struct ExcerptChunks<'a> { } struct ExcerptBytes<'a> { - content_bytes: language::rope::Bytes<'a>, + content_bytes: rope::Bytes<'a>, footer_height: usize, } @@ -1412,7 +1412,7 @@ impl MultiBuffer { edit_count: usize, cx: &mut ModelContext, ) { - use text::RandomCharIter; + use util::RandomCharIter; let snapshot = self.read(cx); let mut edits: Vec<(Range, Arc)> = Vec::new(); @@ -1451,7 +1451,7 @@ impl MultiBuffer { ) { use rand::prelude::*; use std::env; - use text::RandomCharIter; + use util::RandomCharIter; let max_excerpts = env::var("MAX_EXCERPTS") .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable")) @@ -3337,7 +3337,7 @@ mod tests { use rand::prelude::*; use settings::Settings; use std::{env, rc::Rc}; - use text::{Point, RandomCharIter}; + use util::test::sample_text; #[gpui::test] @@ -3955,7 +3955,9 @@ mod tests { } _ => { let buffer_handle = if buffers.is_empty() || rng.gen_bool(0.4) { - let base_text = RandomCharIter::new(&mut rng).take(10).collect::(); + let base_text = util::RandomCharIter::new(&mut rng) + .take(10) + .collect::(); buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx))); buffers.last().unwrap() } else { diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index cb8a1692b9..b30e4b5780 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -1,10 +1,10 @@ use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToOffsetUtf16, ToPoint}; +use rope::{offset_utf16::OffsetUtf16, point::Point, TextDimension}; use std::{ cmp::Ordering, ops::{Range, Sub}, }; use sum_tree::Bias; -use text::{rope::TextDimension, OffsetUtf16, Point}; #[derive(Clone, Eq, PartialEq, Debug, Hash)] pub struct Anchor { diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 9d6450f8ec..ed983d22d9 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -8,7 +8,8 @@ use std::{ use collections::HashMap; use gpui::{AppContext, ModelHandle, MutableAppContext}; use itertools::Itertools; -use language::{rope::TextDimension, Bias, Point, Selection, SelectionGoal, ToPoint}; +use language::{Bias, Selection, SelectionGoal, ToPoint}; +use rope::{point::Point, TextDimension}; use util::post_inc; use crate::{ diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml new file mode 100644 index 0000000000..5b9082d114 --- /dev/null +++ b/crates/fs/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "fs" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/fs.rs" + +[dependencies] +collections = { path = "../collections" } +gpui = { path = "../gpui" } +lsp = { path = "../lsp" } +rope = { path = "../rope" } +util = { path = "../util" } +anyhow = "1.0.57" +async-trait = "0.1" +futures = "0.3" +tempfile = "3" +fsevent = { path = "../fsevent" } +lazy_static = "1.4.0" +parking_lot = "0.11.1" +smol = "1.2.5" +regex = "1.5" +git2 = { version = "0.15", default-features = false } +serde = { workspace = true } +serde_json = { workspace = true } +log = { version = "0.4.16", features = ["kv_unstable_serde"] } +libc = "0.2" + +[features] +test-support = [] diff --git a/crates/project/src/fs.rs b/crates/fs/src/fs.rs similarity index 91% rename from crates/project/src/fs.rs rename to crates/fs/src/fs.rs index a9a0a1707f..2061d3734b 100644 --- a/crates/project/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -1,10 +1,18 @@ +pub mod repository; + use anyhow::{anyhow, Result}; use fsevent::EventStream; use futures::{future::BoxFuture, Stream, StreamExt}; -use git::repository::{GitRepository, LibGitRepository}; -use language::LineEnding; +use git2::Repository as LibGitRepository; +use lazy_static::lazy_static; use parking_lot::Mutex as SyncMutex; +use regex::Regex; +use repository::GitRepository; +use rope::Rope; use smol::io::{AsyncReadExt, AsyncWriteExt}; +use std::borrow::Cow; +use std::cmp; +use std::io::Write; use std::sync::Arc; use std::{ io, @@ -13,7 +21,7 @@ use std::{ pin::Pin, time::{Duration, SystemTime}, }; -use text::Rope; +use tempfile::NamedTempFile; use util::ResultExt; #[cfg(any(test, feature = "test-support"))] @@ -21,10 +29,69 @@ use collections::{btree_map, BTreeMap}; #[cfg(any(test, feature = "test-support"))] use futures::lock::Mutex; #[cfg(any(test, feature = "test-support"))] -use git::repository::FakeGitRepositoryState; +use repository::FakeGitRepositoryState; #[cfg(any(test, feature = "test-support"))] use std::sync::Weak; +lazy_static! { + static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap(); +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineEnding { + Unix, + Windows, +} + +impl Default for LineEnding { + fn default() -> Self { + #[cfg(unix)] + return Self::Unix; + + #[cfg(not(unix))] + return Self::CRLF; + } +} + +impl LineEnding { + pub fn as_str(&self) -> &'static str { + match self { + LineEnding::Unix => "\n", + LineEnding::Windows => "\r\n", + } + } + + pub fn detect(text: &str) -> Self { + let mut max_ix = cmp::min(text.len(), 1000); + while !text.is_char_boundary(max_ix) { + max_ix -= 1; + } + + if let Some(ix) = text[..max_ix].find(&['\n']) { + if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { + Self::Windows + } else { + Self::Unix + } + } else { + Self::default() + } + } + + pub fn normalize(text: &mut String) { + if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") { + *text = replaced; + } + } + + pub fn normalize_arc(text: Arc) -> Arc { + if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") { + replaced.into() + } else { + text + } + } +} #[async_trait::async_trait] pub trait Fs: Send + Sync { async fn create_dir(&self, path: &Path) -> Result<()>; @@ -35,6 +102,7 @@ pub trait Fs: Send + Sync { async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn open_sync(&self, path: &Path) -> Result>; async fn load(&self, path: &Path) -> Result; + async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>; async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; async fn is_file(&self, path: &Path) -> bool; @@ -86,6 +154,33 @@ pub struct Metadata { pub is_dir: bool, } +impl From for CreateOptions { + fn from(options: lsp::CreateFileOptions) -> Self { + Self { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + } + } +} + +impl From for RenameOptions { + fn from(options: lsp::RenameFileOptions) -> Self { + Self { + overwrite: options.overwrite.unwrap_or(false), + ignore_if_exists: options.ignore_if_exists.unwrap_or(false), + } + } +} + +impl From for RemoveOptions { + fn from(options: lsp::DeleteFileOptions) -> Self { + Self { + recursive: options.recursive.unwrap_or(false), + ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), + } + } +} + pub struct RealFs; #[async_trait::async_trait] @@ -168,6 +263,18 @@ impl Fs for RealFs { Ok(text) } + async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { + smol::unblock(move || { + let mut tmp_file = NamedTempFile::new()?; + tmp_file.write_all(data.as_bytes())?; + tmp_file.persist(path)?; + Ok::<(), anyhow::Error>(()) + }) + .await?; + + Ok(()) + } + async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { let buffer_size = text.summary().len.min(10 * 1024); let file = smol::fs::File::create(path).await?; @@ -285,7 +392,7 @@ enum FakeFsEntry { inode: u64, mtime: SystemTime, entries: BTreeMap>>, - git_repo_state: Option>>, + git_repo_state: Option>>, }, Symlink { target: PathBuf, @@ -788,6 +895,14 @@ impl Fs for FakeFs { entry.file_content(&path).cloned() } + async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> { + self.simulate_random_delay().await; + let path = normalize_path(path.as_path()); + self.insert_file(path, data.to_string()).await; + + Ok(()) + } + async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> { self.simulate_random_delay().await; let path = normalize_path(path); @@ -897,7 +1012,7 @@ impl Fs for FakeFs { Arc::new(SyncMutex::new(FakeGitRepositoryState::default())) }) .clone(); - Some(git::repository::FakeGitRepository::open(state)) + Some(repository::FakeGitRepository::open(state)) } else { None } diff --git a/crates/git/src/repository.rs b/crates/fs/src/repository.rs similarity index 100% rename from crates/git/src/repository.rs rename to crates/fs/src/repository.rs diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index b8f3aac0b9..1d15c2b123 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -9,7 +9,7 @@ path = "src/git.rs" [dependencies] anyhow = "1.0.38" clock = { path = "../clock" } -git2 = { version = "0.15", default-features = false } +rope = { path = "../rope" } lazy_static = "1.4.0" sum_tree = { path = "../sum_tree" } text = { path = "../text" } @@ -20,6 +20,7 @@ smol = "1.2" parking_lot = "0.11.1" async-trait = "0.1" futures = "0.3" +git2 = { version = "0.15", default-features = false } [dev-dependencies] unindent = "0.1.7" diff --git a/crates/git/src/diff.rs b/crates/git/src/diff.rs index 4191e5d260..7f3f6101ce 100644 --- a/crates/git/src/diff.rs +++ b/crates/git/src/diff.rs @@ -1,7 +1,8 @@ use std::ops::Range; +use rope::point::Point; use sum_tree::SumTree; -use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; +use text::{Anchor, BufferSnapshot, OffsetRangeExt}; pub use git2 as libgit; use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 36f54e706a..b1b885eca2 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -4,7 +4,6 @@ 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"); diff --git a/crates/go_to_line/Cargo.toml b/crates/go_to_line/Cargo.toml index 93ae96f93e..d69b1f239c 100644 --- a/crates/go_to_line/Cargo.toml +++ b/crates/go_to_line/Cargo.toml @@ -13,5 +13,6 @@ gpui = { path = "../gpui" } menu = { path = "../menu" } settings = { path = "../settings" } text = { path = "../text" } +rope = { path = "../rope" } workspace = { path = "../workspace" } postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 3ca50cee42..51ff87a943 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -4,8 +4,9 @@ use gpui::{ MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; use menu::{Cancel, Confirm}; +use rope::point::Point; use settings::Settings; -use text::{Bias, Point}; +use text::Bias; use workspace::Workspace; actions!(go_to_line, [Toggle]); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index e5b6f4c79b..54d2147929 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -25,9 +25,11 @@ client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } fuzzy = { path = "../fuzzy" } +fs = { path = "../fs" } git = { path = "../git" } gpui = { path = "../gpui" } lsp = { path = "../lsp" } +rope = { path = "../rope" } rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a3c0c54d01..a5cf24877c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -13,9 +13,11 @@ use crate::{ }; use anyhow::{anyhow, Result}; use clock::ReplicaId; +use fs::LineEnding; use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; use parking_lot::Mutex; +use rope::point::Point; use settings::Settings; use similar::{ChangeTag, TextDiff}; use smol::future::yield_now; @@ -38,6 +40,8 @@ use sum_tree::TreeMap; use text::operation_queue::OperationQueue; pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, Operation as _, *}; use theme::SyntaxTheme; +#[cfg(any(test, feature = "test-support"))] +use util::RandomCharIter; use util::TryFutureExt as _; #[cfg(any(test, feature = "test-support"))] @@ -368,7 +372,7 @@ impl Buffer { file, ); this.text.set_line_ending(proto::deserialize_line_ending( - proto::LineEnding::from_i32(message.line_ending) + rpc::proto::LineEnding::from_i32(message.line_ending) .ok_or_else(|| anyhow!("missing line_ending"))?, )); Ok(this) @@ -862,6 +866,8 @@ impl Buffer { })); } } + } else { + self.autoindent_requests.clear(); } } @@ -1633,9 +1639,7 @@ impl Buffer { last_end = Some(range.end); let new_text_len = rng.gen_range(0..10); - let new_text: String = crate::random_char_iter::RandomCharIter::new(&mut *rng) - .take(new_text_len) - .collect(); + let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); edits.push((range, new_text)); } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 3cfddce71f..313c843b02 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1,9 +1,11 @@ use super::*; use clock::ReplicaId; use collections::BTreeMap; +use fs::LineEnding; use gpui::{ModelHandle, MutableAppContext}; use proto::deserialize_operation; use rand::prelude::*; +use rope::point::Point; use settings::Settings; use std::{ cell::RefCell, @@ -14,7 +16,7 @@ use std::{ }; use text::network::Network; use unindent::Unindent as _; -use util::{post_inc, test::marked_text_ranges}; +use util::{post_inc, test::marked_text_ranges, RandomCharIter}; #[cfg(test)] #[ctor::ctor] diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index b52327cac0..dfbc32149c 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -1,12 +1,13 @@ use crate::Diagnostic; use collections::HashMap; +use rope::point_utf16::PointUtf16; use std::{ cmp::{Ordering, Reverse}, iter, ops::Range, }; use sum_tree::{self, Bias, SumTree}; -use text::{Anchor, FromAnchor, PointUtf16, ToOffset}; +use text::{Anchor, FromAnchor, ToOffset}; #[derive(Clone, Debug, Default)] pub struct DiagnosticSet { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index bb75edbc32..4f8615606c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -22,6 +22,7 @@ use lazy_static::lazy_static; use parking_lot::{Mutex, RwLock}; use postage::watch; use regex::Regex; +use rope::point_utf16::PointUtf16; use serde::{de, Deserialize, Deserializer}; use serde_json::Value; use std::{ diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index fddfb7961f..9e3ee7d46b 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -8,19 +8,19 @@ use rpc::proto; use std::{ops::Range, sync::Arc}; use text::*; -pub use proto::{BufferState, LineEnding, Operation, SelectionSet}; +pub use proto::{BufferState, Operation, SelectionSet}; -pub fn deserialize_line_ending(message: proto::LineEnding) -> text::LineEnding { +pub fn deserialize_line_ending(message: proto::LineEnding) -> fs::LineEnding { match message { - LineEnding::Unix => text::LineEnding::Unix, - LineEnding::Windows => text::LineEnding::Windows, + proto::LineEnding::Unix => fs::LineEnding::Unix, + proto::LineEnding::Windows => fs::LineEnding::Windows, } } -pub fn serialize_line_ending(message: text::LineEnding) -> proto::LineEnding { +pub fn serialize_line_ending(message: fs::LineEnding) -> proto::LineEnding { match message { - text::LineEnding::Unix => proto::LineEnding::Unix, - text::LineEnding::Windows => proto::LineEnding::Windows, + fs::LineEnding::Unix => proto::LineEnding::Unix, + fs::LineEnding::Windows => proto::LineEnding::Windows, } } diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 64145e535b..3992d41081 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -1,6 +1,7 @@ use crate::{Grammar, InjectionConfig, Language, LanguageRegistry}; use lazy_static::lazy_static; use parking_lot::Mutex; +use rope::point::Point; use std::{ borrow::Cow, cell::RefCell, @@ -10,7 +11,7 @@ use std::{ sync::Arc, }; use sum_tree::{Bias, SeekTarget, SumTree}; -use text::{rope, Anchor, BufferSnapshot, OffsetRangeExt, Point, Rope, ToOffset, ToPoint}; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Rope, ToOffset, ToPoint}; use tree_sitter::{ Node, Parser, Query, QueryCapture, QueryCaptures, QueryCursor, QueryMatches, Tree, }; @@ -1242,7 +1243,7 @@ mod tests { use crate::LanguageConfig; use rand::rngs::StdRng; use std::env; - use text::{Buffer, Point}; + use text::Buffer; use unindent::Unindent as _; use util::test::marked_text_ranges; diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 1e45e3c6ed..7a41318b86 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -22,12 +22,14 @@ client = { path = "../client" } clock = { path = "../clock" } collections = { path = "../collections" } db = { path = "../db" } +fs = { path = "../fs" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } git = { path = "../git" } gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } +rope = { path = "../rope" } rpc = { path = "../rpc" } settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } @@ -38,7 +40,6 @@ async-trait = "0.1" futures = "0.3" ignore = "0.4" lazy_static = "1.4.0" -libc = "0.2" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } @@ -58,6 +59,7 @@ rocksdb = "0.18" client = { path = "../client", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } db = { path = "../db", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 37f6e76340..42098d2e8b 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -8,10 +8,11 @@ use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, CachedLspAdapter, PointUtf16, ToPointUtf16, + range_from_lsp, Anchor, Bias, Buffer, CachedLspAdapter, ToPointUtf16, }; use lsp::{DocumentHighlightKind, LanguageServer, ServerCapabilities}; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; +use rope::point_utf16::PointUtf16; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; #[async_trait(?Send)] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 84f45070fd..de4d76ebc3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,4 +1,3 @@ -pub mod fs; mod ignore; mod lsp_command; pub mod search; @@ -25,9 +24,8 @@ use language::{ }, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, - File as _, Language, LanguageRegistry, LanguageServerName, LineEnding, LocalFile, - OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, - Transaction, + File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, + Operation, Patch, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, }; use lsp::{ DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer, LanguageString, @@ -37,6 +35,7 @@ use lsp_command::*; use parking_lot::Mutex; use postage::watch; use rand::prelude::*; +use rope::point_utf16::PointUtf16; use search::SearchQuery; use serde::Serialize; use settings::{FormatOnSave, Formatter, Settings}; @@ -1085,13 +1084,6 @@ impl Project { } } - for worktree in self.worktrees(cx).collect::>() { - worktree.update(cx, |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - worktree_share_tasks.push(worktree.share(project_id, cx)); - }); - } - for (server_id, status) in &self.language_server_statuses { self.client .send(proto::StartLanguageServer { @@ -1104,6 +1096,13 @@ impl Project { .log_err(); } + for worktree in self.worktrees(cx).collect::>() { + worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + worktree_share_tasks.push(worktree.share(project_id, cx)); + }); + } + self.client_subscriptions .push(self.client.add_model_for_remote_entity(project_id, cx)); self.metadata_changed(cx); @@ -4562,6 +4561,7 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } + this.shared_buffers.remove(&peer_id); cx.emit(Event::CollaboratorLeft(peer_id)); cx.notify(); @@ -6019,33 +6019,6 @@ impl> From<(WorktreeId, P)> for ProjectPath { } } -impl From for fs::CreateOptions { - fn from(options: lsp::CreateFileOptions) -> Self { - Self { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - } - } -} - -impl From for fs::RenameOptions { - fn from(options: lsp::RenameFileOptions) -> Self { - Self { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - } - } -} - -impl From for fs::RemoveOptions { - fn from(options: lsp::DeleteFileOptions) -> Self { - Self { - recursive: options.recursive.unwrap_or(false), - ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), - } - } -} - fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { proto::Symbol { language_server_name: symbol.language_server_name.0.to_string(), diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index ceb5d033a7..12da0a75db 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,12 +1,14 @@ use crate::{worktree::WorktreeHandle, Event, *}; -use fs::RealFs; +use fs::LineEnding; +use fs::{FakeFs, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe}; use language::{ tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, - LineEnding, OffsetRangeExt, Point, ToPoint, + OffsetRangeExt, ToPoint, }; use lsp::Url; +use rope::point::Point; use serde_json::json; use std::{cell::RefCell, os::unix, rc::Rc, task::Poll}; use unindent::Unindent as _; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 968c2d4bc7..0de647029d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1,14 +1,12 @@ -use super::{ - fs::{self, Fs}, - ignore::IgnoreStack, - DiagnosticSummary, -}; +use super::{ignore::IgnoreStack, DiagnosticSummary}; use crate::{copy_recursive, ProjectEntryId, RemoveOptions}; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, VecDeque}; +use fs::LineEnding; +use fs::{repository::GitRepository, Fs}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, @@ -17,7 +15,6 @@ use futures::{ Stream, StreamExt, }; use fuzzy::CharBag; -use git::repository::GitRepository; use git::{DOT_GIT, GITIGNORE}; use gpui::{ executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, @@ -25,13 +22,14 @@ use gpui::{ }; use language::{ proto::{deserialize_version, serialize_line_ending, serialize_version}, - Buffer, DiagnosticEntry, LineEnding, PointUtf16, Rope, + Buffer, DiagnosticEntry, Rope, }; use parking_lot::Mutex; use postage::{ prelude::{Sink as _, Stream as _}, watch, }; +use rope::point_utf16::PointUtf16; use smol::channel::{self, Sender}; use std::{ @@ -961,9 +959,20 @@ impl LocalWorktree { let (snapshots_tx, mut snapshots_rx) = watch::channel_with(self.snapshot()); let rpc = self.client.clone(); let worktree_id = cx.model_id() as u64; + + for (path, summary) in self.diagnostic_summaries.iter() { + if let Err(e) = rpc.send(proto::UpdateDiagnosticSummary { + project_id, + worktree_id, + summary: Some(summary.to_proto(&path.0)), + }) { + return Task::ready(Err(e)); + } + } + let maintain_remote_snapshot = cx.background().spawn({ let rpc = rpc; - let diagnostic_summaries = self.diagnostic_summaries.clone(); + async move { let mut prev_snapshot = match snapshots_rx.recv().await { Some(snapshot) => { @@ -996,14 +1005,6 @@ impl LocalWorktree { } }; - for (path, summary) in diagnostic_summaries.iter() { - rpc.send(proto::UpdateDiagnosticSummary { - project_id, - worktree_id, - summary: Some(summary.to_proto(&path.0)), - })?; - } - while let Some(snapshot) = snapshots_rx.recv().await { send_worktree_update( &rpc, @@ -2970,11 +2971,10 @@ async fn send_worktree_update(client: &Arc, update: proto::UpdateWorktre #[cfg(test)] mod tests { use super::*; - use crate::fs::FakeFs; use anyhow::Result; use client::test::FakeHttpClient; - use fs::RealFs; - use git::repository::FakeGitRepository; + use fs::repository::FakeGitRepository; + use fs::{FakeFs, RealFs}; use gpui::{executor::Deterministic, TestAppContext}; use rand::prelude::*; use serde_json::json; diff --git a/crates/rope/Cargo.toml b/crates/rope/Cargo.toml new file mode 100644 index 0000000000..0f754c1fb3 --- /dev/null +++ b/crates/rope/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "rope" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/rope.rs" + +[dependencies] +bromberg_sl2 = "0.6" +smallvec = { version = "1.6", features = ["union"] } +sum_tree = { path = "../sum_tree" } +arrayvec = "0.7.1" +log = { version = "0.4.16", features = ["kv_unstable_serde"] } + + +[dev-dependencies] +rand = "0.8.3" +util = { path = "../util", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/text/src/offset_utf16.rs b/crates/rope/src/offset_utf16.rs similarity index 100% rename from crates/text/src/offset_utf16.rs rename to crates/rope/src/offset_utf16.rs diff --git a/crates/text/src/point.rs b/crates/rope/src/point.rs similarity index 100% rename from crates/text/src/point.rs rename to crates/rope/src/point.rs diff --git a/crates/text/src/point_utf16.rs b/crates/rope/src/point_utf16.rs similarity index 100% rename from crates/text/src/point_utf16.rs rename to crates/rope/src/point_utf16.rs diff --git a/crates/text/src/rope.rs b/crates/rope/src/rope.rs similarity index 99% rename from crates/text/src/rope.rs rename to crates/rope/src/rope.rs index e148c048bb..39dc9dc049 100644 --- a/crates/text/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -1,7 +1,12 @@ -use super::Point; -use crate::{OffsetUtf16, PointUtf16}; +pub mod offset_utf16; +pub mod point; +pub mod point_utf16; + use arrayvec::ArrayString; use bromberg_sl2::{DigestString, HashMatrix}; +use offset_utf16::OffsetUtf16; +use point::Point; +use point_utf16::PointUtf16; use smallvec::SmallVec; use std::{cmp, fmt, io, mem, ops::Range, str}; use sum_tree::{Bias, Dimension, SumTree}; @@ -1073,9 +1078,9 @@ fn find_split_ix(text: &str) -> usize { #[cfg(test)] mod tests { use super::*; - use crate::random_char_iter::RandomCharIter; use rand::prelude::*; use std::{cmp::Ordering, env, io::Read}; + use util::RandomCharIter; use Bias::{Left, Right}; #[test] diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 5b1ed6c2af..4dbade4fec 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -113,7 +113,7 @@ impl Peer { } #[instrument(skip_all)] - pub async fn add_connection( + pub fn add_connection( self: &Arc, connection: Connection, create_timer: F, @@ -326,7 +326,7 @@ impl Peer { } #[cfg(any(test, feature = "test-support"))] - pub async fn add_test_connection( + pub fn add_test_connection( self: &Arc, connection: Connection, executor: Arc, @@ -337,7 +337,6 @@ impl Peer { ) { let executor = executor.clone(); self.add_connection(connection, move |duration| executor.timer(duration)) - .await } pub fn disconnect(&self, connection_id: ConnectionId) { @@ -522,21 +521,17 @@ mod tests { let (client1_to_server_conn, server_to_client_1_conn, _kill) = Connection::in_memory(cx.background()); - let (client1_conn_id, io_task1, client1_incoming) = client1 - .add_test_connection(client1_to_server_conn, cx.background()) - .await; - let (_, io_task2, server_incoming1) = server - .add_test_connection(server_to_client_1_conn, cx.background()) - .await; + let (client1_conn_id, io_task1, client1_incoming) = + client1.add_test_connection(client1_to_server_conn, cx.background()); + let (_, io_task2, server_incoming1) = + server.add_test_connection(server_to_client_1_conn, cx.background()); let (client2_to_server_conn, server_to_client_2_conn, _kill) = Connection::in_memory(cx.background()); - let (client2_conn_id, io_task3, client2_incoming) = client2 - .add_test_connection(client2_to_server_conn, cx.background()) - .await; - let (_, io_task4, server_incoming2) = server - .add_test_connection(server_to_client_2_conn, cx.background()) - .await; + let (client2_conn_id, io_task3, client2_incoming) = + client2.add_test_connection(client2_to_server_conn, cx.background()); + let (_, io_task4, server_incoming2) = + server.add_test_connection(server_to_client_2_conn, cx.background()); executor.spawn(io_task1).detach(); executor.spawn(io_task2).detach(); @@ -619,12 +614,10 @@ mod tests { let (client_to_server_conn, server_to_client_conn, _kill) = Connection::in_memory(cx.background()); - let (client_to_server_conn_id, io_task1, mut client_incoming) = client - .add_test_connection(client_to_server_conn, cx.background()) - .await; - let (server_to_client_conn_id, io_task2, mut server_incoming) = server - .add_test_connection(server_to_client_conn, cx.background()) - .await; + let (client_to_server_conn_id, io_task1, mut client_incoming) = + client.add_test_connection(client_to_server_conn, cx.background()); + let (server_to_client_conn_id, io_task2, mut server_incoming) = + server.add_test_connection(server_to_client_conn, cx.background()); executor.spawn(io_task1).detach(); executor.spawn(io_task2).detach(); @@ -719,12 +712,10 @@ mod tests { let (client_to_server_conn, server_to_client_conn, _kill) = Connection::in_memory(cx.background()); - let (client_to_server_conn_id, io_task1, mut client_incoming) = client - .add_test_connection(client_to_server_conn, cx.background()) - .await; - let (server_to_client_conn_id, io_task2, mut server_incoming) = server - .add_test_connection(server_to_client_conn, cx.background()) - .await; + let (client_to_server_conn_id, io_task1, mut client_incoming) = + client.add_test_connection(client_to_server_conn, cx.background()); + let (server_to_client_conn_id, io_task2, mut server_incoming) = + server.add_test_connection(server_to_client_conn, cx.background()); executor.spawn(io_task1).detach(); executor.spawn(io_task2).detach(); @@ -832,9 +823,8 @@ mod tests { let (client_conn, mut server_conn, _kill) = Connection::in_memory(cx.background()); let client = Peer::new(); - let (connection_id, io_handler, mut incoming) = client - .add_test_connection(client_conn, cx.background()) - .await; + let (connection_id, io_handler, mut incoming) = + client.add_test_connection(client_conn, cx.background()); let (io_ended_tx, io_ended_rx) = oneshot::channel(); executor @@ -868,9 +858,8 @@ mod tests { let (client_conn, mut server_conn, _kill) = Connection::in_memory(cx.background()); let client = Peer::new(); - let (connection_id, io_handler, mut incoming) = client - .add_test_connection(client_conn, cx.background()) - .await; + let (connection_id, io_handler, mut incoming) = + client.add_test_connection(client_conn, cx.background()); executor.spawn(io_handler).detach(); executor .spawn(async move { incoming.next().await }) diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 78440a2418..1cc73fabc4 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -14,12 +14,23 @@ test-support = [] assets = { path = "../assets" } collections = { path = "../collections" } gpui = { path = "../gpui" } +fs = { path = "../fs" } +anyhow = "1.0.38" +futures = "0.3" theme = { path = "../theme" } util = { path = "../util" } -anyhow = "1.0.38" +rope = { path = "../rope" } json_comments = "0.2" +postage = { version = "0.4.1", features = ["futures-traits"] } schemars = "0.8" -serde = { version = "1.0", features = ["derive", "rc"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde = { workspace = true } +serde_json = { workspace = true } serde_path_to_error = "0.1.4" toml = "0.5" +tree-sitter = "*" +tree-sitter-json = "*" + +[dev-dependencies] +unindent = "0.1" +gpui = { path = "../gpui", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index d661eb0f21..2e7dc08d16 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,4 +1,5 @@ mod keymap_file; +pub mod settings_file; use anyhow::Result; use gpui::{ @@ -12,8 +13,9 @@ use schemars::{ }; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; -use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc}; +use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc}; use theme::{Theme, ThemeRegistry}; +use tree_sitter::Query; use util::ResultExt as _; pub use keymap_file::{keymap_file_json_schema, KeymapFileContent}; @@ -501,6 +503,101 @@ pub fn settings_file_json_schema( serde_json::to_value(root_schema).unwrap() } +pub fn write_top_level_setting( + mut settings_content: String, + top_level_key: &str, + new_val: &str, +) -> String { + let mut parser = tree_sitter::Parser::new(); + parser.set_language(tree_sitter_json::language()).unwrap(); + let tree = parser.parse(&settings_content, None).unwrap(); + + let mut cursor = tree_sitter::QueryCursor::new(); + + let query = Query::new( + tree_sitter_json::language(), + " + (document + (object + (pair + key: (string) @key + value: (_) @value))) + ", + ) + .unwrap(); + + let mut first_key_start = None; + let mut existing_value_range = None; + let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes()); + for mat in matches { + if mat.captures.len() != 2 { + continue; + } + + let key = mat.captures[0]; + let value = mat.captures[1]; + + first_key_start.get_or_insert_with(|| key.node.start_byte()); + + if let Some(key_text) = settings_content.get(key.node.byte_range()) { + if key_text == format!("\"{top_level_key}\"") { + existing_value_range = Some(value.node.byte_range()); + break; + } + } + } + + match (first_key_start, existing_value_range) { + (None, None) => { + // No document, create a new object and overwrite + settings_content.clear(); + write!( + settings_content, + "{{\n \"{}\": \"{new_val}\"\n}}\n", + top_level_key + ) + .unwrap(); + } + + (_, Some(existing_value_range)) => { + // Existing theme key, overwrite + settings_content.replace_range(existing_value_range, &format!("\"{new_val}\"")); + } + + (Some(first_key_start), None) => { + // No existing theme key, but other settings. Prepend new theme settings and + // match style of first key + let mut row = 0; + let mut column = 0; + for (ix, char) in settings_content.char_indices() { + if ix == first_key_start { + break; + } + if char == '\n' { + row += 1; + column = 0; + } else { + column += char.len_utf8(); + } + } + + let content = format!(r#""{top_level_key}": "{new_val}","#); + settings_content.insert_str(first_key_start, &content); + + if row > 0 { + settings_content.insert_str( + first_key_start + content.len(), + &format!("\n{:width$}", ' ', width = column), + ) + } else { + settings_content.insert_str(first_key_start + content.len(), " ") + } + } + } + + settings_content +} + fn merge(target: &mut T, value: Option) { if let Some(value) = value { *target = value; @@ -512,3 +609,108 @@ pub fn parse_json_with_comments(content: &str) -> Result json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), )?) } + +#[cfg(test)] +mod tests { + use crate::write_top_level_setting; + use unindent::Unindent; + + #[test] + fn test_write_theme_into_settings_with_theme() { + let settings = r#" + { + "theme": "one-dark" + } + "# + .unindent(); + + let new_settings = r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(); + + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); + + assert_eq!(settings_after_theme, new_settings) + } + + #[test] + fn test_write_theme_into_empty_settings() { + let settings = r#" + { + } + "# + .unindent(); + + let new_settings = r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(); + + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); + + assert_eq!(settings_after_theme, new_settings) + } + + #[test] + fn test_write_theme_into_no_settings() { + let settings = "".to_string(); + + let new_settings = r#" + { + "theme": "summerfruit-light" + } + "# + .unindent(); + + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); + + assert_eq!(settings_after_theme, new_settings) + } + + #[test] + fn test_write_theme_into_single_line_settings_without_theme() { + let settings = r#"{ "a": "", "ok": true }"#.to_string(); + let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#; + + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); + + assert_eq!(settings_after_theme, new_settings) + } + + #[test] + fn test_write_theme_pre_object_whitespace() { + let settings = r#" { "a": "", "ok": true }"#.to_string(); + let new_settings = r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#; + + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); + + assert_eq!(settings_after_theme, new_settings) + } + + #[test] + fn test_write_theme_into_multi_line_settings_without_theme() { + let settings = r#" + { + "a": "b" + } + "# + .unindent(); + + let new_settings = r#" + { + "theme": "summerfruit-light", + "a": "b" + } + "# + .unindent(); + + let settings_after_theme = write_top_level_setting(settings, "theme", "summerfruit-light"); + + assert_eq!(settings_after_theme, new_settings) + } +} diff --git a/crates/zed/src/settings_file.rs b/crates/settings/src/settings_file.rs similarity index 83% rename from crates/zed/src/settings_file.rs rename to crates/settings/src/settings_file.rs index 14c9f63e95..6a7c96fd81 100644 --- a/crates/zed/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,14 +1,59 @@ +use fs::Fs; use futures::StreamExt; use gpui::{executor, MutableAppContext}; use postage::sink::Sink as _; use postage::{prelude::Stream, watch}; -use project::Fs; use serde::Deserialize; -use settings::{parse_json_with_comments, KeymapFileContent, Settings, SettingsFileContent}; + use std::{path::Path, sync::Arc, time::Duration}; use theme::ThemeRegistry; use util::ResultExt; +use crate::{ + parse_json_with_comments, write_top_level_setting, KeymapFileContent, Settings, + SettingsFileContent, +}; + +// TODO: Switch SettingsFile to open a worktree and buffer for synchronization +// And instant updates in the Zed editor +#[derive(Clone)] +pub struct SettingsFile { + path: &'static Path, + fs: Arc, +} + +impl SettingsFile { + pub fn new(path: &'static Path, fs: Arc) -> Self { + SettingsFile { path, fs } + } + + pub async fn rewrite_settings_file(&self, f: F) -> anyhow::Result<()> + where + F: Fn(String) -> String, + { + let content = self.fs.load(self.path).await?; + + let new_settings = f(content); + + self.fs + .atomic_write(self.path.to_path_buf(), new_settings) + .await?; + + Ok(()) + } +} + +pub fn write_setting(key: &'static str, val: String, cx: &mut MutableAppContext) { + let settings_file = cx.global::().clone(); + cx.background() + .spawn(async move { + settings_file + .rewrite_settings_file(|settings| write_top_level_setting(settings, key, &val)) + .await + }) + .detach_and_log_err(cx); +} + #[derive(Clone)] pub struct WatchedJsonFile(pub watch::Receiver); @@ -73,7 +118,7 @@ pub fn watch_settings_file( pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) { cx.clear_bindings(); - settings::KeymapFileContent::load_defaults(cx); + KeymapFileContent::load_defaults(cx); content.add_to_cx(cx).log_err(); } @@ -101,8 +146,8 @@ pub fn watch_keymap_file(mut file: WatchedJsonFile, cx: &mut #[cfg(test)] mod tests { use super::*; - use project::FakeFs; - use settings::{EditorSettings, SoftWrap}; + use crate::{EditorSettings, SoftWrap}; + use fs::FakeFs; #[gpui::test] async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) { diff --git a/crates/terminal/src/terminal_element.rs b/crates/terminal/src/terminal_element.rs index edf445a1d3..df745dae46 100644 --- a/crates/terminal/src/terminal_element.rs +++ b/crates/terminal/src/terminal_element.rs @@ -330,13 +330,10 @@ impl TerminalElement { } let mut properties = Properties::new(); - if indexed - .flags - .intersects(Flags::BOLD | Flags::BOLD_ITALIC | Flags::DIM_BOLD) - { + if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { properties = *properties.weight(Weight::BOLD); } - if indexed.flags.intersects(Flags::ITALIC | Flags::BOLD_ITALIC) { + if indexed.flags.intersects(Flags::ITALIC) { properties = *properties.style(Italic); } diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 4fc09eff46..ad960ec93e 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -13,23 +13,24 @@ test-support = ["rand"] [dependencies] clock = { path = "../clock" } collections = { path = "../collections" } +fs = { path = "../fs" } +rope = { path = "../rope" } sum_tree = { path = "../sum_tree" } anyhow = "1.0.38" -arrayvec = "0.7.1" digest = { version = "0.9", features = ["std"] } -bromberg_sl2 = "0.6" lazy_static = "1.4" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11" postage = { version = "0.4.1", features = ["futures-traits"] } rand = { version = "0.8.3", optional = true } -regex = "1.5" smallvec = { version = "1.6", features = ["union"] } +util = { path = "../util" } +regex = "1.5" + [dev-dependencies] collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } -util = { path = "../util", features = ["test-support"] } ctor = "0.1" env_logger = "0.9" rand = "0.8.3" diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 9f70ae1cc7..024c7e643b 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -1,9 +1,10 @@ -use super::{Point, ToOffset}; -use crate::{rope::TextDimension, BufferSnapshot, PointUtf16, ToPoint, ToPointUtf16}; use anyhow::Result; +use rope::{point::Point, point_utf16::PointUtf16, TextDimension}; use std::{cmp::Ordering, fmt::Debug, ops::Range}; use sum_tree::Bias; +use crate::{BufferSnapshot, ToOffset, ToPoint, ToPointUtf16}; + #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)] pub struct Anchor { pub timestamp: clock::Local, diff --git a/crates/text/src/random_char_iter.rs b/crates/text/src/random_char_iter.rs deleted file mode 100644 index 04cdcd3524..0000000000 --- a/crates/text/src/random_char_iter.rs +++ /dev/null @@ -1,36 +0,0 @@ -use rand::prelude::*; - -pub struct RandomCharIter(T); - -impl RandomCharIter { - pub fn new(rng: T) -> Self { - Self(rng) - } -} - -impl Iterator for RandomCharIter { - type Item = char; - - fn next(&mut self) -> Option { - if std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()) { - return if self.0.gen_range(0..100) < 5 { - Some('\n') - } else { - Some(self.0.gen_range(b'a'..b'z' + 1).into()) - }; - } - - match self.0.gen_range(0..100) { - // whitespace - 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.0).copied(), - // two-byte greek letters - 20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))), - // // three-byte characters - 33..=45 => ['✋', '✅', '❌', '❎', '⭐'].choose(&mut self.0).copied(), - // // four-byte characters - 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.0).copied(), - // ascii letters - _ => Some(self.0.gen_range(b'a'..b'z' + 1).into()), - } - } -} diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index e5acbd21bc..881fc8c432 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -1,5 +1,7 @@ -use crate::Anchor; -use crate::{rope::TextDimension, BufferSnapshot}; +use rope::TextDimension; + +use crate::{Anchor, BufferSnapshot}; + use std::cmp::Ordering; use std::ops::Range; diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index d201f87443..2196e870f2 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -2,14 +2,8 @@ mod anchor; pub mod locator; #[cfg(any(test, feature = "test-support"))] pub mod network; -mod offset_utf16; pub mod operation_queue; mod patch; -mod point; -mod point_utf16; -#[cfg(any(test, feature = "test-support"))] -pub mod random_char_iter; -pub mod rope; mod selection; pub mod subscription; #[cfg(test)] @@ -20,22 +14,15 @@ pub use anchor::*; use anyhow::Result; use clock::ReplicaId; use collections::{HashMap, HashSet}; -use lazy_static::lazy_static; +use fs::LineEnding; use locator::Locator; -pub use offset_utf16::*; use operation_queue::OperationQueue; pub use patch::Patch; -pub use point::*; -pub use point_utf16::*; use postage::{barrier, oneshot, prelude::*}; -#[cfg(any(test, feature = "test-support"))] -pub use random_char_iter::*; -use regex::Regex; -use rope::TextDimension; +use rope::{offset_utf16::OffsetUtf16, point::Point, point_utf16::PointUtf16, TextDimension}; pub use rope::{Chunks, Rope, TextSummary}; pub use selection::*; use std::{ - borrow::Cow, cmp::{self, Ordering, Reverse}, future::Future, iter::Iterator, @@ -49,9 +36,8 @@ pub use sum_tree::Bias; use sum_tree::{FilterCursor, SumTree, TreeMap}; use undo_map::UndoMap; -lazy_static! { - static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap(); -} +#[cfg(any(test, feature = "test-support"))] +use util::RandomCharIter; pub type TransactionId = clock::Local; @@ -96,12 +82,6 @@ pub struct Transaction { pub start: clock::Global, } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum LineEnding { - Unix, - Windows, -} - impl HistoryEntry { pub fn transaction_id(&self) -> TransactionId { self.transaction.id @@ -1464,9 +1444,7 @@ impl Buffer { last_end = Some(range.end); let new_text_len = rng.gen_range(0..10); - let new_text: String = crate::random_char_iter::RandomCharIter::new(&mut *rng) - .take(new_text_len) - .collect(); + let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); edits.push((range, new_text.into())); } @@ -2370,56 +2348,6 @@ impl operation_queue::Operation for Operation { } } -impl Default for LineEnding { - fn default() -> Self { - #[cfg(unix)] - return Self::Unix; - - #[cfg(not(unix))] - return Self::CRLF; - } -} - -impl LineEnding { - pub fn as_str(&self) -> &'static str { - match self { - LineEnding::Unix => "\n", - LineEnding::Windows => "\r\n", - } - } - - pub fn detect(text: &str) -> Self { - let mut max_ix = cmp::min(text.len(), 1000); - while !text.is_char_boundary(max_ix) { - max_ix -= 1; - } - - if let Some(ix) = text[..max_ix].find(&['\n']) { - if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { - Self::Windows - } else { - Self::Unix - } - } else { - Self::default() - } - } - - pub fn normalize(text: &mut String) { - if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") { - *text = replaced; - } - } - - fn normalize_arc(text: Arc) -> Arc { - if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") { - replaced.into() - } else { - text - } - } -} - pub trait ToOffset { fn to_offset(&self, snapshot: &BufferSnapshot) -> usize; } diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 804eff2c7a..59cb5fbc2c 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -19,3 +19,4 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } smol = "1.2.5" + diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 59b0bc7e6a..f3ca38b78b 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -107,7 +107,9 @@ impl ThemeSelector { fn show_selected_theme(&mut self, cx: &mut ViewContext) { if let Some(mat) = self.matches.get(self.selected_index) { match self.registry.get(&mat.string) { - Ok(theme) => Self::set_theme(theme, cx), + Ok(theme) => { + Self::set_theme(theme, cx); + } Err(error) => { log::error!("error loading theme {}: {}", mat.string, error) } @@ -151,6 +153,10 @@ impl PickerDelegate for ThemeSelector { fn confirm(&mut self, cx: &mut ViewContext) { self.selection_completed = true; + + let theme_name = cx.global::().theme.meta.name.clone(); + settings::settings_file::write_setting("theme", theme_name, cx); + cx.emit(Event::Dismissed); } diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 78416aa5b5..c083137156 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -7,21 +7,20 @@ edition = "2021" doctest = false [features] -test-support = ["rand", "serde_json", "tempdir", "git2"] +test-support = ["serde_json", "tempdir", "git2"] [dependencies] anyhow = "1.0.38" futures = "0.3" log = { version = "0.4.16", features = ["kv_unstable_serde"] } lazy_static = "1.4.0" -rand = { version = "0.8", optional = true } +rand = { workspace = true } tempdir = { version = "0.3.7", optional = true } serde_json = { version = "1.0", features = ["preserve_order"], optional = true } git2 = { version = "0.15", default-features = false, optional = true } [dev-dependencies] -rand = { version = "0.8" } tempdir = { version = "0.3.7" } serde_json = { version = "1.0", features = ["preserve_order"] } git2 = { version = "0.15", default-features = false } diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 97f409f410..e35f2df7d4 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -2,6 +2,7 @@ pub mod test; use futures::Future; +use rand::{seq::SliceRandom, Rng}; use std::{ cmp::Ordering, ops::AddAssign, @@ -155,6 +156,41 @@ pub fn defer(f: F) -> impl Drop { Defer(Some(f)) } +pub struct RandomCharIter(T); + +impl RandomCharIter { + pub fn new(rng: T) -> Self { + Self(rng) + } +} + +impl Iterator for RandomCharIter { + type Item = char; + + fn next(&mut self) -> Option { + if std::env::var("SIMPLE_TEXT").map_or(false, |v| !v.is_empty()) { + return if self.0.gen_range(0..100) < 5 { + Some('\n') + } else { + Some(self.0.gen_range(b'a'..b'z' + 1).into()) + }; + } + + match self.0.gen_range(0..100) { + // whitespace + 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.0).copied(), + // two-byte greek letters + 20..=32 => char::from_u32(self.0.gen_range(('α' as u32)..('ω' as u32 + 1))), + // // three-byte characters + 33..=45 => ['✋', '✅', '❌', '❎', '⭐'].choose(&mut self.0).copied(), + // // four-byte characters + 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.0).copied(), + // ascii letters + _ => Some(self.0.gen_range(b'a'..b'z' + 1).into()), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 44f2a8cb16..85c9636c69 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -27,6 +27,7 @@ command_palette = { path = "../command_palette" } editor = { path = "../editor" } gpui = { path = "../gpui" } language = { path = "../language" } +rope = { path = "../rope" } search = { path = "../search" } settings = { path = "../settings" } workspace = { path = "../workspace" } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 894b77e6e8..6741d8ac0b 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -15,7 +15,8 @@ use editor::{ display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, ClipboardSelection, DisplayPoint, }; use gpui::{actions, MutableAppContext, ViewContext}; -use language::{AutoindentMode, Point, SelectionGoal}; +use language::{AutoindentMode, SelectionGoal}; +use rope::point::Point; use workspace::Workspace; use self::{ diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index bb8ba26b74..bdac8fe158 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -51,7 +51,7 @@ impl<'a> NeovimBackedTestContext<'a> { pub async fn set_shared_state(&mut self, marked_text: &str) -> ContextHandle { let context_handle = self.set_state(marked_text, Mode::Normal); - let selection = self.editor(|editor, cx| editor.selections.newest::(cx)); + let selection = self.editor(|editor, cx| editor.selections.newest::(cx)); let text = self.buffer_text(); self.neovim.set_state(selection, &text).await; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index ff4e10cfe5..60ac345323 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -8,7 +8,10 @@ use async_compat::Compat; use async_trait::async_trait; #[cfg(feature = "neovim")] use gpui::keymap::Keystroke; -use language::{Point, Selection}; + +use language::Selection; +use rope::point::Point; + #[cfg(feature = "neovim")] use lazy_static::lazy_static; #[cfg(feature = "neovim")] diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 2fd43b7bcb..54e7eaf463 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -12,7 +12,9 @@ test-support = [ "call/test-support", "client/test-support", "project/test-support", - "settings/test-support" + "settings/test-support", + "gpui/test-support", + "fs/test-support" ] [dependencies] @@ -21,6 +23,7 @@ client = { path = "../client" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } drag_and_drop = { path = "../drag_and_drop" } +fs = { path = "../fs" } gpui = { path = "../gpui" } language = { path = "../language" } menu = { path = "../menu" } @@ -42,4 +45,5 @@ call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } -settings = { path = "../settings", features = ["test-support"] } \ No newline at end of file +settings = { path = "../settings", features = ["test-support"] } +fs = { path = "../fs", features = ["test-support"] } \ No newline at end of file diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b07eca465e..1f2847fd8f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -16,6 +16,7 @@ use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use drag_and_drop::DragAndDrop; +use fs::{self, Fs}; use futures::{channel::oneshot, FutureExt, StreamExt}; use gpui::{ actions, @@ -31,7 +32,7 @@ use log::{error, warn}; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; -use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; +use project::{Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; use searchable::SearchableItemHandle; use serde::Deserialize; use settings::{Autosave, DockAnchor, Settings}; @@ -929,7 +930,7 @@ impl AppState { let settings = Settings::test(cx); cx.set_global(settings); - let fs = project::FakeFs::new(cx.background().clone()); + let fs = fs::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::test()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone(), cx); @@ -1171,6 +1172,10 @@ impl Workspace { cx.notify(); } + pub fn titlebar_item(&self) -> Option { + self.titlebar_item.clone() + } + /// Call the given callback with a workspace whose project is local. /// /// If the given workspace has a local project, then it will be passed @@ -2811,8 +2816,9 @@ mod tests { use crate::sidebar::SidebarItem; use super::*; + use fs::FakeFs; use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext}; - use project::{FakeFs, Project, ProjectEntryId}; + use project::{Project, ProjectEntryId}; use serde_json::json; pub fn default_item_factory( diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d0b41b08f1..5bfeb681a6 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.59.0" +version = "0.60.0" [lib] name = "zed" @@ -32,6 +32,7 @@ diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } file_finder = { path = "../file_finder" } search = { path = "../search" } +fs = { path = "../fs" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { path = "../go_to_line" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index f48f8b723e..1c6a818ef3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -14,7 +14,6 @@ use client::{ http::{self, HttpClient}, UserStore, ZED_SECRET_CLIENT_TOKEN, }; -use fs::OpenOptions; use futures::{ channel::{mpsc, oneshot}, FutureExt, SinkExt, StreamExt, @@ -26,20 +25,21 @@ use log::LevelFilter; use parking_lot::Mutex; use project::{Fs, ProjectStore}; use serde_json::json; -use settings::{self, KeymapFileContent, Settings, SettingsFileContent, WorkingDirectory}; +use settings::{ + self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent, + WorkingDirectory, +}; use smol::process::Command; -use std::{env, ffi::OsStr, fs, panic, path::PathBuf, sync::Arc, thread, time::Duration}; +use std::fs::OpenOptions; +use std::{env, ffi::OsStr, panic, path::PathBuf, sync::Arc, thread, time::Duration}; use terminal::terminal_container_view::{get_working_directory, TerminalContainer}; +use fs::RealFs; +use settings::settings_file::{watch_keymap_file, watch_settings_file, WatchedJsonFile}; use theme::ThemeRegistry; use util::{ResultExt, TryFutureExt}; use workspace::{self, AppState, ItemHandle, NewFile, OpenPaths, Workspace}; -use zed::{ - self, build_window_options, - fs::RealFs, - initialize_workspace, languages, menus, - settings_file::{watch_keymap_file, watch_settings_file, WatchedJsonFile}, -}; +use zed::{self, build_window_options, initialize_workspace, languages, menus}; fn main() { let http = http::client(); @@ -65,6 +65,7 @@ fn main() { let themes = ThemeRegistry::new(Assets, app.font_cache()); let default_settings = Settings::defaults(Assets, &app.font_cache(), &themes); + let settings_file = SettingsFile::new(&*zed::paths::SETTINGS, fs.clone()); let config_files = load_config_files(&app, fs.clone()); let login_shell_env_loaded = if stdout_is_a_pty() { @@ -97,10 +98,11 @@ fn main() { .spawn(languages::init(languages.clone(), cx.background().clone())); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - let (settings_file, keymap_file) = cx.background().block(config_files).unwrap(); + let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap(); //Setup settings global before binding actions - watch_settings_file(default_settings, settings_file, themes.clone(), cx); + cx.set_global(settings_file); + watch_settings_file(default_settings, settings_file_content, themes.clone(), cx); watch_keymap_file(keymap_file, cx); context_menu::init(cx); @@ -200,23 +202,23 @@ fn main() { } fn init_paths() { - fs::create_dir_all(&*zed::paths::CONFIG_DIR).expect("could not create config path"); - fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path"); - fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path"); - fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path"); + std::fs::create_dir_all(&*zed::paths::CONFIG_DIR).expect("could not create config path"); + std::fs::create_dir_all(&*zed::paths::LANGUAGES_DIR).expect("could not create languages path"); + std::fs::create_dir_all(&*zed::paths::DB_DIR).expect("could not create database path"); + std::fs::create_dir_all(&*zed::paths::LOGS_DIR).expect("could not create logs path"); // Copy setting files from legacy locations. TODO: remove this after a few releases. thread::spawn(|| { - if fs::metadata(&*zed::paths::legacy::SETTINGS).is_ok() - && fs::metadata(&*zed::paths::SETTINGS).is_err() + if std::fs::metadata(&*zed::paths::legacy::SETTINGS).is_ok() + && std::fs::metadata(&*zed::paths::SETTINGS).is_err() { - fs::copy(&*zed::paths::legacy::SETTINGS, &*zed::paths::SETTINGS).log_err(); + std::fs::copy(&*zed::paths::legacy::SETTINGS, &*zed::paths::SETTINGS).log_err(); } - if fs::metadata(&*zed::paths::legacy::KEYMAP).is_ok() - && fs::metadata(&*zed::paths::KEYMAP).is_err() + if std::fs::metadata(&*zed::paths::legacy::KEYMAP).is_ok() + && std::fs::metadata(&*zed::paths::KEYMAP).is_err() { - fs::copy(&*zed::paths::legacy::KEYMAP, &*zed::paths::KEYMAP).log_err(); + std::fs::copy(&*zed::paths::legacy::KEYMAP, &*zed::paths::KEYMAP).log_err(); } }); } @@ -231,9 +233,10 @@ fn init_logger() { const KIB: u64 = 1024; const MIB: u64 = 1024 * KIB; const MAX_LOG_BYTES: u64 = MIB; - if fs::metadata(&*zed::paths::LOG).map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) + if std::fs::metadata(&*zed::paths::LOG) + .map_or(false, |metadata| metadata.len() > MAX_LOG_BYTES) { - let _ = fs::rename(&*zed::paths::LOG, &*zed::paths::OLD_LOG); + let _ = std::fs::rename(&*zed::paths::LOG, &*zed::paths::OLD_LOG); } let log_file = OpenOptions::new() @@ -289,7 +292,7 @@ fn init_panic_hook(app_version: String, http: Arc, background: A .body(body.into())?; let response = http.send(request).await.context("error sending panic")?; if response.status().is_success() { - fs::remove_file(child_path) + std::fs::remove_file(child_path) .context("error removing panic after sending it successfully") .log_err(); } else { @@ -338,7 +341,7 @@ fn init_panic_hook(app_version: String, http: Arc, background: A }; let panic_filename = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); - fs::write( + std::fs::write( zed::paths::LOGS_DIR.join(format!("zed-{}-{}.panic", app_version, panic_filename)), &message, ) @@ -395,7 +398,7 @@ fn stdout_is_a_pty() -> bool { fn collect_path_args() -> Vec { env::args() .skip(1) - .filter_map(|arg| match fs::canonicalize(arg) { + .filter_map(|arg| match std::fs::canonicalize(arg) { Ok(path) => Some(path), Err(error) => { log::error!("error parsing path argument: {}", error); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index f6f3a34242..ef0c84909a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2,7 +2,6 @@ mod feedback; pub mod languages; pub mod menus; pub mod paths; -pub mod settings_file; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -10,10 +9,11 @@ use anyhow::{anyhow, Context, Result}; use assets::Assets; use breadcrumbs::Breadcrumbs; pub use client; -use collab_ui::CollabTitlebarItem; +use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu}; use collections::VecDeque; pub use editor; use editor::{Editor, MultiBuffer}; + use gpui::{ actions, geometry::vector::vec2f, @@ -23,7 +23,7 @@ use gpui::{ }; use language::Rope; pub use lsp; -pub use project::{self, fs}; +pub use project; use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; @@ -94,6 +94,22 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.toggle_full_screen(); }, ); + cx.add_action( + |workspace: &mut Workspace, + _: &ToggleCollaborationMenu, + cx: &mut ViewContext| { + if let Some(item) = workspace + .titlebar_item() + .and_then(|item| item.downcast::()) + { + cx.as_mut().defer(move |cx| { + item.update(cx, |item, cx| { + item.toggle_contacts_popover(&Default::default(), cx); + }); + }); + } + }, + ); cx.add_global_action(quit); cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {