Merge branch 'main' into fix-dock-focus-issues

Kay Simmons created

Change summary

Cargo.lock                                        |  73 +++++
Cargo.toml                                        |   6 
Dockerfile                                        |   2 
Dockerfile.migrator                               |   2 
assets/keymaps/default.json                       |   1 
crates/client/src/client.rs                       | 135 ++++++++--
crates/client/src/test.rs                         |  12 
crates/collab/Cargo.toml                          |   3 
crates/collab/src/integration_tests.rs            | 146 ++++++++---
crates/collab/src/rpc.rs                          |  18 +
crates/collab/src/rpc/store.rs                    |   2 
crates/collab_ui/src/collab_titlebar_item.rs      |  17 
crates/collab_ui/src/collab_ui.rs                 |   2 
crates/diagnostics/Cargo.toml                     |   1 
crates/diagnostics/src/diagnostics.rs             |   7 
crates/editor/Cargo.toml                          |   3 
crates/editor/src/display_map.rs                  |   9 
crates/editor/src/display_map/block_map.rs        |   7 
crates/editor/src/display_map/fold_map.rs         |   9 
crates/editor/src/display_map/tab_map.rs          |  22 +
crates/editor/src/display_map/wrap_map.rs         |  12 
crates/editor/src/editor.rs                       |   7 
crates/editor/src/editor_tests.rs                 |   2 
crates/editor/src/element.rs                      |  53 ++--
crates/editor/src/items.rs                        |   3 
crates/editor/src/movement.rs                     |   5 
crates/editor/src/multi_buffer.rs                 |  16 
crates/editor/src/multi_buffer/anchor.rs          |   2 
crates/editor/src/selections_collection.rs        |   3 
crates/fs/Cargo.toml                              |  31 ++
crates/fs/src/fs.rs                               | 127 ++++++++++
crates/fs/src/repository.rs                       |   0 
crates/git/Cargo.toml                             |   3 
crates/git/src/diff.rs                            |   3 
crates/git/src/git.rs                             |   1 
crates/go_to_line/Cargo.toml                      |   1 
crates/go_to_line/src/go_to_line.rs               |   3 
crates/language/Cargo.toml                        |   2 
crates/language/src/buffer.rs                     |  12 
crates/language/src/buffer_tests.rs               |   4 
crates/language/src/diagnostic_set.rs             |   3 
crates/language/src/language.rs                   |   1 
crates/language/src/proto.rs                      |  14 
crates/language/src/syntax_map.rs                 |   5 
crates/project/Cargo.toml                         |   4 
crates/project/src/lsp_command.rs                 |   3 
crates/project/src/project.rs                     |  49 ---
crates/project/src/project_tests.rs               |   6 
crates/project/src/worktree.rs                    |  38 +-
crates/rope/Cargo.toml                            |  20 +
crates/rope/src/offset_utf16.rs                   |   0 
crates/rope/src/point.rs                          |   0 
crates/rope/src/point_utf16.rs                    |   0 
crates/rope/src/rope.rs                           |  11 
crates/rpc/src/peer.rs                            |  55 +--
crates/settings/Cargo.toml                        |  17 +
crates/settings/src/settings.rs                   | 204 ++++++++++++++++
crates/settings/src/settings_file.rs              |  55 ++++
crates/terminal/src/terminal_element.rs           |   7 
crates/text/Cargo.toml                            |   9 
crates/text/src/anchor.rs                         |   5 
crates/text/src/random_char_iter.rs               |  36 ---
crates/text/src/selection.rs                      |   6 
crates/text/src/text.rs                           |  82 ------
crates/theme_selector/Cargo.toml                  |   1 
crates/theme_selector/src/theme_selector.rs       |   8 
crates/util/Cargo.toml                            |   5 
crates/util/src/lib.rs                            |  36 +++
crates/vim/Cargo.toml                             |   1 
crates/vim/src/normal.rs                          |   3 
crates/vim/src/test/neovim_backed_test_context.rs |   2 
crates/vim/src/test/neovim_connection.rs          |   5 
crates/workspace/Cargo.toml                       |   8 
crates/workspace/src/workspace.rs                 |  12 
crates/zed/Cargo.toml                             |   3 
crates/zed/src/main.rs                            |  55 ++--
crates/zed/src/zed.rs                             |  22 +
77 files changed, 1,095 insertions(+), 463 deletions(-)

Detailed changes

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",

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
+

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 . .
 

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 \

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"
         }
     },

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<Self>, conn: Connection, cx: &AsyncAppContext) {
+    fn set_connection(self: &Arc<Self>, 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<Deterministic>, 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,

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 {

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"

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::<lsp::notification::DidOpenTextDocument>()
         .await;
+    fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
+        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::notification::PublishDiagnostics>(
         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::<Vec<_>>(),
-            &[(
-                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::notification::PublishDiagnostics>(
         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,

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(())
     }
 

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

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<Self>) {
+    pub fn toggle_contacts_popover(
+        &mut self,
+        _: &ToggleCollaborationMenu,
+        cx: &mut ViewContext<Self>,
+    ) {
         match self.contacts_popover.take() {
             Some(_) => {}
             None => {
@@ -197,7 +198,7 @@ impl CollabTitlebarItem {
         };
         Stack::new()
             .with_child(
-                MouseEventHandler::<ToggleContactsPopover>::new(0, cx, |state, _| {
+                MouseEventHandler::<ToggleCollaborationMenu>::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(),

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;

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" }

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;

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 }

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::<String>();
+                let text = util::RandomCharIter::new(&mut rng)
+                    .take(len)
+                    .collect::<String>();
                 MultiBuffer::build_simple(&text, cx)
             } else {
                 MultiBuffer::build_random(&mut rng, cx)

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() {

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]

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<TabSnapshot>);
 
@@ -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<super::Point> for TabPoint {
-    fn from(point: super::Point) -> Self {
+impl From<Point> for TabPoint {
+    fn from(point: Point) -> Self {
         Self(point)
     }
 }
@@ -362,7 +363,7 @@ pub type TabEdit = text::Edit<TabPoint>;
 
 #[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::<String>();
+            let text = util::RandomCharIter::new(&mut rng)
+                .take(len)
+                .collect::<String>();
             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,

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<Transform> {
 
 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::<String>();
+                let text = util::RandomCharIter::new(&mut rng)
+                    .take(len)
+                    .collect::<String>();
                 MultiBuffer::build_simple(&text, cx)
             }
         });

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);

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},

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 mut thumb_top = top + scrollbar_start * height;
-        let mut thumb_bottom = top + scrollbar_end * height;
-        let thumb_center = (thumb_top + thumb_bottom) / 2.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;
-            }
+        let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
+
+        let mut height = bounds.height();
+        let mut first_row_y_offset = 0.0;
+
+        // 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| {

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},

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]

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<Self>,
     ) {
-        use text::RandomCharIter;
+        use util::RandomCharIter;
 
         let snapshot = self.read(cx);
         let mut edits: Vec<(Range<usize>, Arc<str>)> = 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::<String>();
+                        let base_text = util::RandomCharIter::new(&mut rng)
+                            .take(10)
+                            .collect::<String>();
                         buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx)));
                         buffers.last().unwrap()
                     } else {

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 {

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::{

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 = []

crates/project/src/fs.rs → 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<str>) -> Arc<str> {
+        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<Box<dyn io::Read>>;
     async fn load(&self, path: &Path) -> Result<String>;
+    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<PathBuf>;
     async fn is_file(&self, path: &Path) -> bool;
@@ -86,6 +154,33 @@ pub struct Metadata {
     pub is_dir: bool,
 }
 
+impl From<lsp::CreateFileOptions> 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<lsp::RenameFileOptions> 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<lsp::DeleteFileOptions> 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<String, Arc<Mutex<FakeFsEntry>>>,
-        git_repo_state: Option<Arc<SyncMutex<git::repository::FakeGitRepositoryState>>>,
+        git_repo_state: Option<Arc<SyncMutex<repository::FakeGitRepositoryState>>>,
     },
     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
             }

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"

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};

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");

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"] }

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]);

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" }

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));
         }

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]

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 {

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::{

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,
     }
 }
 

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;
 

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"] }

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)]

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::<Vec<_>>() {
-                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::<Vec<_>>() {
+                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<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
     }
 }
 
-impl From<lsp::CreateFileOptions> 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<lsp::RenameFileOptions> 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<lsp::DeleteFileOptions> 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(),

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 _;

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<Client>, 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;

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"]  }

crates/text/src/rope.rs → 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]

crates/rpc/src/peer.rs 🔗

@@ -113,7 +113,7 @@ impl Peer {
     }
 
     #[instrument(skip_all)]
-    pub async fn add_connection<F, Fut, Out>(
+    pub fn add_connection<F, Fut, Out>(
         self: &Arc<Self>,
         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<Self>,
         connection: Connection,
         executor: Arc<gpui::executor::Background>,
@@ -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 })

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"] }

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<T: Copy>(target: &mut T, value: Option<T>) {
     if let Some(value) = value {
         *target = value;
@@ -512,3 +609,108 @@ pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T>
         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)
+    }
+}

crates/zed/src/settings_file.rs → 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<dyn Fs>,
+}
+
+impl SettingsFile {
+    pub fn new(path: &'static Path, fs: Arc<dyn Fs>) -> Self {
+        SettingsFile { path, fs }
+    }
+
+    pub async fn rewrite_settings_file<F>(&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::<SettingsFile>().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<T>(pub watch::Receiver<T>);
 
@@ -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<KeymapFileContent>, 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) {

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);
         }
 

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"

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,

crates/text/src/random_char_iter.rs 🔗

@@ -1,36 +0,0 @@
-use rand::prelude::*;
-
-pub struct RandomCharIter<T: Rng>(T);
-
-impl<T: Rng> RandomCharIter<T> {
-    pub fn new(rng: T) -> Self {
-        Self(rng)
-    }
-}
-
-impl<T: Rng> Iterator for RandomCharIter<T> {
-    type Item = char;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        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()),
-        }
-    }
-}

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;
 

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<str>) -> Arc<str> {
-        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;
 }

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"
+

crates/theme_selector/src/theme_selector.rs 🔗

@@ -107,7 +107,9 @@ impl ThemeSelector {
     fn show_selected_theme(&mut self, cx: &mut ViewContext<Self>) {
         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>) {
         self.selection_completed = true;
+
+        let theme_name = cx.global::<Settings>().theme.meta.name.clone();
+        settings::settings_file::write_setting("theme", theme_name, cx);
+
         cx.emit(Event::Dismissed);
     }
 

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 }

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: FnOnce()>(f: F) -> impl Drop {
     Defer(Some(f))
 }
 
+pub struct RandomCharIter<T: Rng>(T);
+
+impl<T: Rng> RandomCharIter<T> {
+    pub fn new(rng: T) -> Self {
+        Self(rng)
+    }
+}
+
+impl<T: Rng> Iterator for RandomCharIter<T> {
+    type Item = char;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        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::*;

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" }

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::{

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::<language::Point>(cx));
+        let selection = self.editor(|editor, cx| editor.selections.newest::<rope::point::Point>(cx));
         let text = self.buffer_text();
         self.neovim.set_state(selection, &text).await;
 

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")]

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"] }
+settings = { path = "../settings", features = ["test-support"] }
+fs = { path = "../fs", features = ["test-support"] }

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<AnyViewHandle> {
+        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(

crates/zed/Cargo.toml 🔗

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
 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" }

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<dyn HttpClient>, 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<dyn HttpClient>, 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<PathBuf> {
     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);

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<AppState>, cx: &mut gpui::MutableAppContext) {
             cx.toggle_full_screen();
         },
     );
+    cx.add_action(
+        |workspace: &mut Workspace,
+         _: &ToggleCollaborationMenu,
+         cx: &mut ViewContext<Workspace>| {
+            if let Some(item) = workspace
+                .titlebar_item()
+                .and_then(|item| item.downcast::<CollabTitlebarItem>())
+            {
+                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| {