Merge remote-tracking branch 'origin/main' into assistant-2

Antonio Scandurra created

Change summary

.github/pull_request_template.md                                        |   7 
Cargo.lock                                                              |   5 
assets/keymaps/default.json                                             |  39 
crates/collab/Cargo.toml                                                |   2 
crates/collab/migrations.sqlite/20221109000000_test_schema.sql          |  10 
crates/collab/migrations/20230529164700_add_worktree_settings_files.sql |  10 
crates/collab/src/db.rs                                                 | 101 
crates/collab/src/db/worktree_settings_file.rs                          |  19 
crates/collab/src/rpc.rs                                                |  50 
crates/collab/src/tests/integration_tests.rs                            | 129 
crates/copilot/src/copilot.rs                                           |  11 
crates/copilot_button/src/copilot_button.rs                             |  49 
crates/editor/src/display_map.rs                                        |   7 
crates/editor/src/editor.rs                                             |  22 
crates/editor/src/items.rs                                              |   4 
crates/editor/src/multi_buffer.rs                                       |  20 
crates/feedback/Cargo.toml                                              |   1 
crates/feedback/src/feedback_editor.rs                                  |  12 
crates/feedback/src/feedback_info_text.rs                               |   4 
crates/gpui/src/elements/uniform_list.rs                                |   8 
crates/gpui/src/font_cache.rs                                           |  52 
crates/gpui/src/fonts.rs                                                |  11 
crates/gpui/src/platform.rs                                             |   1 
crates/gpui/src/platform/mac/fonts.rs                                   |   8 
crates/language/src/buffer.rs                                           |  11 
crates/language/src/language_settings.rs                                |  23 
crates/project/src/lsp_command.rs                                       |   3 
crates/project/src/project.rs                                           | 242 
crates/project/src/project_tests.rs                                     |  60 
crates/project/src/worktree.rs                                          |  38 
crates/rpc/proto/zed.proto                                              |   9 
crates/rpc/src/proto.rs                                                 |   2 
crates/rpc/src/rpc.rs                                                   |   2 
crates/settings/src/settings_file.rs                                    |  26 
crates/settings/src/settings_store.rs                                   | 271 
crates/terminal_view/src/terminal_panel.rs                              |  22 
crates/terminal_view/src/terminal_view.rs                               |  15 
crates/theme/src/theme.rs                                               |   3 
crates/util/src/paths.rs                                                |   1 
crates/welcome/src/welcome.rs                                           |   2 
crates/workspace/src/dock.rs                                            |  69 
crates/workspace/src/notifications.rs                                   | 197 
crates/workspace/src/pane.rs                                            |  34 
crates/workspace/src/workspace.rs                                       | 567 
crates/zed/Cargo.toml                                                   |   2 
crates/zed/src/languages/json.rs                                        |   5 
crates/zed/src/languages/yaml.rs                                        |   7 
crates/zed/src/main.rs                                                  |   7 
crates/zed/src/menus.rs                                                 |  15 
crates/zed/src/zed.rs                                                   |   2 
styles/src/buildLicenses.ts                                             |  78 
styles/src/buildThemes.ts                                               |   2 
styles/src/colorSchemes.ts                                              |  99 
styles/src/styleTree/tabBar.ts                                          |   3 
styles/src/styleTree/workspace.ts                                       |  15 
styles/src/themes/andromeda/LICENSE                                     |  21 
styles/src/themes/andromeda/andromeda.ts                                |  10 
styles/src/themes/atelier/LICENSE                                       |  21 
styles/src/themes/atelier/atelier-cave-dark.ts                          |   6 
styles/src/themes/atelier/atelier-cave-light.ts                         |   6 
styles/src/themes/atelier/atelier-dune-dark.ts                          |   6 
styles/src/themes/atelier/atelier-dune-light.ts                         |   6 
styles/src/themes/atelier/atelier-estuary-dark.ts                       |   6 
styles/src/themes/atelier/atelier-estuary-light.ts                      |   6 
styles/src/themes/atelier/atelier-forest-dark.ts                        |   6 
styles/src/themes/atelier/atelier-forest-light.ts                       |   6 
styles/src/themes/atelier/atelier-heath-dark.ts                         |   6 
styles/src/themes/atelier/atelier-heath-light.ts                        |   6 
styles/src/themes/atelier/atelier-lakeside-dark.ts                      |   6 
styles/src/themes/atelier/atelier-lakeside-light.ts                     |   6 
styles/src/themes/atelier/atelier-plateau-dark.ts                       |   6 
styles/src/themes/atelier/atelier-plateau-light.ts                      |   6 
styles/src/themes/atelier/atelier-savanna-dark.ts                       |   6 
styles/src/themes/atelier/atelier-savanna-light.ts                      |   6 
styles/src/themes/atelier/atelier-seaside-dark.ts                       |   6 
styles/src/themes/atelier/atelier-seaside-light.ts                      |   6 
styles/src/themes/atelier/atelier-sulphurpool-dark.ts                   |   6 
styles/src/themes/atelier/atelier-sulphurpool-light.ts                  |   6 
styles/src/themes/atelier/common.ts                                     |   7 
styles/src/themes/ayu/LICENSE                                           |  21 
styles/src/themes/ayu/ayu-dark.ts                                       |   4 
styles/src/themes/ayu/ayu-light.ts                                      |   4 
styles/src/themes/ayu/ayu-mirage.ts                                     |   4 
styles/src/themes/ayu/common.ts                                         |  12 
styles/src/themes/common/colorScheme.ts                                 |  12 
styles/src/themes/gruvbox/LICENSE                                       |  21 
styles/src/themes/gruvbox/gruvbox-common.ts                             |   5 
styles/src/themes/gruvbox/gruvbox-dark-hard.ts                          |   0 
styles/src/themes/gruvbox/gruvbox-dark-soft.ts                          |   0 
styles/src/themes/gruvbox/gruvbox-dark.ts                               |   0 
styles/src/themes/gruvbox/gruvbox-light-hard.ts                         |   0 
styles/src/themes/gruvbox/gruvbox-light-soft.ts                         |   0 
styles/src/themes/gruvbox/gruvbox-light.ts                              |   0 
styles/src/themes/one/LICENSE                                           |  21 
styles/src/themes/one/one-dark.ts                                       |  12 
styles/src/themes/one/one-light.ts                                      |  12 
styles/src/themes/rose-pine/LICENSE                                     |  21 
styles/src/themes/rose-pine/rose-pine-dawn.ts                           |  10 
styles/src/themes/rose-pine/rose-pine-moon.ts                           |  10 
styles/src/themes/rose-pine/rose-pine.ts                                |  10 
styles/src/themes/sandcastle/LICENSE                                    |  21 
styles/src/themes/sandcastle/sandcastle.ts                              |  10 
styles/src/themes/solarized/LICENSE                                     |  21 
styles/src/themes/solarized/solarized.ts                                |  10 
styles/src/themes/summercamp/LICENSE                                    |  21 
styles/src/themes/summercamp/summercamp.ts                              |  10 
106 files changed, 2,024 insertions(+), 841 deletions(-)

Detailed changes

.github/pull_request_template.md πŸ”—

@@ -2,12 +2,11 @@
 
 Release Notes:
 
-Use `N/A` in this section if this item should be skipped in the release notes.
+- N/A
 
-Add release note lines here:
+or
 
-* (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
-* ...
+- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
 
 If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
 These will be removed by the person making the release.

Cargo.lock πŸ”—

@@ -1251,7 +1251,7 @@ dependencies = [
 
 [[package]]
 name = "collab"
-version = "0.12.4"
+version = "0.12.5"
 dependencies = [
  "anyhow",
  "async-tungstenite",
@@ -2241,6 +2241,7 @@ dependencies = [
  "log",
  "postage",
  "project",
+ "regex",
  "search",
  "serde",
  "serde_derive",
@@ -8781,7 +8782,7 @@ dependencies = [
 
 [[package]]
 name = "zed"
-version = "0.89.0"
+version = "0.90.0"
 dependencies = [
  "activity_indicator",
  "ai",

assets/keymaps/default.json πŸ”—

@@ -375,42 +375,9 @@
         "workspace::ActivatePane",
         8
       ],
-      "cmd-b": [
-        "workspace::ToggleLeftDock",
-        {
-          "focus": true
-        }
-      ],
-      "cmd-shift-b": [
-        "workspace::ToggleLeftDock",
-        {
-          "focus": false
-        }
-      ],
-      "cmd-r": [
-        "workspace::ToggleRightDock",
-        {
-          "focus": true
-        }
-      ],
-      "cmd-shift-r": [
-        "workspace::ToggleRightDock",
-        {
-          "focus": false
-        }
-      ],
-      "cmd-j": [
-        "workspace::ToggleBottomDock",
-        {
-          "focus": true
-        }
-      ],
-      "cmd-shift-j": [
-        "workspace::ToggleBottomDock",
-        {
-          "focus": false
-        }
-      ],
+      "cmd-b": "workspace::ToggleLeftDock",
+      "cmd-r": "workspace::ToggleRightDock",
+      "cmd-j": "workspace::ToggleBottomDock",
       "cmd-shift-f": "workspace::NewSearch",
       "cmd-k cmd-t": "theme_selector::Toggle",
       "cmd-k cmd-s": "zed::OpenKeymap",

crates/collab/Cargo.toml πŸ”—

@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
 default-run = "collab"
 edition = "2021"
 name = "collab"
-version = "0.12.4"
+version = "0.12.5"
 publish = false
 
 [[bin]]

crates/collab/migrations.sqlite/20221109000000_test_schema.sql πŸ”—

@@ -112,6 +112,16 @@ CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_rep
 CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
 CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
 
+CREATE TABLE "worktree_settings_files" (
+    "project_id" INTEGER NOT NULL,
+    "worktree_id" INTEGER NOT NULL,
+    "path" VARCHAR NOT NULL,
+    "content" TEXT,
+    PRIMARY KEY(project_id, worktree_id, path),
+    FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
+);
+CREATE INDEX "index_worktree_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
+CREATE INDEX "index_worktree_settings_files_on_project_id_and_worktree_id" ON "worktree_settings_files" ("project_id", "worktree_id");
 
 CREATE TABLE "worktree_diagnostic_summaries" (
     "project_id" INTEGER NOT NULL,

crates/collab/migrations/20230529164700_add_worktree_settings_files.sql πŸ”—

@@ -0,0 +1,10 @@
+CREATE TABLE "worktree_settings_files" (
+    "project_id" INTEGER NOT NULL,
+    "worktree_id" INT8 NOT NULL,
+    "path" VARCHAR NOT NULL,
+    "content" TEXT NOT NULL,
+    PRIMARY KEY(project_id, worktree_id, path),
+    FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
+);
+CREATE INDEX "index_settings_files_on_project_id" ON "worktree_settings_files" ("project_id");
+CREATE INDEX "index_settings_files_on_project_id_and_wt_id" ON "worktree_settings_files" ("project_id", "worktree_id");

crates/collab/src/db.rs πŸ”—

@@ -16,6 +16,7 @@ mod worktree_diagnostic_summary;
 mod worktree_entry;
 mod worktree_repository;
 mod worktree_repository_statuses;
+mod worktree_settings_file;
 
 use crate::executor::Executor;
 use crate::{Error, Result};
@@ -1494,6 +1495,7 @@ impl Database {
                         updated_repositories: Default::default(),
                         removed_repositories: Default::default(),
                         diagnostic_summaries: Default::default(),
+                        settings_files: Default::default(),
                         scan_id: db_worktree.scan_id as u64,
                         completed_scan_id: db_worktree.completed_scan_id as u64,
                     };
@@ -1638,6 +1640,25 @@ impl Database {
                     })
                     .collect::<Vec<_>>();
 
+                {
+                    let mut db_settings_files = worktree_settings_file::Entity::find()
+                        .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
+                        .stream(&*tx)
+                        .await?;
+                    while let Some(db_settings_file) = db_settings_files.next().await {
+                        let db_settings_file = db_settings_file?;
+                        if let Some(worktree) = worktrees
+                            .iter_mut()
+                            .find(|w| w.id == db_settings_file.worktree_id as u64)
+                        {
+                            worktree.settings_files.push(WorktreeSettingsFile {
+                                path: db_settings_file.path,
+                                content: db_settings_file.content,
+                            });
+                        }
+                    }
+                }
+
                 let mut collaborators = project
                     .find_related(project_collaborator::Entity)
                     .all(&*tx)
@@ -2637,6 +2658,58 @@ impl Database {
         .await
     }
 
+    pub async fn update_worktree_settings(
+        &self,
+        update: &proto::UpdateWorktreeSettings,
+        connection: ConnectionId,
+    ) -> Result<RoomGuard<Vec<ConnectionId>>> {
+        let project_id = ProjectId::from_proto(update.project_id);
+        let room_id = self.room_id_for_project(project_id).await?;
+        self.room_transaction(room_id, |tx| async move {
+            // Ensure the update comes from the host.
+            let project = project::Entity::find_by_id(project_id)
+                .one(&*tx)
+                .await?
+                .ok_or_else(|| anyhow!("no such project"))?;
+            if project.host_connection()? != connection {
+                return Err(anyhow!("can't update a project hosted by someone else"))?;
+            }
+
+            if let Some(content) = &update.content {
+                worktree_settings_file::Entity::insert(worktree_settings_file::ActiveModel {
+                    project_id: ActiveValue::Set(project_id),
+                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
+                    path: ActiveValue::Set(update.path.clone()),
+                    content: ActiveValue::Set(content.clone()),
+                })
+                .on_conflict(
+                    OnConflict::columns([
+                        worktree_settings_file::Column::ProjectId,
+                        worktree_settings_file::Column::WorktreeId,
+                        worktree_settings_file::Column::Path,
+                    ])
+                    .update_column(worktree_settings_file::Column::Content)
+                    .to_owned(),
+                )
+                .exec(&*tx)
+                .await?;
+            } else {
+                worktree_settings_file::Entity::delete(worktree_settings_file::ActiveModel {
+                    project_id: ActiveValue::Set(project_id),
+                    worktree_id: ActiveValue::Set(update.worktree_id as i64),
+                    path: ActiveValue::Set(update.path.clone()),
+                    ..Default::default()
+                })
+                .exec(&*tx)
+                .await?;
+            }
+
+            let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
+            Ok(connection_ids)
+        })
+        .await
+    }
+
     pub async fn join_project(
         &self,
         project_id: ProjectId,
@@ -2707,6 +2780,7 @@ impl Database {
                             entries: Default::default(),
                             repository_entries: Default::default(),
                             diagnostic_summaries: Default::default(),
+                            settings_files: Default::default(),
                             scan_id: db_worktree.scan_id as u64,
                             completed_scan_id: db_worktree.completed_scan_id as u64,
                         },
@@ -2819,6 +2893,25 @@ impl Database {
                 }
             }
 
+            // Populate worktree settings files
+            {
+                let mut db_settings_files = worktree_settings_file::Entity::find()
+                    .filter(worktree_settings_file::Column::ProjectId.eq(project_id))
+                    .stream(&*tx)
+                    .await?;
+                while let Some(db_settings_file) = db_settings_files.next().await {
+                    let db_settings_file = db_settings_file?;
+                    if let Some(worktree) =
+                        worktrees.get_mut(&(db_settings_file.worktree_id as u64))
+                    {
+                        worktree.settings_files.push(WorktreeSettingsFile {
+                            path: db_settings_file.path,
+                            content: db_settings_file.content,
+                        });
+                    }
+                }
+            }
+
             // Populate language servers.
             let language_servers = project
                 .find_related(language_server::Entity)
@@ -3482,6 +3575,7 @@ pub struct RejoinedWorktree {
     pub updated_repositories: Vec<proto::RepositoryEntry>,
     pub removed_repositories: Vec<u64>,
     pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
+    pub settings_files: Vec<WorktreeSettingsFile>,
     pub scan_id: u64,
     pub completed_scan_id: u64,
 }
@@ -3537,10 +3631,17 @@ pub struct Worktree {
     pub entries: Vec<proto::Entry>,
     pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
     pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
+    pub settings_files: Vec<WorktreeSettingsFile>,
     pub scan_id: u64,
     pub completed_scan_id: u64,
 }
 
+#[derive(Debug)]
+pub struct WorktreeSettingsFile {
+    pub path: String,
+    pub content: String,
+}
+
 #[cfg(test)]
 pub use test::*;
 

crates/collab/src/db/worktree_settings_file.rs πŸ”—

@@ -0,0 +1,19 @@
+use super::ProjectId;
+use sea_orm::entity::prelude::*;
+
+#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
+#[sea_orm(table_name = "worktree_settings_files")]
+pub struct Model {
+    #[sea_orm(primary_key)]
+    pub project_id: ProjectId,
+    #[sea_orm(primary_key)]
+    pub worktree_id: i64,
+    #[sea_orm(primary_key)]
+    pub path: String,
+    pub content: String,
+}
+
+#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
+pub enum Relation {}
+
+impl ActiveModelBehavior for ActiveModel {}

crates/collab/src/rpc.rs πŸ”—

@@ -200,6 +200,7 @@ impl Server {
             .add_message_handler(start_language_server)
             .add_message_handler(update_language_server)
             .add_message_handler(update_diagnostic_summary)
+            .add_message_handler(update_worktree_settings)
             .add_request_handler(forward_project_request::<proto::GetHover>)
             .add_request_handler(forward_project_request::<proto::GetDefinition>)
             .add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
@@ -1088,6 +1089,18 @@ async fn rejoin_room(
                         },
                     )?;
                 }
+
+                for settings_file in worktree.settings_files {
+                    session.peer.send(
+                        session.connection_id,
+                        proto::UpdateWorktreeSettings {
+                            project_id: project.id.to_proto(),
+                            worktree_id: worktree.id,
+                            path: settings_file.path,
+                            content: Some(settings_file.content),
+                        },
+                    )?;
+                }
             }
 
             for language_server in &project.language_servers {
@@ -1410,6 +1423,18 @@ async fn join_project(
                 },
             )?;
         }
+
+        for settings_file in dbg!(worktree.settings_files) {
+            session.peer.send(
+                session.connection_id,
+                proto::UpdateWorktreeSettings {
+                    project_id: project_id.to_proto(),
+                    worktree_id: worktree.id,
+                    path: settings_file.path,
+                    content: Some(settings_file.content),
+                },
+            )?;
+        }
     }
 
     for language_server in &project.language_servers {
@@ -1525,6 +1550,31 @@ async fn update_diagnostic_summary(
     Ok(())
 }
 
+async fn update_worktree_settings(
+    message: proto::UpdateWorktreeSettings,
+    session: Session,
+) -> Result<()> {
+    dbg!(&message);
+
+    let guest_connection_ids = session
+        .db()
+        .await
+        .update_worktree_settings(&message, session.connection_id)
+        .await?;
+
+    broadcast(
+        Some(session.connection_id),
+        guest_connection_ids.iter().copied(),
+        |connection_id| {
+            session
+                .peer
+                .forward_send(session.connection_id, connection_id, message.clone())
+        },
+    );
+
+    Ok(())
+}
+
 async fn start_language_server(
     request: proto::StartLanguageServer,
     session: Session,

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -3114,6 +3114,135 @@ async fn test_fs_operations(
     });
 }
 
+#[gpui::test(iterations = 10)]
+async fn test_local_settings(
+    deterministic: Arc<Deterministic>,
+    cx_a: &mut TestAppContext,
+    cx_b: &mut TestAppContext,
+) {
+    deterministic.forbid_parking();
+    let mut server = TestServer::start(&deterministic).await;
+    let client_a = server.create_client(cx_a, "user_a").await;
+    let client_b = server.create_client(cx_b, "user_b").await;
+    server
+        .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+        .await;
+    let active_call_a = cx_a.read(ActiveCall::global);
+
+    // As client A, open a project that contains some local settings files
+    client_a
+        .fs
+        .insert_tree(
+            "/dir",
+            json!({
+                ".zed": {
+                    "settings.json": r#"{ "tab_size": 2 }"#
+                },
+                "a": {
+                    ".zed": {
+                        "settings.json": r#"{ "tab_size": 8 }"#
+                    },
+                    "a.txt": "a-contents",
+                },
+                "b": {
+                    "b.txt": "b-contents",
+                }
+            }),
+        )
+        .await;
+    let (project_a, _) = client_a.build_local_project("/dir", cx_a).await;
+    let project_id = active_call_a
+        .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+        .await
+        .unwrap();
+
+    // As client B, join that project and observe the local settings.
+    let project_b = client_b.build_remote_project(project_id, cx_b).await;
+    let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
+    deterministic.run_until_parked();
+    cx_b.read(|cx| {
+        let store = cx.global::<SettingsStore>();
+        assert_eq!(
+            store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
+            &[
+                (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
+                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
+            ]
+        )
+    });
+
+    // As client A, update a settings file. As Client B, see the changed settings.
+    client_a
+        .fs
+        .insert_file("/dir/.zed/settings.json", r#"{}"#.into())
+        .await;
+    deterministic.run_until_parked();
+    cx_b.read(|cx| {
+        let store = cx.global::<SettingsStore>();
+        assert_eq!(
+            store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
+            &[
+                (Path::new("").into(), r#"{}"#.to_string()),
+                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
+            ]
+        )
+    });
+
+    // As client A, create and remove some settings files. As client B, see the changed settings.
+    client_a
+        .fs
+        .remove_file("/dir/.zed/settings.json".as_ref(), Default::default())
+        .await
+        .unwrap();
+    client_a
+        .fs
+        .create_dir("/dir/b/.zed".as_ref())
+        .await
+        .unwrap();
+    client_a
+        .fs
+        .insert_file("/dir/b/.zed/settings.json", r#"{"tab_size": 4}"#.into())
+        .await;
+    deterministic.run_until_parked();
+    cx_b.read(|cx| {
+        let store = cx.global::<SettingsStore>();
+        assert_eq!(
+            store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
+            &[
+                (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
+                (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
+            ]
+        )
+    });
+
+    // As client B, disconnect.
+    server.forbid_connections();
+    server.disconnect_client(client_b.peer_id().unwrap());
+
+    // As client A, change and remove settings files while client B is disconnected.
+    client_a
+        .fs
+        .insert_file("/dir/a/.zed/settings.json", r#"{"hard_tabs":true}"#.into())
+        .await;
+    client_a
+        .fs
+        .remove_file("/dir/b/.zed/settings.json".as_ref(), Default::default())
+        .await
+        .unwrap();
+    deterministic.run_until_parked();
+
+    // As client B, reconnect and see the changed settings.
+    server.allow_connections();
+    deterministic.advance_clock(RECEIVE_TIMEOUT);
+    cx_b.read(|cx| {
+        let store = cx.global::<SettingsStore>();
+        assert_eq!(
+            store.local_settings(worktree_b.id()).collect::<Vec<_>>(),
+            &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
+        )
+    });
+}
+
 #[gpui::test(iterations = 10)]
 async fn test_buffer_conflict_after_save(
     deterministic: Arc<Deterministic>,

crates/copilot/src/copilot.rs πŸ”—

@@ -318,7 +318,7 @@ impl Copilot {
     fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext<Copilot>) {
         let http = self.http.clone();
         let node_runtime = self.node_runtime.clone();
-        if all_language_settings(cx).copilot_enabled(None, None) {
+        if all_language_settings(None, cx).copilot_enabled(None, None) {
             if matches!(self.server, CopilotServer::Disabled) {
                 let start_task = cx
                     .spawn({
@@ -785,10 +785,7 @@ impl Copilot {
         let buffer = buffer.read(cx);
         let uri = registered_buffer.uri.clone();
         let position = position.to_point_utf16(buffer);
-        let settings = language_settings(
-            buffer.language_at(position).map(|l| l.name()).as_deref(),
-            cx,
-        );
+        let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx);
         let tab_size = settings.tab_size;
         let hard_tabs = settings.hard_tabs;
         let relative_path = buffer
@@ -1175,6 +1172,10 @@ mod tests {
         fn to_proto(&self) -> rpc::proto::File {
             unimplemented!()
         }
+
+        fn worktree_id(&self) -> usize {
+            0
+        }
     }
 
     impl language::LocalFile for File {

crates/copilot_button/src/copilot_button.rs πŸ”—

@@ -9,7 +9,10 @@ use gpui::{
     AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
     ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
-use language::language_settings::{self, all_language_settings, AllLanguageSettings};
+use language::{
+    language_settings::{self, all_language_settings, AllLanguageSettings},
+    File, Language,
+};
 use settings::{update_settings_file, SettingsStore};
 use std::{path::Path, sync::Arc};
 use util::{paths, ResultExt};
@@ -26,8 +29,8 @@ pub struct CopilotButton {
     popup_menu: ViewHandle<ContextMenu>,
     editor_subscription: Option<(Subscription, usize)>,
     editor_enabled: Option<bool>,
-    language: Option<Arc<str>>,
-    path: Option<Arc<Path>>,
+    language: Option<Arc<Language>>,
+    file: Option<Arc<dyn File>>,
     fs: Arc<dyn Fs>,
 }
 
@@ -41,7 +44,7 @@ impl View for CopilotButton {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let all_language_settings = &all_language_settings(cx);
+        let all_language_settings = all_language_settings(None, cx);
         if !all_language_settings.copilot.feature_enabled {
             return Empty::new().into_any();
         }
@@ -165,7 +168,7 @@ impl CopilotButton {
             editor_subscription: None,
             editor_enabled: None,
             language: None,
-            path: None,
+            file: None,
             fs,
         }
     }
@@ -197,14 +200,13 @@ impl CopilotButton {
 
         if let Some(language) = self.language.clone() {
             let fs = fs.clone();
-            let language_enabled =
-                language_settings::language_settings(Some(language.as_ref()), cx)
-                    .show_copilot_suggestions;
+            let language_enabled = language_settings::language_settings(Some(&language), None, cx)
+                .show_copilot_suggestions;
             menu_options.push(ContextMenuItem::handler(
                 format!(
                     "{} Suggestions for {}",
                     if language_enabled { "Hide" } else { "Show" },
-                    language
+                    language.name()
                 ),
                 move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
             ));
@@ -212,9 +214,9 @@ impl CopilotButton {
 
         let settings = settings::get::<AllLanguageSettings>(cx);
 
-        if let Some(path) = self.path.as_ref() {
-            let path_enabled = settings.copilot_enabled_for_path(path);
-            let path = path.clone();
+        if let Some(file) = &self.file {
+            let path = file.path().clone();
+            let path_enabled = settings.copilot_enabled_for_path(&path);
             menu_options.push(ContextMenuItem::handler(
                 format!(
                     "{} Suggestions for This Path",
@@ -276,17 +278,15 @@ impl CopilotButton {
         let editor = editor.read(cx);
         let snapshot = editor.buffer().read(cx).snapshot(cx);
         let suggestion_anchor = editor.selections.newest_anchor().start;
-        let language_name = snapshot
-            .language_at(suggestion_anchor)
-            .map(|language| language.name());
-        let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
+        let language = snapshot.language_at(suggestion_anchor);
+        let file = snapshot.file_at(suggestion_anchor).cloned();
 
         self.editor_enabled = Some(
-            all_language_settings(cx)
-                .copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
+            all_language_settings(self.file.as_ref(), cx)
+                .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
         );
-        self.language = language_name;
-        self.path = path.cloned();
+        self.language = language.cloned();
+        self.file = file;
 
         cx.notify()
     }
@@ -363,17 +363,18 @@ async fn configure_disabled_globs(
 }
 
 fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
-    let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
+    let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
     update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
         file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
     });
 }
 
-fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
-    let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
+fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
+    let show_copilot_suggestions =
+        all_language_settings(None, cx).copilot_enabled(Some(&language), None);
     update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
         file.languages
-            .entry(language)
+            .entry(language.name())
             .or_default()
             .show_copilot_suggestions = Some(!show_copilot_suggestions);
     });

crates/editor/src/display_map.rs πŸ”—

@@ -272,12 +272,11 @@ impl DisplayMap {
     }
 
     fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
-        let language_name = buffer
+        let language = buffer
             .read(cx)
             .as_singleton()
-            .and_then(|buffer| buffer.read(cx).language())
-            .map(|language| language.name());
-        language_settings(language_name.as_deref(), cx).tab_size
+            .and_then(|buffer| buffer.read(cx).language());
+        language_settings(language.as_deref(), None, cx).tab_size
     }
 
     #[cfg(test)]

crates/editor/src/editor.rs πŸ”—

@@ -31,7 +31,9 @@ use copilot::Copilot;
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use editor_settings::EditorSettings;
-pub use element::*;
+pub use element::{
+    Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
+};
 use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
@@ -3214,12 +3216,10 @@ impl Editor {
         snapshot: &MultiBufferSnapshot,
         cx: &mut ViewContext<Self>,
     ) -> bool {
-        let path = snapshot.file_at(location).map(|file| file.path().as_ref());
-        let language_name = snapshot
-            .language_at(location)
-            .map(|language| language.name());
-        let settings = all_language_settings(cx);
-        settings.copilot_enabled(language_name.as_deref(), path)
+        let file = snapshot.file_at(location);
+        let language = snapshot.language_at(location);
+        let settings = all_language_settings(file, cx);
+        settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
     }
 
     fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
@@ -7102,11 +7102,13 @@ impl Editor {
         };
 
         // If None, we are in a file without an extension
-        let file_extension = file_extension.or(self
+        let file = self
             .buffer
             .read(cx)
             .as_singleton()
-            .and_then(|b| b.read(cx).file())
+            .and_then(|b| b.read(cx).file());
+        let file_extension = file_extension.or(file
+            .as_ref()
             .and_then(|file| Path::new(file.file_name(cx)).extension())
             .and_then(|e| e.to_str())
             .map(|a| a.to_string()));
@@ -7117,7 +7119,7 @@ impl Editor {
             .get("vim_mode")
             == Some(&serde_json::Value::Bool(true));
         let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
-        let copilot_enabled = all_language_settings(cx).copilot_enabled(None, None);
+        let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
         let copilot_enabled_for_language = self
             .buffer
             .read(cx)

crates/editor/src/items.rs πŸ”—

@@ -1231,6 +1231,10 @@ mod tests {
             unimplemented!()
         }
 
+        fn worktree_id(&self) -> usize {
+            0
+        }
+
         fn is_deleted(&self) -> bool {
             unimplemented!()
         }

crates/editor/src/multi_buffer.rs πŸ”—

@@ -1377,8 +1377,14 @@ impl MultiBuffer {
         point: T,
         cx: &'a AppContext,
     ) -> &'a LanguageSettings {
-        let language = self.language_at(point, cx);
-        language_settings(language.map(|l| l.name()).as_deref(), cx)
+        let mut language = None;
+        let mut file = None;
+        if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) {
+            let buffer = buffer.read(cx);
+            language = buffer.language_at(offset);
+            file = buffer.file();
+        }
+        language_settings(language.as_ref(), file, cx)
     }
 
     pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
@@ -2785,9 +2791,13 @@ impl MultiBufferSnapshot {
         point: T,
         cx: &'a AppContext,
     ) -> &'a LanguageSettings {
-        self.point_to_buffer_offset(point)
-            .map(|(buffer, offset)| buffer.settings_at(offset, cx))
-            .unwrap_or_else(|| language_settings(None, cx))
+        let mut language = None;
+        let mut file = None;
+        if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
+            language = buffer.language_at(offset);
+            file = buffer.file();
+        }
+        language_settings(language, file, cx)
     }
 
     pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {

crates/feedback/Cargo.toml πŸ”—

@@ -16,6 +16,7 @@ editor = { path = "../editor" }
 language = { path = "../language" }
 gpui = { path = "../gpui" }
 project = { path = "../project" }
+regex.workspace = true
 search = { path = "../search" }
 settings = { path = "../settings" }
 theme = { path = "../theme" }

crates/feedback/src/feedback_editor.rs πŸ”—

@@ -14,6 +14,7 @@ use isahc::Request;
 use language::Buffer;
 use postage::prelude::Stream;
 use project::Project;
+use regex::Regex;
 use serde::Serialize;
 use smallvec::SmallVec;
 use std::{
@@ -46,6 +47,7 @@ pub fn init(cx: &mut AppContext) {
 #[derive(Serialize)]
 struct FeedbackRequestBody<'a> {
     feedback_text: &'a str,
+    email: Option<String>,
     metrics_id: Option<Arc<str>>,
     installation_id: Option<Arc<str>>,
     system_specs: SystemSpecs,
@@ -157,8 +159,18 @@ impl FeedbackEditor {
         let is_staff = telemetry.is_staff();
         let http_client = zed_client.http_client();
 
+        let re = Regex::new(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b").unwrap();
+
+        let emails: Vec<&str> = re
+            .captures_iter(feedback_text)
+            .map(|capture| capture.get(0).unwrap().as_str())
+            .collect();
+
+        let email = emails.first().map(|e| e.to_string());
+
         let request = FeedbackRequestBody {
             feedback_text: &feedback_text,
+            email,
             metrics_id,
             installation_id,
             system_specs,

crates/feedback/src/feedback_info_text.rs πŸ”—

@@ -34,7 +34,7 @@ impl View for FeedbackInfoText {
         Flex::row()
             .with_child(
                 Text::new(
-                    "We read whatever you submit here. For issues and discussions, visit the ",
+                    "Share your feedback. Include your email for replies. For issues and discussions, visit the ",
                     theme.feedback.info_text_default.text.clone(),
                 )
                 .with_soft_wrap(false)
@@ -60,7 +60,7 @@ impl View for FeedbackInfoText {
                 }),
             )
             .with_child(
-                Text::new(" on GitHub.", theme.feedback.info_text_default.text.clone())
+                Text::new(".", theme.feedback.info_text_default.text.clone())
                     .with_soft_wrap(false)
                     .aligned(),
             )

crates/gpui/src/elements/uniform_list.rs πŸ”—

@@ -36,7 +36,7 @@ struct StateInner {
     scroll_to: Option<ScrollTarget>,
 }
 
-pub struct LayoutState<V: View> {
+pub struct UniformListLayoutState<V: View> {
     scroll_max: f32,
     item_height: f32,
     items: Vec<AnyElement<V>>,
@@ -152,7 +152,7 @@ impl<V: View> UniformList<V> {
 }
 
 impl<V: View> Element<V> for UniformList<V> {
-    type LayoutState = LayoutState<V>;
+    type LayoutState = UniformListLayoutState<V>;
     type PaintState = ();
 
     fn layout(
@@ -169,7 +169,7 @@ impl<V: View> Element<V> for UniformList<V> {
 
         let no_items = (
             constraint.min,
-            LayoutState {
+            UniformListLayoutState {
                 item_height: 0.,
                 scroll_max: 0.,
                 items: Default::default(),
@@ -263,7 +263,7 @@ impl<V: View> Element<V> for UniformList<V> {
 
         (
             size,
-            LayoutState {
+            UniformListLayoutState {
                 item_height,
                 scroll_max,
                 items,

crates/gpui/src/font_cache.rs πŸ”—

@@ -25,8 +25,9 @@ struct Family {
 pub struct FontCache(RwLock<FontCacheState>);
 
 pub struct FontCacheState {
-    fonts: Arc<dyn platform::FontSystem>,
+    font_system: Arc<dyn platform::FontSystem>,
     families: Vec<Family>,
+    default_family: Option<FamilyId>,
     font_selections: HashMap<FamilyId, HashMap<Properties, FontId>>,
     metrics: HashMap<FontId, Metrics>,
     wrapper_pool: HashMap<(FontId, OrderedFloat<f32>), Vec<LineWrapper>>,
@@ -42,8 +43,9 @@ unsafe impl Send for FontCache {}
 impl FontCache {
     pub fn new(fonts: Arc<dyn platform::FontSystem>) -> Self {
         Self(RwLock::new(FontCacheState {
-            fonts,
+            font_system: fonts,
             families: Default::default(),
+            default_family: None,
             font_selections: Default::default(),
             metrics: Default::default(),
             wrapper_pool: Default::default(),
@@ -73,14 +75,14 @@ impl FontCache {
 
             let mut state = RwLockUpgradableReadGuard::upgrade(state);
 
-            if let Ok(font_ids) = state.fonts.load_family(name, features) {
+            if let Ok(font_ids) = state.font_system.load_family(name, features) {
                 if font_ids.is_empty() {
                     continue;
                 }
 
                 let family_id = FamilyId(state.families.len());
                 for font_id in &font_ids {
-                    if state.fonts.glyph_for_char(*font_id, 'm').is_none() {
+                    if state.font_system.glyph_for_char(*font_id, 'm').is_none() {
                         return Err(anyhow!("font must contain a glyph for the 'm' character"));
                     }
                 }
@@ -99,6 +101,31 @@ impl FontCache {
         ))
     }
 
+    /// Returns an arbitrary font family that is available on the system.
+    pub fn known_existing_family(&self) -> FamilyId {
+        if let Some(family_id) = self.0.read().default_family {
+            return family_id;
+        }
+
+        let default_family = self
+            .load_family(
+                &["Courier", "Helvetica", "Arial", "Verdana"],
+                &Default::default(),
+            )
+            .unwrap_or_else(|_| {
+                let all_family_names = self.0.read().font_system.all_families();
+                let all_family_names: Vec<_> = all_family_names
+                    .iter()
+                    .map(|string| string.as_str())
+                    .collect();
+                self.load_family(&all_family_names, &Default::default())
+                    .expect("could not load any default font family")
+            });
+
+        self.0.write().default_family = Some(default_family);
+        default_family
+    }
+
     pub fn default_font(&self, family_id: FamilyId) -> FontId {
         self.select_font(family_id, &Properties::default()).unwrap()
     }
@@ -115,7 +142,7 @@ impl FontCache {
             let mut inner = RwLockUpgradableReadGuard::upgrade(inner);
             let family = &inner.families[family_id.0];
             let font_id = inner
-                .fonts
+                .font_system
                 .select_font(&family.font_ids, properties)
                 .unwrap_or(family.font_ids[0]);
 
@@ -137,7 +164,7 @@ impl FontCache {
         if let Some(metrics) = state.metrics.get(&font_id) {
             f(metrics)
         } else {
-            let metrics = state.fonts.font_metrics(font_id);
+            let metrics = state.font_system.font_metrics(font_id);
             let metric = f(&metrics);
             let mut state = RwLockUpgradableReadGuard::upgrade(state);
             state.metrics.insert(font_id, metrics);
@@ -157,8 +184,11 @@ impl FontCache {
         let bounds;
         {
             let state = self.0.read();
-            glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
-            bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
+            glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
+            bounds = state
+                .font_system
+                .typographic_bounds(font_id, glyph_id)
+                .unwrap();
         }
         bounds.width() * self.em_scale(font_id, font_size)
     }
@@ -168,8 +198,8 @@ impl FontCache {
         let advance;
         {
             let state = self.0.read();
-            glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
-            advance = state.fonts.advance(font_id, glyph_id).unwrap();
+            glyph_id = state.font_system.glyph_for_char(font_id, 'm').unwrap();
+            advance = state.font_system.advance(font_id, glyph_id).unwrap();
         }
         advance.x() * self.em_scale(font_id, font_size)
     }
@@ -214,7 +244,7 @@ impl FontCache {
             .or_default();
         let wrapper = wrappers
             .pop()
-            .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.fonts.clone()));
+            .unwrap_or_else(|| LineWrapper::new(font_id, font_size, state.font_system.clone()));
         LineWrapperHandle {
             wrapper: Some(wrapper),
             font_cache: self.clone(),

crates/gpui/src/fonts.rs πŸ”—

@@ -295,13 +295,14 @@ impl Default for TextStyle {
                 .as_ref()
                 .expect("TextStyle::default can only be called within a call to with_font_cache");
 
-            let font_family_name = Arc::from("Courier");
-            let font_family_id = font_cache
-                .load_family(&[&font_family_name], &Default::default())
-                .unwrap();
+            let font_family_id = font_cache.known_existing_family();
             let font_id = font_cache
                 .select_font(font_family_id, &Default::default())
-                .unwrap();
+                .expect("did not have any font in system-provided family");
+            let font_family_name = font_cache
+                .family_name(font_family_id)
+                .expect("we loaded this family from the font cache, so this should work");
+
             Self {
                 color: Default::default(),
                 font_family_name,

crates/gpui/src/platform.rs πŸ”—

@@ -343,6 +343,7 @@ pub enum RasterizationOptions {
 
 pub trait FontSystem: Send + Sync {
     fn add_fonts(&self, fonts: &[Arc<Vec<u8>>]) -> anyhow::Result<()>;
+    fn all_families(&self) -> Vec<String>;
     fn load_family(&self, name: &str, features: &FontFeatures) -> anyhow::Result<Vec<FontId>>;
     fn select_font(
         &self,

crates/gpui/src/platform/mac/fonts.rs πŸ”—

@@ -66,6 +66,14 @@ impl platform::FontSystem for FontSystem {
         self.0.write().add_fonts(fonts)
     }
 
+    fn all_families(&self) -> Vec<String> {
+        self.0
+            .read()
+            .system_source
+            .all_families()
+            .expect("core text should never return an error")
+    }
+
     fn load_family(&self, name: &str, features: &Features) -> anyhow::Result<Vec<FontId>> {
         self.0.write().load_family(name, features)
     }

crates/language/src/buffer.rs πŸ”—

@@ -216,6 +216,11 @@ pub trait File: Send + Sync {
     /// of its worktree, then this method will return the name of the worktree itself.
     fn file_name<'a>(&'a self, cx: &'a AppContext) -> &'a OsStr;
 
+    /// Returns the id of the worktree to which this file belongs.
+    ///
+    /// This is needed for looking up project-specific settings.
+    fn worktree_id(&self) -> usize;
+
     fn is_deleted(&self) -> bool;
 
     fn as_any(&self) -> &dyn Any;
@@ -1802,8 +1807,7 @@ impl BufferSnapshot {
     }
 
     pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
-        let language_name = self.language_at(position).map(|language| language.name());
-        let settings = language_settings(language_name.as_deref(), cx);
+        let settings = language_settings(self.language_at(position), self.file(), cx);
         if settings.hard_tabs {
             IndentSize::tab()
         } else {
@@ -2127,8 +2131,7 @@ impl BufferSnapshot {
         position: D,
         cx: &'a AppContext,
     ) -> &'a LanguageSettings {
-        let language = self.language_at(position);
-        language_settings(language.map(|l| l.name()).as_deref(), cx)
+        language_settings(self.language_at(position), self.file.as_ref(), cx)
     }
 
     pub fn language_scope_at<D: ToOffset>(&self, position: D) -> Option<LanguageScope> {

crates/language/src/language_settings.rs πŸ”—

@@ -1,3 +1,4 @@
+use crate::{File, Language};
 use anyhow::Result;
 use collections::HashMap;
 use globset::GlobMatcher;
@@ -13,12 +14,21 @@ pub fn init(cx: &mut AppContext) {
     settings::register::<AllLanguageSettings>(cx);
 }
 
-pub fn language_settings<'a>(language: Option<&str>, cx: &'a AppContext) -> &'a LanguageSettings {
-    settings::get::<AllLanguageSettings>(cx).language(language)
+pub fn language_settings<'a>(
+    language: Option<&Arc<Language>>,
+    file: Option<&Arc<dyn File>>,
+    cx: &'a AppContext,
+) -> &'a LanguageSettings {
+    let language_name = language.map(|l| l.name());
+    all_language_settings(file, cx).language(language_name.as_deref())
 }
 
-pub fn all_language_settings<'a>(cx: &'a AppContext) -> &'a AllLanguageSettings {
-    settings::get::<AllLanguageSettings>(cx)
+pub fn all_language_settings<'a>(
+    file: Option<&Arc<dyn File>>,
+    cx: &'a AppContext,
+) -> &'a AllLanguageSettings {
+    let location = file.map(|f| (f.worktree_id(), f.path().as_ref()));
+    settings::get_local(location, cx)
 }
 
 #[derive(Debug, Clone)]
@@ -155,7 +165,7 @@ impl AllLanguageSettings {
             .any(|glob| glob.is_match(path))
     }
 
-    pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool {
+    pub fn copilot_enabled(&self, language: Option<&Arc<Language>>, path: Option<&Path>) -> bool {
         if !self.copilot.feature_enabled {
             return false;
         }
@@ -166,7 +176,8 @@ impl AllLanguageSettings {
             }
         }
 
-        self.language(language_name).show_copilot_suggestions
+        self.language(language.map(|l| l.name()).as_deref())
+            .show_copilot_suggestions
     }
 }
 

crates/project/src/lsp_command.rs πŸ”—

@@ -1717,8 +1717,7 @@ impl LspCommand for OnTypeFormatting {
             .await?;
 
         let tab_size = buffer.read_with(&cx, |buffer, cx| {
-            let language_name = buffer.language().map(|language| language.name());
-            language_settings(language_name.as_deref(), cx).tab_size
+            language_settings(buffer.language(), buffer.file(), cx).tab_size
         });
 
         Ok(Self {

crates/project/src/project.rs πŸ”—

@@ -28,7 +28,7 @@ use gpui::{
     ModelHandle, Task, WeakModelHandle,
 };
 use language::{
-    language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter},
+    language_settings::{language_settings, FormatOnSave, Formatter},
     point_to_lsp,
     proto::{
         deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
@@ -72,7 +72,10 @@ use std::{
     time::{Duration, Instant, SystemTime},
 };
 use terminals::Terminals;
-use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _};
+use util::{
+    debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc,
+    ResultExt, TryFutureExt as _,
+};
 
 pub use fs::*;
 pub use worktree::*;
@@ -460,6 +463,7 @@ impl Project {
         client.add_model_request_handler(Self::handle_update_buffer);
         client.add_model_message_handler(Self::handle_update_diagnostic_summary);
         client.add_model_message_handler(Self::handle_update_worktree);
+        client.add_model_message_handler(Self::handle_update_worktree_settings);
         client.add_model_request_handler(Self::handle_create_project_entry);
         client.add_model_request_handler(Self::handle_rename_project_entry);
         client.add_model_request_handler(Self::handle_copy_project_entry);
@@ -686,42 +690,37 @@ impl Project {
     }
 
     fn on_settings_changed(&mut self, cx: &mut ModelContext<Self>) {
-        let settings = all_language_settings(cx);
-
         let mut language_servers_to_start = Vec::new();
         for buffer in self.opened_buffers.values() {
             if let Some(buffer) = buffer.upgrade(cx) {
                 let buffer = buffer.read(cx);
-                if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language())
-                {
-                    if settings
-                        .language(Some(&language.name()))
-                        .enable_language_server
-                    {
-                        let worktree = file.worktree.read(cx);
-                        language_servers_to_start.push((
-                            worktree.id(),
-                            worktree.as_local().unwrap().abs_path().clone(),
-                            language.clone(),
-                        ));
+                if let Some((file, language)) = buffer.file().zip(buffer.language()) {
+                    let settings = language_settings(Some(language), Some(file), cx);
+                    if settings.enable_language_server {
+                        if let Some(file) = File::from_dyn(Some(file)) {
+                            language_servers_to_start
+                                .push((file.worktree.clone(), language.clone()));
+                        }
                     }
                 }
             }
         }
 
         let mut language_servers_to_stop = Vec::new();
-        for language in self.languages.to_vec() {
-            for lsp_adapter in language.lsp_adapters() {
-                if !settings
-                    .language(Some(&language.name()))
-                    .enable_language_server
-                {
-                    let lsp_name = &lsp_adapter.name;
-                    for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
-                        if lsp_name == started_lsp_name {
-                            language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
-                        }
-                    }
+        let languages = self.languages.to_vec();
+        for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
+            let language = languages.iter().find(|l| {
+                l.lsp_adapters()
+                    .iter()
+                    .any(|adapter| &adapter.name == started_lsp_name)
+            });
+            if let Some(language) = language {
+                let worktree = self.worktree_for_id(*worktree_id, cx);
+                let file = worktree.and_then(|tree| {
+                    tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
+                });
+                if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
+                    language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
                 }
             }
         }
@@ -733,8 +732,9 @@ impl Project {
         }
 
         // Start all the newly-enabled language servers.
-        for (worktree_id, worktree_path, language) in language_servers_to_start {
-            self.start_language_servers(worktree_id, worktree_path, language, cx);
+        for (worktree, language) in language_servers_to_start {
+            let worktree_path = worktree.read(cx).abs_path();
+            self.start_language_servers(&worktree, worktree_path, language, cx);
         }
 
         if !self.copilot_enabled && Copilot::global(cx).is_some() {
@@ -1107,6 +1107,21 @@ impl Project {
                 .log_err();
         }
 
+        let store = cx.global::<SettingsStore>();
+        for worktree in self.worktrees(cx) {
+            let worktree_id = worktree.read(cx).id().to_proto();
+            for (path, content) in store.local_settings(worktree.id()) {
+                self.client
+                    .send(proto::UpdateWorktreeSettings {
+                        project_id,
+                        worktree_id,
+                        path: path.to_string_lossy().into(),
+                        content: Some(content),
+                    })
+                    .log_err();
+            }
+        }
+
         let (updates_tx, mut updates_rx) = mpsc::unbounded();
         let client = self.client.clone();
         self.client_state = Some(ProjectClientState::Local {
@@ -1219,6 +1234,14 @@ impl Project {
         message_id: u32,
         cx: &mut ModelContext<Self>,
     ) -> Result<()> {
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            for worktree in &self.worktrees {
+                store
+                    .clear_local_settings(worktree.handle_id(), cx)
+                    .log_err();
+            }
+        });
+
         self.join_project_response_message_id = message_id;
         self.set_worktrees_from_proto(message.worktrees, cx)?;
         self.set_collaborators_from_proto(message.collaborators, cx)?;
@@ -2321,25 +2344,34 @@ impl Project {
         });
 
         if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
-            if let Some(worktree) = file.worktree.read(cx).as_local() {
-                let worktree_id = worktree.id();
-                let worktree_abs_path = worktree.abs_path().clone();
-                self.start_language_servers(worktree_id, worktree_abs_path, new_language, cx);
+            let worktree = file.worktree.clone();
+            if let Some(tree) = worktree.read(cx).as_local() {
+                self.start_language_servers(&worktree, tree.abs_path().clone(), new_language, cx);
             }
         }
     }
 
     fn start_language_servers(
         &mut self,
-        worktree_id: WorktreeId,
+        worktree: &ModelHandle<Worktree>,
         worktree_path: Arc<Path>,
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
-        if !language_settings(Some(&language.name()), cx).enable_language_server {
+        if !language_settings(
+            Some(&language),
+            worktree
+                .update(cx, |tree, cx| tree.root_file(cx))
+                .map(|f| f as _)
+                .as_ref(),
+            cx,
+        )
+        .enable_language_server
+        {
             return;
         }
 
+        let worktree_id = worktree.read(cx).id();
         for adapter in language.lsp_adapters() {
             let key = (worktree_id, adapter.name.clone());
             if self.language_server_ids.contains_key(&key) {
@@ -2748,23 +2780,22 @@ impl Project {
         buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
         cx: &mut ModelContext<Self>,
     ) -> Option<()> {
-        let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, Arc<Language>)> = buffers
+        let language_server_lookup_info: HashSet<(ModelHandle<Worktree>, Arc<Language>)> = buffers
             .into_iter()
             .filter_map(|buffer| {
                 let buffer = buffer.read(cx);
                 let file = File::from_dyn(buffer.file())?;
-                let worktree = file.worktree.read(cx).as_local()?;
                 let full_path = file.full_path(cx);
                 let language = self
                     .languages
                     .language_for_file(&full_path, Some(buffer.as_rope()))
                     .now_or_never()?
                     .ok()?;
-                Some((worktree.id(), worktree.abs_path().clone(), language))
+                Some((file.worktree.clone(), language))
             })
             .collect();
-        for (worktree_id, worktree_abs_path, language) in language_server_lookup_info {
-            self.restart_language_servers(worktree_id, worktree_abs_path, language, cx);
+        for (worktree, language) in language_server_lookup_info {
+            self.restart_language_servers(worktree, language, cx);
         }
 
         None
@@ -2773,11 +2804,13 @@ impl Project {
     // TODO This will break in the case where the adapter's root paths and worktrees are not equal
     fn restart_language_servers(
         &mut self,
-        worktree_id: WorktreeId,
-        fallback_path: Arc<Path>,
+        worktree: ModelHandle<Worktree>,
         language: Arc<Language>,
         cx: &mut ModelContext<Self>,
     ) {
+        let worktree_id = worktree.read(cx).id();
+        let fallback_path = worktree.read(cx).abs_path();
+
         let mut stops = Vec::new();
         for adapter in language.lsp_adapters() {
             stops.push(self.stop_language_server(worktree_id, adapter.name.clone(), cx));
@@ -2807,7 +2840,7 @@ impl Project {
                     .map(|path_buf| Arc::from(path_buf.as_path()))
                     .unwrap_or(fallback_path);
 
-                this.start_language_servers(worktree_id, root_path, language.clone(), cx);
+                this.start_language_servers(&worktree, root_path, language.clone(), cx);
 
                 // Lookup new server ids and set them for each of the orphaned worktrees
                 for adapter in language.lsp_adapters() {
@@ -3432,8 +3465,7 @@ impl Project {
                 let mut project_transaction = ProjectTransaction::default();
                 for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers {
                     let settings = buffer.read_with(&cx, |buffer, cx| {
-                        let language_name = buffer.language().map(|language| language.name());
-                        language_settings(language_name.as_deref(), cx).clone()
+                        language_settings(buffer.language(), buffer.file(), cx).clone()
                     });
 
                     let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
@@ -4463,11 +4495,14 @@ impl Project {
         push_to_history: bool,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<Option<Transaction>>> {
-        let tab_size = buffer.read_with(cx, |buffer, cx| {
-            let language_name = buffer.language().map(|language| language.name());
-            language_settings(language_name.as_deref(), cx).tab_size
+        let (position, tab_size) = buffer.read_with(cx, |buffer, cx| {
+            let position = position.to_point_utf16(buffer);
+            (
+                position,
+                language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx)
+                    .tab_size,
+            )
         });
-        let position = position.to_point_utf16(buffer.read(cx));
         self.request_lsp(
             buffer.clone(),
             OnTypeFormatting {
@@ -4873,6 +4908,7 @@ impl Project {
                 worktree::Event::UpdatedEntries(changes) => {
                     this.update_local_worktree_buffers(&worktree, changes, cx);
                     this.update_local_worktree_language_servers(&worktree, changes, cx);
+                    this.update_local_worktree_settings(&worktree, changes, cx);
                 }
                 worktree::Event::UpdatedGitRepositories(updated_repos) => {
                     this.update_local_worktree_buffers_git_repos(worktree, updated_repos, cx)
@@ -4893,8 +4929,12 @@ impl Project {
                 .push(WorktreeHandle::Weak(worktree.downgrade()));
         }
 
-        cx.observe_release(worktree, |this, worktree, cx| {
+        let handle_id = worktree.id();
+        cx.observe_release(worktree, move |this, worktree, cx| {
             let _ = this.remove_worktree(worktree.id(), cx);
+            cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                store.clear_local_settings(handle_id, cx).log_err()
+            });
         })
         .detach();
 
@@ -5179,6 +5219,71 @@ impl Project {
         .detach();
     }
 
+    fn update_local_worktree_settings(
+        &mut self,
+        worktree: &ModelHandle<Worktree>,
+        changes: &UpdatedEntriesSet,
+        cx: &mut ModelContext<Self>,
+    ) {
+        let project_id = self.remote_id();
+        let worktree_id = worktree.id();
+        let worktree = worktree.read(cx).as_local().unwrap();
+        let remote_worktree_id = worktree.id();
+
+        let mut settings_contents = Vec::new();
+        for (path, _, change) in changes.iter() {
+            if path.ends_with(&*LOCAL_SETTINGS_RELATIVE_PATH) {
+                let settings_dir = Arc::from(
+                    path.ancestors()
+                        .nth(LOCAL_SETTINGS_RELATIVE_PATH.components().count())
+                        .unwrap(),
+                );
+                let fs = self.fs.clone();
+                let removed = *change == PathChange::Removed;
+                let abs_path = worktree.absolutize(path);
+                settings_contents.push(async move {
+                    (settings_dir, (!removed).then_some(fs.load(&abs_path).await))
+                });
+            }
+        }
+
+        if settings_contents.is_empty() {
+            return;
+        }
+
+        let client = self.client.clone();
+        cx.spawn_weak(move |_, mut cx| async move {
+            let settings_contents: Vec<(Arc<Path>, _)> =
+                futures::future::join_all(settings_contents).await;
+            cx.update(|cx| {
+                cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                    for (directory, file_content) in settings_contents {
+                        let file_content = file_content.and_then(|content| content.log_err());
+                        store
+                            .set_local_settings(
+                                worktree_id,
+                                directory.clone(),
+                                file_content.as_ref().map(String::as_str),
+                                cx,
+                            )
+                            .log_err();
+                        if let Some(remote_id) = project_id {
+                            client
+                                .send(proto::UpdateWorktreeSettings {
+                                    project_id: remote_id,
+                                    worktree_id: remote_worktree_id.to_proto(),
+                                    path: directory.to_string_lossy().into_owned(),
+                                    content: file_content,
+                                })
+                                .log_err();
+                        }
+                    }
+                });
+            });
+        })
+        .detach();
+    }
+
     pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
         let new_active_entry = entry.and_then(|project_path| {
             let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
@@ -5431,6 +5536,30 @@ impl Project {
         })
     }
 
+    async fn handle_update_worktree_settings(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> Result<()> {
+        this.update(&mut cx, |this, cx| {
+            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+            if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
+                cx.update_global::<SettingsStore, _, _>(|store, cx| {
+                    store
+                        .set_local_settings(
+                            worktree.id(),
+                            PathBuf::from(&envelope.payload.path).into(),
+                            envelope.payload.content.as_ref().map(String::as_str),
+                            cx,
+                        )
+                        .log_err();
+                });
+            }
+            Ok(())
+        })
+    }
+
     async fn handle_create_project_entry(
         this: ModelHandle<Self>,
         envelope: TypedEnvelope<proto::CreateProjectEntry>,
@@ -6521,8 +6650,8 @@ impl Project {
         }
 
         self.metadata_changed(cx);
-        for (id, _) in old_worktrees_by_id {
-            cx.emit(Event::WorktreeRemoved(id));
+        for id in old_worktrees_by_id.keys() {
+            cx.emit(Event::WorktreeRemoved(*id));
         }
 
         Ok(())
@@ -6892,6 +7021,13 @@ impl WorktreeHandle {
             WorktreeHandle::Weak(handle) => handle.upgrade(cx),
         }
     }
+
+    pub fn handle_id(&self) -> usize {
+        match self {
+            WorktreeHandle::Strong(handle) => handle.id(),
+            WorktreeHandle::Weak(handle) => handle.id(),
+        }
+    }
 }
 
 impl OpenBuffer {

crates/project/src/project_tests.rs πŸ”—

@@ -63,6 +63,66 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
     });
 }
 
+#[gpui::test]
+async fn test_managing_project_specific_settings(
+    deterministic: Arc<Deterministic>,
+    cx: &mut gpui::TestAppContext,
+) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.background());
+    fs.insert_tree(
+        "/the-root",
+        json!({
+            ".zed": {
+                "settings.json": r#"{ "tab_size": 8 }"#
+            },
+            "a": {
+                "a.rs": "fn a() {\n    A\n}"
+            },
+            "b": {
+                ".zed": {
+                    "settings.json": r#"{ "tab_size": 2 }"#
+                },
+                "b.rs": "fn b() {\n  B\n}"
+            }
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
+    let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
+
+    deterministic.run_until_parked();
+    cx.read(|cx| {
+        let tree = worktree.read(cx);
+
+        let settings_a = language_settings(
+            None,
+            Some(
+                &(File::for_entry(
+                    tree.entry_for_path("a/a.rs").unwrap().clone(),
+                    worktree.clone(),
+                ) as _),
+            ),
+            cx,
+        );
+        let settings_b = language_settings(
+            None,
+            Some(
+                &(File::for_entry(
+                    tree.entry_for_path("b/b.rs").unwrap().clone(),
+                    worktree.clone(),
+                ) as _),
+            ),
+            cx,
+        );
+
+        assert_eq!(settings_a.tab_size.get(), 8);
+        assert_eq!(settings_b.tab_size.get(), 2);
+    });
+}
+
 #[gpui::test]
 async fn test_managing_language_servers(
     deterministic: Arc<Deterministic>,

crates/project/src/worktree.rs πŸ”—

@@ -677,6 +677,11 @@ impl Worktree {
             Worktree::Remote(worktree) => worktree.abs_path.clone(),
         }
     }
+
+    pub fn root_file(&self, cx: &mut ModelContext<Self>) -> Option<Arc<File>> {
+        let entry = self.root_entry()?;
+        Some(File::for_entry(entry.clone(), cx.handle()))
+    }
 }
 
 impl LocalWorktree {
@@ -684,14 +689,6 @@ impl LocalWorktree {
         path.starts_with(&self.abs_path)
     }
 
-    fn absolutize(&self, path: &Path) -> PathBuf {
-        if path.file_name().is_some() {
-            self.abs_path.join(path)
-        } else {
-            self.abs_path.to_path_buf()
-        }
-    }
-
     pub(crate) fn load_buffer(
         &mut self,
         id: u64,
@@ -1544,6 +1541,14 @@ impl Snapshot {
         &self.abs_path
     }
 
+    pub fn absolutize(&self, path: &Path) -> PathBuf {
+        if path.file_name().is_some() {
+            self.abs_path.join(path)
+        } else {
+            self.abs_path.to_path_buf()
+        }
+    }
+
     pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
         self.entries_by_id.get(&entry_id, &()).is_some()
     }
@@ -2383,6 +2388,10 @@ impl language::File for File {
             .unwrap_or_else(|| OsStr::new(&self.worktree.read(cx).root_name))
     }
 
+    fn worktree_id(&self) -> usize {
+        self.worktree.id()
+    }
+
     fn is_deleted(&self) -> bool {
         self.is_deleted
     }
@@ -2447,6 +2456,17 @@ impl language::LocalFile for File {
 }
 
 impl File {
+    pub fn for_entry(entry: Entry, worktree: ModelHandle<Worktree>) -> Arc<Self> {
+        Arc::new(Self {
+            worktree,
+            path: entry.path.clone(),
+            mtime: entry.mtime,
+            entry_id: entry.id,
+            is_local: true,
+            is_deleted: false,
+        })
+    }
+
     pub fn from_proto(
         proto: rpc::proto::File,
         worktree: ModelHandle<Worktree>,
@@ -2507,7 +2527,7 @@ pub enum EntryKind {
     File(CharBag),
 }
 
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, PartialEq)]
 pub enum PathChange {
     /// A filesystem entry was was created.
     Added,

crates/rpc/proto/zed.proto πŸ”—

@@ -132,6 +132,8 @@ message Envelope {
 
         OnTypeFormatting on_type_formatting = 111;
         OnTypeFormattingResponse on_type_formatting_response = 112;
+
+        UpdateWorktreeSettings update_worktree_settings = 113;
     }
 }
 
@@ -339,6 +341,13 @@ message UpdateWorktree {
     string abs_path = 10;
 }
 
+message UpdateWorktreeSettings {
+    uint64 project_id = 1;
+    uint64 worktree_id = 2;
+    string path = 3;
+    optional string content = 4;
+}
+
 message CreateProjectEntry {
     uint64 project_id = 1;
     uint64 worktree_id = 2;

crates/rpc/src/proto.rs πŸ”—

@@ -236,6 +236,7 @@ messages!(
     (UpdateProject, Foreground),
     (UpdateProjectCollaborator, Foreground),
     (UpdateWorktree, Foreground),
+    (UpdateWorktreeSettings, Foreground),
     (UpdateDiffBase, Foreground),
     (GetPrivateUserInfo, Foreground),
     (GetPrivateUserInfoResponse, Foreground),
@@ -345,6 +346,7 @@ entity_messages!(
     UpdateProject,
     UpdateProjectCollaborator,
     UpdateWorktree,
+    UpdateWorktreeSettings,
     UpdateDiffBase
 );
 

crates/rpc/src/rpc.rs πŸ”—

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 56;
+pub const PROTOCOL_VERSION: u32 = 57;

crates/settings/src/settings_file.rs πŸ”—

@@ -4,7 +4,14 @@ use assets::Assets;
 use fs::Fs;
 use futures::{channel::mpsc, StreamExt};
 use gpui::{executor::Background, AppContext, AssetSource};
-use std::{borrow::Cow, io::ErrorKind, path::PathBuf, str, sync::Arc, time::Duration};
+use std::{
+    borrow::Cow,
+    io::ErrorKind,
+    path::{Path, PathBuf},
+    str,
+    sync::Arc,
+    time::Duration,
+};
 use util::{paths, ResultExt};
 
 pub fn register<T: Setting>(cx: &mut AppContext) {
@@ -17,6 +24,10 @@ pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
     cx.global::<SettingsStore>().get(None)
 }
 
+pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
+    cx.global::<SettingsStore>().get(location)
+}
+
 pub fn default_settings() -> Cow<'static, str> {
     match Assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap() {
         Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
@@ -55,15 +66,22 @@ pub fn watch_config_file(
         .spawn(async move {
             let events = fs.watch(&path, Duration::from_millis(100)).await;
             futures::pin_mut!(events);
+
+            let contents = fs.load(&path).await.unwrap_or_default();
+            if tx.unbounded_send(contents).is_err() {
+                return;
+            }
+
             loop {
+                if events.next().await.is_none() {
+                    break;
+                }
+
                 if let Ok(contents) = fs.load(&path).await {
                     if !tx.unbounded_send(contents).is_ok() {
                         break;
                     }
                 }
-                if events.next().await.is_none() {
-                    break;
-                }
             }
         })
         .detach();

crates/settings/src/settings_store.rs πŸ”—

@@ -1,4 +1,4 @@
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use collections::{btree_map, hash_map, BTreeMap, HashMap};
 use gpui::AppContext;
 use lazy_static::lazy_static;
@@ -84,19 +84,30 @@ pub struct SettingsJsonSchemaParams<'a> {
 }
 
 /// A set of strongly-typed setting values defined via multiple JSON files.
-#[derive(Default)]
 pub struct SettingsStore {
     setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
-    default_deserialized_settings: Option<serde_json::Value>,
-    user_deserialized_settings: Option<serde_json::Value>,
-    local_deserialized_settings: BTreeMap<Arc<Path>, serde_json::Value>,
+    default_deserialized_settings: serde_json::Value,
+    user_deserialized_settings: serde_json::Value,
+    local_deserialized_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
     tab_size_callback: Option<(TypeId, Box<dyn Fn(&dyn Any) -> Option<usize>>)>,
 }
 
+impl Default for SettingsStore {
+    fn default() -> Self {
+        SettingsStore {
+            setting_values: Default::default(),
+            default_deserialized_settings: serde_json::json!({}),
+            user_deserialized_settings: serde_json::json!({}),
+            local_deserialized_settings: Default::default(),
+            tab_size_callback: Default::default(),
+        }
+    }
+}
+
 #[derive(Debug)]
 struct SettingValue<T> {
     global_value: Option<T>,
-    local_values: Vec<(Arc<Path>, T)>,
+    local_values: Vec<(usize, Arc<Path>, T)>,
 }
 
 trait AnySettingValue {
@@ -109,9 +120,9 @@ trait AnySettingValue {
         custom: &[DeserializedSetting],
         cx: &AppContext,
     ) -> Result<Box<dyn Any>>;
-    fn value_for_path(&self, path: Option<&Path>) -> &dyn Any;
+    fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
     fn set_global_value(&mut self, value: Box<dyn Any>);
-    fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>);
+    fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
     fn json_schema(
         &self,
         generator: &mut SchemaGenerator,
@@ -136,27 +147,24 @@ impl SettingsStore {
             local_values: Vec::new(),
         }));
 
-        if let Some(default_settings) = &self.default_deserialized_settings {
-            if let Some(default_settings) = setting_value
-                .deserialize_setting(default_settings)
+        if let Some(default_settings) = setting_value
+            .deserialize_setting(&self.default_deserialized_settings)
+            .log_err()
+        {
+            let mut user_values_stack = Vec::new();
+
+            if let Some(user_settings) = setting_value
+                .deserialize_setting(&self.user_deserialized_settings)
                 .log_err()
             {
-                let mut user_values_stack = Vec::new();
-
-                if let Some(user_settings) = &self.user_deserialized_settings {
-                    if let Some(user_settings) =
-                        setting_value.deserialize_setting(user_settings).log_err()
-                    {
-                        user_values_stack = vec![user_settings];
-                    }
-                }
+                user_values_stack = vec![user_settings];
+            }
 
-                if let Some(setting) = setting_value
-                    .load_setting(&default_settings, &user_values_stack, cx)
-                    .log_err()
-                {
-                    setting_value.set_global_value(setting);
-                }
+            if let Some(setting) = setting_value
+                .load_setting(&default_settings, &user_values_stack, cx)
+                .log_err()
+            {
+                setting_value.set_global_value(setting);
             }
         }
     }
@@ -165,7 +173,7 @@ impl SettingsStore {
     ///
     /// Panics if the given setting type has not been registered, or if there is no
     /// value for this setting.
-    pub fn get<T: Setting>(&self, path: Option<&Path>) -> &T {
+    pub fn get<T: Setting>(&self, path: Option<(usize, &Path)>) -> &T {
         self.setting_values
             .get(&TypeId::of::<T>())
             .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
@@ -189,9 +197,7 @@ impl SettingsStore {
     /// This is only for debugging and reporting. For user-facing functionality,
     /// use the typed setting interface.
     pub fn untyped_user_settings(&self) -> &serde_json::Value {
-        self.user_deserialized_settings
-            .as_ref()
-            .unwrap_or(&serde_json::Value::Null)
+        &self.user_deserialized_settings
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -213,11 +219,7 @@ impl SettingsStore {
         cx: &AppContext,
         update: impl FnOnce(&mut T::FileContent),
     ) {
-        if self.user_deserialized_settings.is_none() {
-            self.set_user_settings("{}", cx).unwrap();
-        }
-        let old_text =
-            serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap();
+        let old_text = serde_json::to_string(&self.user_deserialized_settings).unwrap();
         let new_text = self.new_text_for_update::<T>(old_text, update);
         self.set_user_settings(&new_text, cx).unwrap();
     }
@@ -250,11 +252,7 @@ impl SettingsStore {
             .setting_values
             .get(&setting_type_id)
             .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
-            .deserialize_setting(
-                self.user_deserialized_settings
-                    .as_ref()
-                    .expect("no user settings loaded"),
-            )
+            .deserialize_setting(&self.user_deserialized_settings)
             .unwrap_or_else(|e| {
                 panic!(
                     "could not deserialize setting type {} from user settings: {}",
@@ -323,10 +321,14 @@ impl SettingsStore {
         default_settings_content: &str,
         cx: &AppContext,
     ) -> Result<()> {
-        self.default_deserialized_settings =
-            Some(parse_json_with_comments(default_settings_content)?);
-        self.recompute_values(None, cx)?;
-        Ok(())
+        let settings: serde_json::Value = parse_json_with_comments(default_settings_content)?;
+        if settings.is_object() {
+            self.default_deserialized_settings = settings;
+            self.recompute_values(None, cx)?;
+            Ok(())
+        } else {
+            Err(anyhow!("settings must be an object"))
+        }
     }
 
     /// Set the user settings via a JSON string.
@@ -335,28 +337,49 @@ impl SettingsStore {
         user_settings_content: &str,
         cx: &AppContext,
     ) -> Result<()> {
-        self.user_deserialized_settings = Some(parse_json_with_comments(user_settings_content)?);
-        self.recompute_values(None, cx)?;
-        Ok(())
+        let settings: serde_json::Value = parse_json_with_comments(user_settings_content)?;
+        if settings.is_object() {
+            self.user_deserialized_settings = settings;
+            self.recompute_values(None, cx)?;
+            Ok(())
+        } else {
+            Err(anyhow!("settings must be an object"))
+        }
     }
 
     /// Add or remove a set of local settings via a JSON string.
     pub fn set_local_settings(
         &mut self,
+        root_id: usize,
         path: Arc<Path>,
         settings_content: Option<&str>,
         cx: &AppContext,
     ) -> Result<()> {
         if let Some(content) = settings_content {
             self.local_deserialized_settings
-                .insert(path.clone(), parse_json_with_comments(content)?);
+                .insert((root_id, path.clone()), parse_json_with_comments(content)?);
         } else {
-            self.local_deserialized_settings.remove(&path);
+            self.local_deserialized_settings
+                .remove(&(root_id, path.clone()));
         }
-        self.recompute_values(Some(&path), cx)?;
+        self.recompute_values(Some((root_id, &path)), cx)?;
         Ok(())
     }
 
+    /// Add or remove a set of local settings via a JSON string.
+    pub fn clear_local_settings(&mut self, root_id: usize, cx: &AppContext) -> Result<()> {
+        self.local_deserialized_settings
+            .retain(|k, _| k.0 != root_id);
+        self.recompute_values(Some((root_id, "".as_ref())), cx)?;
+        Ok(())
+    }
+
+    pub fn local_settings(&self, root_id: usize) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
+        self.local_deserialized_settings
+            .range((root_id, Path::new("").into())..(root_id + 1, Path::new("").into()))
+            .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
+    }
+
     pub fn json_schema(
         &self,
         schema_params: &SettingsJsonSchemaParams,
@@ -436,72 +459,70 @@ impl SettingsStore {
 
     fn recompute_values(
         &mut self,
-        changed_local_path: Option<&Path>,
+        changed_local_path: Option<(usize, &Path)>,
         cx: &AppContext,
     ) -> Result<()> {
         // Reload the global and local values for every setting.
         let mut user_settings_stack = Vec::<DeserializedSetting>::new();
-        let mut paths_stack = Vec::<Option<&Path>>::new();
+        let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
         for setting_value in self.setting_values.values_mut() {
-            if let Some(default_settings) = &self.default_deserialized_settings {
-                let default_settings = setting_value.deserialize_setting(default_settings)?;
+            let default_settings =
+                setting_value.deserialize_setting(&self.default_deserialized_settings)?;
 
-                user_settings_stack.clear();
-                paths_stack.clear();
+            user_settings_stack.clear();
+            paths_stack.clear();
 
-                if let Some(user_settings) = &self.user_deserialized_settings {
-                    if let Some(user_settings) =
-                        setting_value.deserialize_setting(user_settings).log_err()
-                    {
-                        user_settings_stack.push(user_settings);
-                        paths_stack.push(None);
-                    }
+            if let Some(user_settings) = setting_value
+                .deserialize_setting(&self.user_deserialized_settings)
+                .log_err()
+            {
+                user_settings_stack.push(user_settings);
+                paths_stack.push(None);
+            }
+
+            // If the global settings file changed, reload the global value for the field.
+            if changed_local_path.is_none() {
+                if let Some(value) = setting_value
+                    .load_setting(&default_settings, &user_settings_stack, cx)
+                    .log_err()
+                {
+                    setting_value.set_global_value(value);
                 }
+            }
 
-                // If the global settings file changed, reload the global value for the field.
-                if changed_local_path.is_none() {
-                    if let Some(value) = setting_value
-                        .load_setting(&default_settings, &user_settings_stack, cx)
-                        .log_err()
-                    {
-                        setting_value.set_global_value(value);
+            // Reload the local values for the setting.
+            for ((root_id, path), local_settings) in &self.local_deserialized_settings {
+                // Build a stack of all of the local values for that setting.
+                while let Some(prev_entry) = paths_stack.last() {
+                    if let Some((prev_root_id, prev_path)) = prev_entry {
+                        if root_id != prev_root_id || !path.starts_with(prev_path) {
+                            paths_stack.pop();
+                            user_settings_stack.pop();
+                            continue;
+                        }
                     }
+                    break;
                 }
 
-                // Reload the local values for the setting.
-                for (path, local_settings) in &self.local_deserialized_settings {
-                    // Build a stack of all of the local values for that setting.
-                    while let Some(prev_path) = paths_stack.last() {
-                        if let Some(prev_path) = prev_path {
-                            if !path.starts_with(prev_path) {
-                                paths_stack.pop();
-                                user_settings_stack.pop();
-                                continue;
-                            }
-                        }
-                        break;
+                if let Some(local_settings) =
+                    setting_value.deserialize_setting(&local_settings).log_err()
+                {
+                    paths_stack.push(Some((*root_id, path.as_ref())));
+                    user_settings_stack.push(local_settings);
+
+                    // If a local settings file changed, then avoid recomputing local
+                    // settings for any path outside of that directory.
+                    if changed_local_path.map_or(false, |(changed_root_id, changed_local_path)| {
+                        *root_id != changed_root_id || !path.starts_with(changed_local_path)
+                    }) {
+                        continue;
                     }
 
-                    if let Some(local_settings) =
-                        setting_value.deserialize_setting(&local_settings).log_err()
+                    if let Some(value) = setting_value
+                        .load_setting(&default_settings, &user_settings_stack, cx)
+                        .log_err()
                     {
-                        paths_stack.push(Some(path.as_ref()));
-                        user_settings_stack.push(local_settings);
-
-                        // If a local settings file changed, then avoid recomputing local
-                        // settings for any path outside of that directory.
-                        if changed_local_path.map_or(false, |changed_local_path| {
-                            !path.starts_with(changed_local_path)
-                        }) {
-                            continue;
-                        }
-
-                        if let Some(value) = setting_value
-                            .load_setting(&default_settings, &user_settings_stack, cx)
-                            .log_err()
-                        {
-                            setting_value.set_local_value(path.clone(), value);
-                        }
+                        setting_value.set_local_value(*root_id, path.clone(), value);
                     }
                 }
             }
@@ -510,6 +531,24 @@ impl SettingsStore {
     }
 }
 
+impl Debug for SettingsStore {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("SettingsStore")
+            .field(
+                "types",
+                &self
+                    .setting_values
+                    .values()
+                    .map(|value| value.setting_type_name())
+                    .collect::<Vec<_>>(),
+            )
+            .field("default_settings", &self.default_deserialized_settings)
+            .field("user_settings", &self.user_deserialized_settings)
+            .field("local_settings", &self.local_deserialized_settings)
+            .finish_non_exhaustive()
+    }
+}
+
 impl<T: Setting> AnySettingValue for SettingValue<T> {
     fn key(&self) -> Option<&'static str> {
         T::KEY
@@ -546,10 +585,10 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
         Ok(DeserializedSetting(Box::new(value)))
     }
 
-    fn value_for_path(&self, path: Option<&Path>) -> &dyn Any {
-        if let Some(path) = path {
-            for (settings_path, value) in self.local_values.iter().rev() {
-                if path.starts_with(&settings_path) {
+    fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
+        if let Some((root_id, path)) = path {
+            for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
+                if root_id == *settings_root_id && path.starts_with(&settings_path) {
                     return value;
                 }
             }
@@ -563,11 +602,14 @@ impl<T: Setting> AnySettingValue for SettingValue<T> {
         self.global_value = Some(*value.downcast().unwrap());
     }
 
-    fn set_local_value(&mut self, path: Arc<Path>, value: Box<dyn Any>) {
+    fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>) {
         let value = *value.downcast().unwrap();
-        match self.local_values.binary_search_by_key(&&path, |e| &e.0) {
-            Ok(ix) => self.local_values[ix].1 = value,
-            Err(ix) => self.local_values.insert(ix, (path, value)),
+        match self
+            .local_values
+            .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1))
+        {
+            Ok(ix) => self.local_values[ix].2 = value,
+            Err(ix) => self.local_values.insert(ix, (root_id, path, value)),
         }
     }
 
@@ -884,6 +926,7 @@ mod tests {
 
         store
             .set_local_settings(
+                1,
                 Path::new("/root1").into(),
                 Some(r#"{ "user": { "staff": true } }"#),
                 cx,
@@ -891,6 +934,7 @@ mod tests {
             .unwrap();
         store
             .set_local_settings(
+                1,
                 Path::new("/root1/subdir").into(),
                 Some(r#"{ "user": { "name": "Jane Doe" } }"#),
                 cx,
@@ -899,6 +943,7 @@ mod tests {
 
         store
             .set_local_settings(
+                1,
                 Path::new("/root2").into(),
                 Some(r#"{ "user": { "age": 42 }, "key2": "b" }"#),
                 cx,
@@ -906,7 +951,7 @@ mod tests {
             .unwrap();
 
         assert_eq!(
-            store.get::<UserSettings>(Some(Path::new("/root1/something"))),
+            store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
             &UserSettings {
                 name: "John Doe".to_string(),
                 age: 31,
@@ -914,7 +959,7 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<UserSettings>(Some(Path::new("/root1/subdir/something"))),
+            store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
             &UserSettings {
                 name: "Jane Doe".to_string(),
                 age: 31,
@@ -922,7 +967,7 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<UserSettings>(Some(Path::new("/root2/something"))),
+            store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
             &UserSettings {
                 name: "John Doe".to_string(),
                 age: 42,
@@ -930,7 +975,7 @@ mod tests {
             }
         );
         assert_eq!(
-            store.get::<MultiKeySettings>(Some(Path::new("/root2/something"))),
+            store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
             &MultiKeySettings {
                 key1: "a".to_string(),
                 key2: "b".to_string(),

crates/terminal_view/src/terminal_panel.rs πŸ”—

@@ -22,7 +22,7 @@ const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
 actions!(terminal_panel, [ToggleFocus]);
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_action(TerminalPanel::add_terminal);
+    cx.add_action(TerminalPanel::new_terminal);
 }
 
 pub enum Event {
@@ -70,6 +70,7 @@ impl TerminalPanel {
                     .with_child(Pane::render_tab_bar_button(
                         0,
                         "icons/plus_12.svg",
+                        false,
                         Some((
                             "New Terminal".into(),
                             Some(Box::new(workspace::NewTerminal)),
@@ -80,7 +81,7 @@ impl TerminalPanel {
                             cx.window_context().defer(move |cx| {
                                 if let Some(this) = this.upgrade(cx) {
                                     this.update(cx, |this, cx| {
-                                        this.add_terminal(&Default::default(), cx);
+                                        this.add_terminal(cx);
                                     });
                                 }
                             })
@@ -94,6 +95,7 @@ impl TerminalPanel {
                         } else {
                             "icons/maximize_8.svg"
                         },
+                        pane.is_zoomed(),
                         Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
                         cx,
                         move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
@@ -220,7 +222,19 @@ impl TerminalPanel {
         }
     }
 
-    fn add_terminal(&mut self, _: &workspace::NewTerminal, cx: &mut ViewContext<Self>) {
+    fn new_terminal(
+        workspace: &mut Workspace,
+        _: &workspace::NewTerminal,
+        cx: &mut ViewContext<Workspace>,
+    ) {
+        let Some(this) = workspace.focus_panel::<Self>(cx) else {
+            return;
+        };
+
+        this.update(cx, |this, cx| this.add_terminal(cx))
+    }
+
+    fn add_terminal(&mut self, cx: &mut ViewContext<Self>) {
         let workspace = self.workspace.clone();
         cx.spawn(|this, mut cx| async move {
             let pane = this.read_with(&cx, |this, _| this.pane.clone())?;
@@ -363,7 +377,7 @@ impl Panel for TerminalPanel {
 
     fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
         if active && self.pane.read(cx).items_len() == 0 {
-            self.add_terminal(&Default::default(), cx)
+            self.add_terminal(cx)
         }
     }
 

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -38,7 +38,7 @@ use workspace::{
     notifications::NotifyResultExt,
     pane, register_deserializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
-    Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+    NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
 };
 
 pub use terminal::TerminalSettings;
@@ -66,10 +66,10 @@ pub fn init(cx: &mut AppContext) {
     terminal_panel::init(cx);
     terminal::init(cx);
 
-    cx.add_action(TerminalView::deploy);
-
     register_deserializable_item::<TerminalView>(cx);
 
+    cx.add_action(TerminalView::deploy);
+
     //Useful terminal views
     cx.add_action(TerminalView::send_text);
     cx.add_action(TerminalView::send_keystroke);
@@ -101,7 +101,7 @@ impl TerminalView {
     ///Create a new Terminal in the current working directory or the user's home directory
     pub fn deploy(
         workspace: &mut Workspace,
-        _: &workspace::NewTerminal,
+        _: &NewCenterTerminal,
         cx: &mut ViewContext<Workspace>,
     ) {
         let strategy = settings::get::<TerminalSettings>(cx);
@@ -133,8 +133,8 @@ impl TerminalView {
             Event::Wakeup => {
                 if !cx.is_self_focused() {
                     this.has_new_content = true;
-                    cx.notify();
                 }
+                cx.notify();
                 cx.emit(Event::Wakeup);
             }
             Event::Bell => {
@@ -905,7 +905,10 @@ mod tests {
         cx: &mut TestAppContext,
     ) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
         let params = cx.update(AppState::test);
-        cx.update(|cx| theme::init((), cx));
+        cx.update(|cx| {
+            theme::init((), cx);
+            language::init(cx);
+        });
 
         let project = Project::test(params.fs.clone(), [], cx).await;
         let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));

crates/theme/src/theme.rs πŸ”—

@@ -90,7 +90,8 @@ pub struct Workspace {
     pub breadcrumbs: Interactive<ContainedText>,
     pub disconnected_overlay: ContainedText,
     pub modal: ContainerStyle,
-    pub zoomed_foreground: ContainerStyle,
+    pub zoomed_panel_foreground: ContainerStyle,
+    pub zoomed_pane_foreground: ContainerStyle,
     pub zoomed_background: ContainerStyle,
     pub notification: ContainerStyle,
     pub notifications: Notifications,

crates/util/src/paths.rs πŸ”—

@@ -15,6 +15,7 @@ lazy_static::lazy_static! {
     pub static ref LAST_USERNAME: PathBuf = CONFIG_DIR.join("last-username.txt");
     pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
     pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
+    pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json");
 }
 
 pub mod legacy {

crates/welcome/src/welcome.rs πŸ”—

@@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) {
 
 pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
     open_new(&app_state, cx, |workspace, cx| {
-        workspace.toggle_dock(DockPosition::Left, false, cx);
+        workspace.toggle_dock(DockPosition::Left, cx);
         let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
         workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
         cx.focus(&welcome_page);

crates/workspace/src/dock.rs πŸ”—

@@ -175,12 +175,16 @@ impl Dock {
         }
     }
 
+    pub fn position(&self) -> DockPosition {
+        self.position
+    }
+
     pub fn is_open(&self) -> bool {
         self.is_open
     }
 
     pub fn has_focus(&self, cx: &WindowContext) -> bool {
-        self.active_panel()
+        self.visible_panel()
             .map_or(false, |panel| panel.has_focus(cx))
     }
 
@@ -207,7 +211,7 @@ impl Dock {
         self.active_panel_index
     }
 
-    pub fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
+    pub(crate) fn set_open(&mut self, open: bool, cx: &mut ViewContext<Self>) {
         if open != self.is_open {
             self.is_open = open;
             if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) {
@@ -218,11 +222,6 @@ impl Dock {
         }
     }
 
-    pub fn toggle_open(&mut self, cx: &mut ViewContext<Self>) {
-        self.set_open(!self.is_open, cx);
-        cx.notify();
-    }
-
     pub fn set_panel_zoomed(
         &mut self,
         panel: &AnyViewHandle,
@@ -265,7 +264,7 @@ impl Dock {
                         cx.focus(&panel);
                     }
                 } else if T::should_close_on_event(event)
-                    && this.active_panel().map_or(false, |p| p.id() == panel.id())
+                    && this.visible_panel().map_or(false, |p| p.id() == panel.id())
                 {
                     this.set_open(false, cx);
                 }
@@ -321,12 +320,16 @@ impl Dock {
         }
     }
 
-    pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
-        let entry = self.active_entry()?;
+    pub fn visible_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+        let entry = self.visible_entry()?;
         Some(&entry.panel)
     }
 
-    fn active_entry(&self) -> Option<&PanelEntry> {
+    pub fn active_panel(&self) -> Option<&Rc<dyn PanelHandle>> {
+        Some(&self.panel_entries.get(self.active_panel_index)?.panel)
+    }
+
+    fn visible_entry(&self) -> Option<&PanelEntry> {
         if self.is_open {
             self.panel_entries.get(self.active_panel_index)
         } else {
@@ -335,7 +338,7 @@ impl Dock {
     }
 
     pub fn zoomed_panel(&self, cx: &WindowContext) -> Option<Rc<dyn PanelHandle>> {
-        let entry = self.active_entry()?;
+        let entry = self.visible_entry()?;
         if entry.panel.is_zoomed(cx) {
             Some(entry.panel.clone())
         } else {
@@ -368,7 +371,7 @@ impl Dock {
     }
 
     pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
-        if let Some(active_entry) = self.active_entry() {
+        if let Some(active_entry) = self.visible_entry() {
             Empty::new()
                 .into_any()
                 .contained()
@@ -405,7 +408,7 @@ impl View for Dock {
     }
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        if let Some(active_entry) = self.active_entry() {
+        if let Some(active_entry) = self.visible_entry() {
             let style = self.style(cx);
             ChildView::new(active_entry.panel.as_any(), cx)
                 .contained()
@@ -423,7 +426,7 @@ impl View for Dock {
 
     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
         if cx.is_self_focused() {
-            if let Some(active_entry) = self.active_entry() {
+            if let Some(active_entry) = self.visible_entry() {
                 cx.focus(active_entry.panel.as_any());
             } else {
                 cx.focus_parent();
@@ -479,11 +482,22 @@ impl View for PanelButtons {
         Flex::row()
             .with_children(panels.into_iter().enumerate().map(
                 |(panel_ix, (view, context_menu))| {
-                    let (tooltip, tooltip_action) = view.icon_tooltip(cx);
+                    let is_active = is_open && panel_ix == active_ix;
+                    let (tooltip, tooltip_action) = if is_active {
+                        (
+                            format!("Close {} dock", dock_position.to_label()),
+                            Some(match dock_position {
+                                DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
+                                DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
+                                DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
+                            }),
+                        )
+                    } else {
+                        view.icon_tooltip(cx)
+                    };
                     Stack::new()
                         .with_child(
                             MouseEventHandler::<Self, _>::new(panel_ix, cx, |state, cx| {
-                                let is_active = is_open && panel_ix == active_ix;
                                 let style = button_style.style_for(state, is_active);
                                 Flex::row()
                                     .with_child(
@@ -510,13 +524,22 @@ impl View for PanelButtons {
                             })
                             .with_cursor_style(CursorStyle::PointingHand)
                             .on_click(MouseButton::Left, {
+                                let tooltip_action =
+                                    tooltip_action.as_ref().map(|action| action.boxed_clone());
                                 move |_, this, cx| {
-                                    if let Some(workspace) = this.workspace.upgrade(cx) {
-                                        cx.window_context().defer(move |cx| {
-                                            workspace.update(cx, |workspace, cx| {
-                                                workspace.toggle_panel(dock_position, panel_ix, cx)
-                                            });
-                                        });
+                                    if let Some(tooltip_action) = &tooltip_action {
+                                        let window_id = cx.window_id();
+                                        let view_id = this.workspace.id();
+                                        let tooltip_action = tooltip_action.boxed_clone();
+                                        cx.spawn(|_, mut cx| async move {
+                                            cx.dispatch_action(
+                                                window_id,
+                                                view_id,
+                                                &*tooltip_action,
+                                            )
+                                            .ok();
+                                        })
+                                        .detach();
                                     }
                                 }
                             })

crates/workspace/src/notifications.rs πŸ”—

@@ -1,5 +1,5 @@
 use crate::{Toast, Workspace};
-use collections::HashSet;
+use collections::HashMap;
 use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
 use std::{any::TypeId, ops::DerefMut};
 
@@ -33,12 +33,12 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
     }
 }
 
-struct NotificationTracker {
-    notifications_sent: HashSet<TypeId>,
+pub(crate) struct NotificationTracker {
+    notifications_sent: HashMap<TypeId, Vec<usize>>,
 }
 
 impl std::ops::Deref for NotificationTracker {
-    type Target = HashSet<TypeId>;
+    type Target = HashMap<TypeId, Vec<usize>>;
 
     fn deref(&self) -> &Self::Target {
         &self.notifications_sent
@@ -54,24 +54,33 @@ impl DerefMut for NotificationTracker {
 impl NotificationTracker {
     fn new() -> Self {
         Self {
-            notifications_sent: HashSet::default(),
+            notifications_sent: Default::default(),
         }
     }
 }
 
 impl Workspace {
+    pub fn has_shown_notification_once<V: Notification>(
+        &self,
+        id: usize,
+        cx: &ViewContext<Self>,
+    ) -> bool {
+        cx.global::<NotificationTracker>()
+            .get(&TypeId::of::<V>())
+            .map(|ids| ids.contains(&id))
+            .unwrap_or(false)
+    }
+
     pub fn show_notification_once<V: Notification>(
         &mut self,
         id: usize,
         cx: &mut ViewContext<Self>,
         build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
     ) {
-        if !cx
-            .global::<NotificationTracker>()
-            .contains(&TypeId::of::<V>())
-        {
+        if !self.has_shown_notification_once::<V>(id, cx) {
             cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
-                tracker.insert(TypeId::of::<V>())
+                let entry = tracker.entry(TypeId::of::<V>()).or_default();
+                entry.push(id);
             });
 
             self.show_notification::<V>(id, cx, build_notification)
@@ -154,9 +163,10 @@ pub mod simple_message_notification {
     use gpui::{
         actions,
         elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
+        fonts::TextStyle,
         impl_actions,
         platform::{CursorStyle, MouseButton},
-        AppContext, Element, Entity, View, ViewContext,
+        AnyElement, AppContext, Element, Entity, View, ViewContext,
     };
     use menu::Cancel;
     use serde::Deserialize;
@@ -184,8 +194,13 @@ pub mod simple_message_notification {
         )
     }
 
+    enum NotificationMessage {
+        Text(Cow<'static, str>),
+        Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
+    }
+
     pub struct MessageNotification {
-        message: Cow<'static, str>,
+        message: NotificationMessage,
         on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
         click_message: Option<Cow<'static, str>>,
     }
@@ -204,7 +219,17 @@ pub mod simple_message_notification {
             S: Into<Cow<'static, str>>,
         {
             Self {
-                message: message.into(),
+                message: NotificationMessage::Text(message.into()),
+                on_click: None,
+                click_message: None,
+            }
+        }
+
+        pub fn new_element(
+            message: fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>,
+        ) -> MessageNotification {
+            Self {
+                message: NotificationMessage::Element(message),
                 on_click: None,
                 click_message: None,
             }
@@ -243,84 +268,90 @@ pub mod simple_message_notification {
             enum MessageNotificationTag {}
 
             let click_message = self.click_message.clone();
-            let message = self.message.clone();
+            let message = match &self.message {
+                NotificationMessage::Text(text) => {
+                    Text::new(text.to_owned(), theme.message.text.clone()).into_any()
+                }
+                NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
+            };
             let on_click = self.on_click.clone();
             let has_click_action = on_click.is_some();
 
-            MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
-                Flex::column()
-                    .with_child(
-                        Flex::row()
-                            .with_child(
-                                Text::new(message, theme.message.text.clone())
-                                    .contained()
-                                    .with_style(theme.message.container)
-                                    .aligned()
-                                    .top()
-                                    .left()
-                                    .flex(1., true),
-                            )
-                            .with_child(
-                                MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
-                                    let style = theme.dismiss_button.style_for(state, false);
-                                    Svg::new("icons/x_mark_8.svg")
-                                        .with_color(style.color)
-                                        .constrained()
-                                        .with_width(style.icon_width)
-                                        .aligned()
-                                        .contained()
-                                        .with_style(style.container)
-                                        .constrained()
-                                        .with_width(style.button_width)
-                                        .with_height(style.button_width)
-                                })
-                                .with_padding(Padding::uniform(5.))
-                                .on_click(MouseButton::Left, move |_, this, cx| {
-                                    this.dismiss(&Default::default(), cx);
-                                })
-                                .with_cursor_style(CursorStyle::PointingHand)
-                                .aligned()
-                                .constrained()
-                                .with_height(
-                                    cx.font_cache().line_height(theme.message.text.font_size),
-                                )
+            Flex::column()
+                .with_child(
+                    Flex::row()
+                        .with_child(
+                            message
+                                .contained()
+                                .with_style(theme.message.container)
                                 .aligned()
                                 .top()
-                                .flex_float(),
-                            ),
-                    )
-                    .with_children({
-                        let style = theme.action_message.style_for(state, false);
-                        if let Some(click_message) = click_message {
-                            Some(
-                                Flex::row().with_child(
-                                    Text::new(click_message, style.text.clone())
+                                .left()
+                                .flex(1., true),
+                        )
+                        .with_child(
+                            MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
+                                let style = theme.dismiss_button.style_for(state, false);
+                                Svg::new("icons/x_mark_8.svg")
+                                    .with_color(style.color)
+                                    .constrained()
+                                    .with_width(style.icon_width)
+                                    .aligned()
+                                    .contained()
+                                    .with_style(style.container)
+                                    .constrained()
+                                    .with_width(style.button_width)
+                                    .with_height(style.button_width)
+                            })
+                            .with_padding(Padding::uniform(5.))
+                            .on_click(MouseButton::Left, move |_, this, cx| {
+                                this.dismiss(&Default::default(), cx);
+                            })
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .aligned()
+                            .constrained()
+                            .with_height(cx.font_cache().line_height(theme.message.text.font_size))
+                            .aligned()
+                            .top()
+                            .flex_float(),
+                        ),
+                )
+                .with_children({
+                    click_message
+                        .map(|click_message| {
+                            MouseEventHandler::<MessageNotificationTag, _>::new(
+                                0,
+                                cx,
+                                |state, _| {
+                                    let style = theme.action_message.style_for(state, false);
+
+                                    Flex::row()
+                                        .with_child(
+                                            Text::new(click_message, style.text.clone())
+                                                .contained()
+                                                .with_style(style.container),
+                                        )
                                         .contained()
-                                        .with_style(style.container),
-                                ),
+                                },
                             )
-                        } else {
-                            None
-                        }
+                            .on_click(MouseButton::Left, move |_, this, cx| {
+                                if let Some(on_click) = on_click.as_ref() {
+                                    on_click(cx);
+                                    this.dismiss(&Default::default(), cx);
+                                }
+                            })
+                            // Since we're not using a proper overlay, we have to capture these extra events
+                            .on_down(MouseButton::Left, |_, _, _| {})
+                            .on_up(MouseButton::Left, |_, _, _| {})
+                            .with_cursor_style(if has_click_action {
+                                CursorStyle::PointingHand
+                            } else {
+                                CursorStyle::Arrow
+                            })
+                        })
                         .into_iter()
-                    })
-                    .contained()
-            })
-            // Since we're not using a proper overlay, we have to capture these extra events
-            .on_down(MouseButton::Left, |_, _, _| {})
-            .on_up(MouseButton::Left, |_, _, _| {})
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(on_click) = on_click.as_ref() {
-                    on_click(cx);
-                    this.dismiss(&Default::default(), cx);
-                }
-            })
-            .with_cursor_style(if has_click_action {
-                CursorStyle::PointingHand
-            } else {
-                CursorStyle::Arrow
-            })
-            .into_any()
+                })
+                .into_any()
         }
     }
 

crates/workspace/src/pane.rs πŸ”—

@@ -2,8 +2,8 @@ mod dragged_item_receiver;
 
 use super::{ItemHandle, SplitDirection};
 use crate::{
-    item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal,
-    ToggleZoom, Workspace, WorkspaceSettings,
+    item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
+    NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
 };
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
@@ -131,7 +131,6 @@ pub enum Event {
 pub struct Pane {
     items: Vec<Box<dyn ItemHandle>>,
     activation_history: Vec<usize>,
-    is_active: bool,
     zoomed: bool,
     active_item_index: usize,
     last_focused_view_by_item: HashMap<usize, AnyWeakViewHandle>,
@@ -238,7 +237,6 @@ impl Pane {
         Self {
             items: Vec::new(),
             activation_history: Vec::new(),
-            is_active: true,
             zoomed: false,
             active_item_index: 0,
             last_focused_view_by_item: Default::default(),
@@ -270,6 +268,7 @@ impl Pane {
                     .with_child(Self::render_tab_bar_button(
                         0,
                         "icons/plus_12.svg",
+                        false,
                         Some(("New...".into(), None)),
                         cx,
                         |pane, cx| pane.deploy_new_menu(cx),
@@ -279,6 +278,7 @@ impl Pane {
                     .with_child(Self::render_tab_bar_button(
                         1,
                         "icons/split_12.svg",
+                        false,
                         Some(("Split Pane".into(), None)),
                         cx,
                         |pane, cx| pane.deploy_split_menu(cx),
@@ -292,6 +292,7 @@ impl Pane {
                         } else {
                             "icons/maximize_8.svg"
                         },
+                        pane.is_zoomed(),
                         Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))),
                         cx,
                         move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
@@ -306,15 +307,6 @@ impl Pane {
         &self.workspace
     }
 
-    pub fn is_active(&self) -> bool {
-        self.is_active
-    }
-
-    pub fn set_active(&mut self, is_active: bool, cx: &mut ViewContext<Self>) {
-        self.is_active = is_active;
-        cx.notify();
-    }
-
     pub fn has_focus(&self) -> bool {
         self.has_focus
     }
@@ -547,6 +539,11 @@ impl Pane {
     }
 
     pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+        // Potentially warn the user of the new keybinding
+        let workspace_handle = self.workspace().clone();
+        cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
+            .detach();
+
         if self.zoomed {
             cx.emit(Event::ZoomOut);
         } else if !self.items.is_empty() {
@@ -1005,7 +1002,7 @@ impl Pane {
                 AnchorCorner::TopRight,
                 vec![
                     ContextMenuItem::action("New File", NewFile),
-                    ContextMenuItem::action("New Terminal", NewTerminal),
+                    ContextMenuItem::action("New Terminal", NewCenterTerminal),
                     ContextMenuItem::action("New Search", NewSearch),
                 ],
                 cx,
@@ -1129,7 +1126,7 @@ impl Pane {
             None
         };
 
-        let pane_active = self.is_active;
+        let pane_active = self.has_focus;
 
         enum Tabs {}
         let mut row = Flex::row().scrollable::<Tabs>(1, autoscroll, cx);
@@ -1412,6 +1409,7 @@ impl Pane {
     pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
         index: usize,
         icon: &'static str,
+        active: bool,
         tooltip: Option<(String, Option<Box<dyn Action>>)>,
         cx: &mut ViewContext<Pane>,
         on_click: F,
@@ -1421,7 +1419,7 @@ impl Pane {
 
         let mut button = MouseEventHandler::<TabBarButton, _>::new(index, cx, |mouse_state, cx| {
             let theme = &settings::get::<ThemeSettings>(cx).theme.workspace.tab_bar;
-            let style = theme.pane_button.style_for(mouse_state, false);
+            let style = theme.pane_button.style_for(mouse_state, active);
             Svg::new(icon)
                 .with_color(style.color)
                 .constrained()
@@ -1508,7 +1506,7 @@ impl View for Pane {
                         let mut tab_row = Flex::row()
                             .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs"));
 
-                        if self.is_active {
+                        if self.has_focus {
                             let render_tab_bar_buttons = self.render_tab_bar_buttons.clone();
                             tab_row.add_child(
                                 (render_tab_bar_buttons)(self, cx)
@@ -1599,6 +1597,7 @@ impl View for Pane {
         if !self.has_focus {
             self.has_focus = true;
             cx.emit(Event::Focus);
+            cx.notify();
         }
 
         self.toolbar.update(cx, |toolbar, cx| {
@@ -1633,6 +1632,7 @@ impl View for Pane {
         self.toolbar.update(cx, |toolbar, cx| {
             toolbar.pane_focus_update(false, cx);
         });
+        cx.notify();
     }
 
     fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {

crates/workspace/src/workspace.rs πŸ”—

@@ -53,13 +53,14 @@ use std::{
     cmp, env,
     future::Future,
     path::{Path, PathBuf},
+    rc::Rc,
     str,
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
 };
 
 use crate::{
-    notifications::simple_message_notification::MessageNotification,
+    notifications::{simple_message_notification::MessageNotification, NotificationTracker},
     persistence::model::{
         DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
     },
@@ -80,7 +81,7 @@ use serde::Deserialize;
 use shared_screen::SharedScreen;
 use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
-use theme::Theme;
+use theme::{Theme, ThemeSettings};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
 use util::{async_iife, paths, ResultExt};
 pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
@@ -103,24 +104,6 @@ pub trait Modal: View {
 #[derive(Clone, PartialEq)]
 pub struct RemoveWorktreeFromProject(pub WorktreeId);
 
-#[derive(Copy, Clone, Default, Deserialize, PartialEq)]
-pub struct ToggleLeftDock {
-    #[serde(default = "default_true")]
-    pub focus: bool,
-}
-
-#[derive(Copy, Clone, Default, Deserialize, PartialEq)]
-pub struct ToggleBottomDock {
-    #[serde(default = "default_true")]
-    pub focus: bool,
-}
-
-#[derive(Copy, Clone, Default, Deserialize, PartialEq)]
-pub struct ToggleRightDock {
-    #[serde(default = "default_true")]
-    pub focus: bool,
-}
-
 actions!(
     workspace,
     [
@@ -137,22 +120,21 @@ actions!(
         ActivateNextPane,
         FollowNextCollaborator,
         NewTerminal,
+        NewCenterTerminal,
         ToggleTerminalFocus,
         NewSearch,
         Feedback,
         Restart,
         Welcome,
         ToggleZoom,
+        ToggleLeftDock,
+        ToggleRightDock,
+        ToggleBottomDock,
     ]
 );
 
 actions!(zed, [OpenSettings]);
 
-impl_actions!(
-    workspace,
-    [ToggleLeftDock, ToggleBottomDock, ToggleRightDock]
-);
-
 #[derive(Clone, PartialEq)]
 pub struct OpenPaths {
     pub paths: Vec<PathBuf>,
@@ -268,14 +250,14 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
         workspace.activate_next_pane(cx)
     });
-    cx.add_action(|workspace: &mut Workspace, action: &ToggleLeftDock, cx| {
-        workspace.toggle_dock(DockPosition::Left, action.focus, cx);
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
+        workspace.toggle_dock(DockPosition::Left, cx);
     });
-    cx.add_action(|workspace: &mut Workspace, action: &ToggleRightDock, cx| {
-        workspace.toggle_dock(DockPosition::Right, action.focus, cx);
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
+        workspace.toggle_dock(DockPosition::Right, cx);
     });
-    cx.add_action(|workspace: &mut Workspace, action: &ToggleBottomDock, cx| {
-        workspace.toggle_dock(DockPosition::Bottom, action.focus, cx);
+    cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
+        workspace.toggle_dock(DockPosition::Bottom, cx);
     });
     cx.add_action(Workspace::activate_pane_at_index);
     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
@@ -498,6 +480,7 @@ pub struct Workspace {
     remote_entity_subscription: Option<client::Subscription>,
     modal: Option<AnyViewHandle>,
     zoomed: Option<AnyWeakViewHandle>,
+    zoomed_position: Option<DockPosition>,
     center: PaneGroup,
     left_dock: ViewHandle<Dock>,
     bottom_dock: ViewHandle<Dock>,
@@ -703,6 +686,7 @@ impl Workspace {
             weak_self: weak_handle.clone(),
             modal: None,
             zoomed: None,
+            zoomed_position: None,
             center: PaneGroup::new(center_pane.clone()),
             panes: vec![center_pane.clone()],
             panes_by_item: Default::default(),
@@ -901,10 +885,15 @@ impl Workspace {
 
                         was_visible = dock.is_open()
                             && dock
-                                .active_panel()
+                                .visible_panel()
                                 .map_or(false, |active_panel| active_panel.id() == panel.id());
                         dock.remove_panel(&panel, cx);
                     });
+
+                    if panel.is_zoomed(cx) {
+                        this.zoomed_position = Some(new_position);
+                    }
+
                     dock = match panel.read(cx).position(cx) {
                         DockPosition::Left => &this.left_dock,
                         DockPosition::Bottom => &this.bottom_dock,
@@ -919,18 +908,27 @@ impl Workspace {
                         }
                     });
                 } else if T::should_zoom_in_on_event(event) {
-                    this.zoom_out(cx);
                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
                     if panel.has_focus(cx) {
                         this.zoomed = Some(panel.downgrade().into_any());
+                        this.zoomed_position = Some(panel.read(cx).position(cx));
                     }
                 } else if T::should_zoom_out_on_event(event) {
-                    this.zoom_out(cx);
+                    dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
+                    if this.zoomed_position == Some(prev_position) {
+                        this.zoomed = None;
+                        this.zoomed_position = None;
+                    }
+                    cx.notify();
                 } else if T::is_focus_event(event) {
+                    let position = panel.read(cx).position(cx);
+                    this.dismiss_zoomed_items_to_reveal(Some(position), cx);
                     if panel.is_zoomed(cx) {
                         this.zoomed = Some(panel.downgrade().into_any());
+                        this.zoomed_position = Some(position);
                     } else {
                         this.zoomed = None;
+                        this.zoomed_position = None;
                     }
                     cx.notify();
                 }
@@ -976,9 +974,8 @@ impl Workspace {
                     let timestamp = entry.timestamp;
                     match history.entry(project_path) {
                         hash_map::Entry::Occupied(mut entry) => {
-                            let (old_fs_path, old_timestamp) = entry.get();
+                            let (_, old_timestamp) = entry.get();
                             if &timestamp > old_timestamp {
-                                assert_eq!(&fs_path, old_fs_path, "Inconsistent nav history");
                                 entry.insert((fs_path, timestamp));
                             }
                         }
@@ -1593,89 +1590,98 @@ impl Workspace {
         }
     }
 
-    pub fn toggle_dock(
-        &mut self,
-        dock_side: DockPosition,
-        focus: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
+    pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
         let dock = match dock_side {
             DockPosition::Left => &self.left_dock,
             DockPosition::Bottom => &self.bottom_dock,
             DockPosition::Right => &self.right_dock,
         };
+        let mut focus_center = false;
+        let mut reveal_dock = false;
         dock.update(cx, |dock, cx| {
-            let open = !dock.is_open();
-            dock.set_open(open, cx);
+            let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
+            let was_visible = dock.is_open() && !other_is_zoomed;
+            dock.set_open(!was_visible, cx);
+
+            if let Some(active_panel) = dock.active_panel() {
+                if was_visible {
+                    if active_panel.has_focus(cx) {
+                        focus_center = true;
+                    }
+                } else {
+                    if active_panel.is_zoomed(cx) {
+                        cx.focus(active_panel.as_any());
+                    }
+                    reveal_dock = true;
+                }
+            }
         });
 
-        if dock.read(cx).is_open() && focus {
-            cx.focus(dock);
-        } else {
-            cx.focus_self();
+        if reveal_dock {
+            self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
         }
-        cx.notify();
-        self.serialize_workspace(cx);
-    }
-
-    pub fn toggle_panel(
-        &mut self,
-        position: DockPosition,
-        panel_index: usize,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let dock = match position {
-            DockPosition::Left => &mut self.left_dock,
-            DockPosition::Bottom => &mut self.bottom_dock,
-            DockPosition::Right => &mut self.right_dock,
-        };
-        let active_item = dock.update(cx, move |dock, cx| {
-            if dock.is_open() && dock.active_panel_index() == panel_index {
-                dock.set_open(false, cx);
-                None
-            } else {
-                dock.set_open(true, cx);
-                dock.activate_panel(panel_index, cx);
-                dock.active_panel().cloned()
-            }
-        });
 
-        if let Some(active_item) = active_item {
-            if active_item.has_focus(cx) {
-                cx.focus_self();
-            } else {
-                cx.focus(active_item.as_any());
-            }
-        } else {
+        if focus_center {
             cx.focus_self();
         }
 
+        cx.notify();
         self.serialize_workspace(cx);
+    }
 
-        cx.notify();
+    /// Transfer focus to the panel of the given type.
+    pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
+        self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
+            .as_any()
+            .clone()
+            .downcast()
     }
 
+    /// Focus the panel of the given type if it isn't already focused. If it is
+    /// already focused, then transfer focus back to the workspace center.
     pub fn toggle_panel_focus<T: Panel>(&mut self, cx: &mut ViewContext<Self>) {
+        self.focus_or_unfocus_panel::<T>(cx, |panel, cx| !panel.has_focus(cx));
+    }
+
+    /// Focus or unfocus the given panel type, depending on the given callback.
+    fn focus_or_unfocus_panel<T: Panel>(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+        should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext<Dock>) -> bool,
+    ) -> Option<Rc<dyn PanelHandle>> {
         for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
             if let Some(panel_index) = dock.read(cx).panel_index_for_type::<T>() {
-                let active_item = dock.update(cx, |dock, cx| {
-                    dock.set_open(true, cx);
+                let mut focus_center = false;
+                let mut reveal_dock = false;
+                let panel = dock.update(cx, |dock, cx| {
                     dock.activate_panel(panel_index, cx);
-                    dock.active_panel().cloned()
-                });
-                if let Some(active_item) = active_item {
-                    if active_item.has_focus(cx) {
-                        cx.focus_self();
-                    } else {
-                        cx.focus(active_item.as_any());
+
+                    let panel = dock.active_panel().cloned();
+                    if let Some(panel) = panel.as_ref() {
+                        if should_focus(&**panel, cx) {
+                            dock.set_open(true, cx);
+                            cx.focus(panel.as_any());
+                            reveal_dock = true;
+                        } else {
+                            // if panel.is_zoomed(cx) {
+                            //     dock.set_open(false, cx);
+                            // }
+                            focus_center = true;
+                        }
                     }
+                    panel
+                });
+
+                if focus_center {
+                    cx.focus_self();
                 }
 
                 self.serialize_workspace(cx);
                 cx.notify();
-                break;
+                return panel;
             }
         }
+        None
     }
 
     pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<ViewHandle<T>> {
@@ -1697,6 +1703,46 @@ impl Workspace {
         self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx));
         self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx));
         self.zoomed = None;
+        self.zoomed_position = None;
+
+        cx.notify();
+    }
+
+    fn dismiss_zoomed_items_to_reveal(
+        &mut self,
+        dock_to_reveal: Option<DockPosition>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        // If a center pane is zoomed, unzoom it.
+        for pane in &self.panes {
+            if pane != &self.active_pane || dock_to_reveal.is_some() {
+                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
+            }
+        }
+
+        // If another dock is zoomed, hide it.
+        let mut focus_center = false;
+        for dock in [&self.left_dock, &self.right_dock, &self.bottom_dock] {
+            dock.update(cx, |dock, cx| {
+                if Some(dock.position()) != dock_to_reveal {
+                    if let Some(panel) = dock.active_panel() {
+                        if panel.is_zoomed(cx) {
+                            focus_center |= panel.has_focus(cx);
+                            dock.set_open(false, cx);
+                        }
+                    }
+                }
+            });
+        }
+
+        if focus_center {
+            cx.focus_self();
+        }
+
+        if self.zoomed_position != dock_to_reveal {
+            self.zoomed = None;
+            self.zoomed_position = None;
+        }
 
         cx.notify();
     }
@@ -1896,11 +1942,7 @@ impl Workspace {
 
     fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
         if self.active_pane != pane {
-            self.active_pane
-                .update(cx, |pane, cx| pane.set_active(false, cx));
             self.active_pane = pane.clone();
-            self.active_pane
-                .update(cx, |pane, cx| pane.set_active(true, cx));
             self.status_bar.update(cx, |status_bar, cx| {
                 status_bar.set_active_pane(&self.active_pane, cx);
             });
@@ -1908,11 +1950,13 @@ impl Workspace {
             self.last_active_center_pane = Some(pane.downgrade());
         }
 
+        self.dismiss_zoomed_items_to_reveal(None, cx);
         if pane.read(cx).is_zoomed() {
             self.zoomed = Some(pane.downgrade().into_any());
         } else {
             self.zoomed = None;
         }
+        self.zoomed_position = None;
 
         self.update_followers(
             proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView {
@@ -1968,15 +2012,21 @@ impl Workspace {
             }
             pane::Event::ZoomIn => {
                 if pane == self.active_pane {
-                    self.zoom_out(cx);
                     pane.update(cx, |pane, cx| pane.set_zoomed(true, cx));
                     if pane.read(cx).has_focus() {
                         self.zoomed = Some(pane.downgrade().into_any());
+                        self.zoomed_position = None;
                     }
                     cx.notify();
                 }
             }
-            pane::Event::ZoomOut => self.zoom_out(cx),
+            pane::Event::ZoomOut => {
+                pane.update(cx, |pane, cx| pane.set_zoomed(false, cx));
+                if self.zoomed_position.is_none() {
+                    self.zoomed = None;
+                }
+                cx.notify();
+            }
         }
 
         self.serialize_workspace(cx);
@@ -2817,7 +2867,7 @@ impl Workspace {
                             })
                         })
                         .collect::<Vec<_>>(),
-                    pane.is_active(),
+                    pane.has_focus(),
                 )
             };
 
@@ -2845,7 +2895,7 @@ impl Workspace {
         fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure {
             let left_dock = this.left_dock.read(cx);
             let left_visible = left_dock.is_open();
-            let left_active_panel = left_dock.active_panel().and_then(|panel| {
+            let left_active_panel = left_dock.visible_panel().and_then(|panel| {
                 Some(
                     cx.view_ui_name(panel.as_any().window_id(), panel.id())?
                         .to_string(),
@@ -2854,7 +2904,7 @@ impl Workspace {
 
             let right_dock = this.right_dock.read(cx);
             let right_visible = right_dock.is_open();
-            let right_active_panel = right_dock.active_panel().and_then(|panel| {
+            let right_active_panel = right_dock.visible_panel().and_then(|panel| {
                 Some(
                     cx.view_ui_name(panel.as_any().window_id(), panel.id())?
                         .to_string(),
@@ -2863,7 +2913,7 @@ impl Workspace {
 
             let bottom_dock = this.bottom_dock.read(cx);
             let bottom_visible = bottom_dock.is_open();
-            let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| {
+            let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| {
                 Some(
                     cx.view_ui_name(panel.as_any().window_id(), panel.id())?
                         .to_string(),
@@ -3045,7 +3095,7 @@ impl Workspace {
             DockPosition::Right => &self.right_dock,
             DockPosition::Bottom => &self.bottom_dock,
         };
-        let active_panel = dock.read(cx).active_panel()?;
+        let active_panel = dock.read(cx).visible_panel()?;
         let element = if Some(active_panel.id()) == self.zoomed.as_ref().map(|zoomed| zoomed.id()) {
             dock.read(cx).render_placeholder(cx)
         } else {
@@ -3159,6 +3209,87 @@ async fn open_items(
     opened_items
 }
 
+fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
+    const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
+    const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
+    const MESSAGE_ID: usize = 2;
+
+    if workspace
+        .read_with(cx, |workspace, cx| {
+            workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
+        })
+        .unwrap_or(false)
+    {
+        return;
+    }
+
+    if db::kvp::KEY_VALUE_STORE
+        .read_kvp(NEW_DOCK_HINT_KEY)
+        .ok()
+        .flatten()
+        .is_some()
+    {
+        if !workspace
+            .read_with(cx, |workspace, cx| {
+                workspace.has_shown_notification_once::<MessageNotification>(MESSAGE_ID, cx)
+            })
+            .unwrap_or(false)
+        {
+            cx.update(|cx| {
+                cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
+                    let entry = tracker
+                        .entry(TypeId::of::<MessageNotification>())
+                        .or_default();
+                    if !entry.contains(&MESSAGE_ID) {
+                        entry.push(MESSAGE_ID);
+                    }
+                });
+            });
+        }
+
+        return;
+    }
+
+    cx.spawn(|_| async move {
+        db::kvp::KEY_VALUE_STORE
+            .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
+            .await
+            .ok();
+    })
+    .detach();
+
+    workspace
+        .update(cx, |workspace, cx| {
+            workspace.show_notification_once(2, cx, |cx| {
+                cx.add_view(|_| {
+                    MessageNotification::new_element(|text, _| {
+                        Text::new(
+                            "Looking for the dock? Try ctrl-`!\nshift-escape now zooms your pane.",
+                            text,
+                        )
+                        .with_custom_runs(vec![26..32, 34..46], |_, bounds, scene, cx| {
+                            let code_span_background_color = settings::get::<ThemeSettings>(cx)
+                                .theme
+                                .editor
+                                .document_highlight_read_background;
+
+                            scene.push_quad(gpui::Quad {
+                                bounds,
+                                background: Some(code_span_background_color),
+                                border: Default::default(),
+                                corner_radius: 2.0,
+                            })
+                        })
+                        .into_any()
+                    })
+                    .with_click_message("Read more about the new panel system")
+                    .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
+                })
+            })
+        })
+        .ok();
+}
+
 fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
     const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
 
@@ -3175,7 +3306,7 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
             } else {
                 let backup_path = (*db::BACKUP_DB_PATH).read();
                 if let Some(backup_path) = backup_path.clone() {
-                    workspace.show_notification_once(0, cx, move |cx| {
+                    workspace.show_notification_once(1, cx, move |cx| {
                         cx.add_view(move |_| {
                             MessageNotification::new(format!(
                                 "Database file was corrupted. Old database backed up to {}",
@@ -3246,10 +3377,44 @@ impl View for Workspace {
                                     .with_children(self.zoomed.as_ref().and_then(|zoomed| {
                                         enum ZoomBackground {}
                                         let zoomed = zoomed.upgrade(cx)?;
+
+                                        let mut foreground_style =
+                                            theme.workspace.zoomed_pane_foreground;
+                                        if let Some(zoomed_dock_position) = self.zoomed_position {
+                                            foreground_style =
+                                                theme.workspace.zoomed_panel_foreground;
+                                            let margin = foreground_style.margin.top;
+                                            let border = foreground_style.border.top;
+
+                                            // Only include a margin and border on the opposite side.
+                                            foreground_style.margin.top = 0.;
+                                            foreground_style.margin.left = 0.;
+                                            foreground_style.margin.bottom = 0.;
+                                            foreground_style.margin.right = 0.;
+                                            foreground_style.border.top = false;
+                                            foreground_style.border.left = false;
+                                            foreground_style.border.bottom = false;
+                                            foreground_style.border.right = false;
+                                            match zoomed_dock_position {
+                                                DockPosition::Left => {
+                                                    foreground_style.margin.right = margin;
+                                                    foreground_style.border.right = border;
+                                                }
+                                                DockPosition::Right => {
+                                                    foreground_style.margin.left = margin;
+                                                    foreground_style.border.left = border;
+                                                }
+                                                DockPosition::Bottom => {
+                                                    foreground_style.margin.top = margin;
+                                                    foreground_style.border.top = border;
+                                                }
+                                            }
+                                        }
+
                                         Some(
                                             ChildView::new(&zoomed, cx)
                                                 .contained()
-                                                .with_style(theme.workspace.zoomed_foreground)
+                                                .with_style(foreground_style)
                                                 .aligned()
                                                 .contained()
                                                 .with_style(theme.workspace.zoomed_background)
@@ -3599,10 +3764,6 @@ fn parse_pixel_position_env_var(value: &str) -> Option<Vector2F> {
     Some(vec2f(width as f32, height as f32))
 }
 
-fn default_true() -> bool {
-    true
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -4181,6 +4342,153 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) {
+        init_test(cx);
+        let fs = FakeFs::new(cx.background());
+
+        let project = Project::test(fs, [], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
+
+        let panel = workspace.update(cx, |workspace, cx| {
+            let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right));
+            workspace.add_panel(panel.clone(), cx);
+
+            workspace
+                .right_dock()
+                .update(cx, |right_dock, cx| right_dock.set_open(true, cx));
+
+            panel
+        });
+
+        let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+        pane.update(cx, |pane, cx| {
+            let item = cx.add_view(|_| TestItem::new());
+            pane.add_item(Box::new(item), true, true, None, cx);
+        });
+
+        // Transfer focus from center to panel
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(cx);
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(!panel.is_zoomed(cx));
+            assert!(panel.has_focus(cx));
+        });
+
+        // Transfer focus from panel to center
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(cx);
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(!panel.is_zoomed(cx));
+            assert!(!panel.has_focus(cx));
+        });
+
+        // Close the dock
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx);
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(!workspace.right_dock().read(cx).is_open());
+            assert!(!panel.is_zoomed(cx));
+            assert!(!panel.has_focus(cx));
+        });
+
+        // Open the dock
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx);
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(!panel.is_zoomed(cx));
+            assert!(!panel.has_focus(cx));
+        });
+
+        // Focus and zoom panel
+        panel.update(cx, |panel, cx| {
+            cx.focus_self();
+            panel.set_zoomed(true, cx)
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(panel.has_focus(cx));
+        });
+
+        // Transfer focus to the center closes the dock
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(cx);
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(!workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(!panel.has_focus(cx));
+        });
+
+        // Transfering focus back to the panel keeps it zoomed
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_panel_focus::<TestPanel>(cx);
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(panel.has_focus(cx));
+        });
+
+        // Close the dock while it is zoomed
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx)
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(!workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(workspace.zoomed.is_none());
+            assert!(!panel.has_focus(cx));
+        });
+
+        // Opening the dock, when it's zoomed, retains focus
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx)
+        });
+
+        workspace.read_with(cx, |workspace, cx| {
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(panel.is_zoomed(cx));
+            assert!(workspace.zoomed.is_some());
+            assert!(panel.has_focus(cx));
+        });
+
+        // Unzoom and close the panel, zoom the active pane.
+        panel.update(cx, |panel, cx| panel.set_zoomed(false, cx));
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx)
+        });
+        pane.update(cx, |pane, cx| pane.toggle_zoom(&Default::default(), cx));
+
+        // Opening a dock unzooms the pane.
+        workspace.update(cx, |workspace, cx| {
+            workspace.toggle_dock(DockPosition::Right, cx)
+        });
+        workspace.read_with(cx, |workspace, cx| {
+            let pane = pane.read(cx);
+            assert!(!pane.is_zoomed());
+            assert!(pane.has_focus());
+            assert!(workspace.right_dock().read(cx).is_open());
+            assert!(workspace.zoomed.is_none());
+        });
+    }
+
     #[gpui::test]
     async fn test_panels(cx: &mut gpui::TestAppContext) {
         init_test(cx);
@@ -4204,7 +4512,7 @@ mod tests {
 
             let left_dock = workspace.left_dock();
             assert_eq!(
-                left_dock.read(cx).active_panel().unwrap().id(),
+                left_dock.read(cx).visible_panel().unwrap().id(),
                 panel_1.id()
             );
             assert_eq!(
@@ -4214,7 +4522,12 @@ mod tests {
 
             left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
             assert_eq!(
-                workspace.right_dock().read(cx).active_panel().unwrap().id(),
+                workspace
+                    .right_dock()
+                    .read(cx)
+                    .visible_panel()
+                    .unwrap()
+                    .id(),
                 panel_2.id()
             );
 
@@ -4230,10 +4543,10 @@ mod tests {
             // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right.
             // Since it was the only panel on the left, the left dock should now be closed.
             assert!(!workspace.left_dock().read(cx).is_open());
-            assert!(workspace.left_dock().read(cx).active_panel().is_none());
+            assert!(workspace.left_dock().read(cx).visible_panel().is_none());
             let right_dock = workspace.right_dock();
             assert_eq!(
-                right_dock.read(cx).active_panel().unwrap().id(),
+                right_dock.read(cx).visible_panel().unwrap().id(),
                 panel_1.id()
             );
             assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
@@ -4248,7 +4561,12 @@ mod tests {
             // And the right dock is unaffected in it's displaying of panel_1
             assert!(workspace.right_dock().read(cx).is_open());
             assert_eq!(
-                workspace.right_dock().read(cx).active_panel().unwrap().id(),
+                workspace
+                    .right_dock()
+                    .read(cx)
+                    .visible_panel()
+                    .unwrap()
+                    .id(),
                 panel_1.id()
             );
         });
@@ -4263,7 +4581,7 @@ mod tests {
             let left_dock = workspace.left_dock();
             assert!(left_dock.read(cx).is_open());
             assert_eq!(
-                left_dock.read(cx).active_panel().unwrap().id(),
+                left_dock.read(cx).visible_panel().unwrap().id(),
                 panel_1.id()
             );
             assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
@@ -4297,7 +4615,7 @@ mod tests {
             let left_dock = workspace.left_dock();
             assert!(left_dock.read(cx).is_open());
             assert_eq!(
-                left_dock.read(cx).active_panel().unwrap().id(),
+                left_dock.read(cx).visible_panel().unwrap().id(),
                 panel_1.id()
             );
             assert!(panel_1.is_focused(cx));
@@ -4311,7 +4629,7 @@ mod tests {
             let left_dock = workspace.left_dock();
             assert!(left_dock.read(cx).is_open());
             assert_eq!(
-                left_dock.read(cx).active_panel().unwrap().id(),
+                left_dock.read(cx).visible_panel().unwrap().id(),
                 panel_1.id()
             );
         });
@@ -4320,6 +4638,14 @@ mod tests {
         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn));
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+            assert_eq!(workspace.zoomed_position, Some(DockPosition::Left));
+        });
+
+        // Move panel to another dock while it is zoomed
+        panel_1.update(cx, |panel, cx| panel.set_position(DockPosition::Right, cx));
+        workspace.read_with(cx, |workspace, _| {
+            assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
         });
 
         // If focus is transferred to another view that's not a panel or another pane, we still show
@@ -4328,12 +4654,14 @@ mod tests {
         focus_receiver.update(cx, |_, cx| cx.focus_self());
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
         });
 
         // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed.
         workspace.update(cx, |_, cx| cx.focus_self());
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.zoomed, None);
+            assert_eq!(workspace.zoomed_position, None);
         });
 
         // If focus is transferred again to another view that's not a panel or a pane, we won't
@@ -4341,18 +4669,21 @@ mod tests {
         focus_receiver.update(cx, |_, cx| cx.focus_self());
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.zoomed, None);
+            assert_eq!(workspace.zoomed_position, None);
         });
 
         // When focus is transferred back to the panel, it is zoomed again.
         panel_1.update(cx, |_, cx| cx.focus_self());
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any()));
+            assert_eq!(workspace.zoomed_position, Some(DockPosition::Right));
         });
 
         // Emitting a ZoomOut event unzooms the panel.
         panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut));
         workspace.read_with(cx, |workspace, _| {
             assert_eq!(workspace.zoomed, None);
+            assert_eq!(workspace.zoomed_position, None);
         });
 
         // Emit closed event on panel 1, which is active
@@ -4360,8 +4691,8 @@ mod tests {
 
         // Now the left dock is closed, because panel_1 was the active panel
         workspace.read_with(cx, |workspace, cx| {
-            let left_dock = workspace.left_dock();
-            assert!(!left_dock.read(cx).is_open());
+            let right_dock = workspace.right_dock();
+            assert!(!right_dock.read(cx).is_open());
         });
     }
 

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.89.0"
+version = "0.90.0"
 publish = false
 
 [lib]

crates/zed/src/languages/json.rs πŸ”—

@@ -135,7 +135,10 @@ impl LspAdapter for JsonLspAdapter {
                     },
                     "schemas": [
                         {
-                            "fileMatch": [schema_file_match(&paths::SETTINGS)],
+                            "fileMatch": [
+                                schema_file_match(&paths::SETTINGS),
+                                &*paths::LOCAL_SETTINGS_RELATIVE_PATH,
+                            ],
                             "schema": settings_schema,
                         },
                         {

crates/zed/src/languages/yaml.rs πŸ”—

@@ -3,7 +3,7 @@ use async_trait::async_trait;
 use futures::{future::BoxFuture, FutureExt, StreamExt};
 use gpui::AppContext;
 use language::{
-    language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
+    language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
 };
 use node_runtime::NodeRuntime;
 use serde_json::Value;
@@ -101,13 +101,16 @@ impl LspAdapter for YamlLspAdapter {
     }
 
     fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
+        let tab_size = all_language_settings(None, cx)
+            .language(Some("YAML"))
+            .tab_size;
         Some(
             future::ready(serde_json::json!({
                 "yaml": {
                     "keyOrdering": false
                 },
                 "[yaml]": {
-                    "editor.tabSize": language_settings(Some("YAML"), cx).tab_size,
+                    "editor.tabSize": tab_size,
                 }
             }))
             .boxed(),

crates/zed/src/main.rs πŸ”—

@@ -41,7 +41,7 @@ use std::{
         Arc, Weak,
     },
     thread,
-    time::Duration,
+    time::{Duration, SystemTime, UNIX_EPOCH},
 };
 use sum_tree::Bias;
 use terminal_view::{get_working_directory, TerminalSettings, TerminalView};
@@ -376,6 +376,7 @@ struct Panic {
     backtrace: Vec<String>,
     // TODO
     // stripped_backtrace: String,
+    time: u128,
 }
 
 #[derive(Serialize)]
@@ -413,6 +414,10 @@ fn init_panic_hook(app_version: String) {
                 .map(|line| line.to_string())
                 .collect(),
             // modified_backtrace: None,
+            time: SystemTime::now()
+                .duration_since(UNIX_EPOCH)
+                .unwrap()
+                .as_millis(),
         };
 
         if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() {

crates/zed/src/menus.rs πŸ”—

@@ -89,18 +89,9 @@ pub fn menus() -> Vec<Menu<'static>> {
                 MenuItem::action("Zoom Out", super::DecreaseBufferFontSize),
                 MenuItem::action("Reset Zoom", super::ResetBufferFontSize),
                 MenuItem::separator(),
-                MenuItem::action(
-                    "Toggle Left Dock",
-                    workspace::ToggleLeftDock { focus: false },
-                ),
-                MenuItem::action(
-                    "Toggle Right Dock",
-                    workspace::ToggleRightDock { focus: false },
-                ),
-                MenuItem::action(
-                    "Toggle Bottom Dock",
-                    workspace::ToggleBottomDock { focus: false },
-                ),
+                MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock),
+                MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock),
+                MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock),
                 MenuItem::submenu(Menu {
                     name: "Editor Layout",
                     items: vec![

crates/zed/src/zed.rs πŸ”—

@@ -357,7 +357,7 @@ pub fn initialize_workspace(
                             .map_or(false, |entry| entry.is_dir())
                     })
             {
-                workspace.toggle_dock(project_panel_position, false, cx);
+                workspace.toggle_dock(project_panel_position, cx);
             }
 
             workspace.add_panel(terminal_panel, cx);

styles/src/buildLicenses.ts πŸ”—

@@ -1,11 +1,9 @@
 import * as fs from "fs"
 import toml from "toml"
 import { schemeMeta } from "./colorSchemes"
-import { Meta, Verification } from "./themes/common/colorScheme"
-import https from "https"
-import crypto from "crypto"
+import { MetaAndLicense } from "./themes/common/colorScheme"
 
-const accepted_licenses_file = `${__dirname}/../../script/licenses/zed-licenses.toml`
+const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml`
 
 // Use the cargo-about configuration file as the source of truth for supported licenses.
 function parseAcceptedToml(file: string): string[] {
@@ -20,8 +18,11 @@ function parseAcceptedToml(file: string): string[] {
     return obj.accepted
 }
 
-function checkLicenses(schemeMeta: Meta[], licenses: string[]) {
-    for (let meta of schemeMeta) {
+function checkLicenses(
+    schemeMetaWithLicense: MetaAndLicense[],
+    licenses: string[]
+) {
+    for (const { meta } of schemeMetaWithLicense) {
         // FIXME: Add support for conjuctions and conditions
         if (licenses.indexOf(meta.license.SPDX) < 0) {
             throw Error(
@@ -31,62 +32,23 @@ function checkLicenses(schemeMeta: Meta[], licenses: string[]) {
     }
 }
 
-function getLicenseText(
-    schemeMeta: Meta[],
-    callback: (meta: Meta, license_text: string) => void
-) {
-    for (let meta of schemeMeta) {
-        if (typeof meta.license.license_text == "string") {
-            callback(meta, meta.license.license_text)
-        } else {
-            let license_text_obj: Verification = meta.license.license_text
-            // The following copied from the example code on nodejs.org:
-            // https://nodejs.org/api/http.html#httpgetoptions-callback
-            https
-                .get(license_text_obj.https_url, (res) => {
-                    const { statusCode } = res
-
-                    if (statusCode < 200 || statusCode >= 300) {
-                        throw new Error(
-                            `Failed to fetch license for: ${meta.name}, Status Code: ${statusCode}`
-                        )
-                    }
-
-                    res.setEncoding("utf8")
-                    let rawData = ""
-                    res.on("data", (chunk) => {
-                        rawData += chunk
-                    })
-                    res.on("end", () => {
-                        const hash = crypto
-                            .createHash("sha256")
-                            .update(rawData)
-                            .digest("hex")
-                        if (license_text_obj.license_checksum == hash) {
-                            callback(meta, rawData)
-                        } else {
-                            throw Error(
-                                `Checksum for ${meta.name} did not match file downloaded from ${license_text_obj.https_url}`
-                            )
-                        }
-                    })
-                })
-                .on("error", (e) => {
-                    throw e
-                })
-        }
+function generateLicenseFile(schemeMetaWithLicense: MetaAndLicense[]) {
+    for (const { meta, licenseFile } of schemeMetaWithLicense) {
+        const licenseText = fs.readFileSync(licenseFile).toString()
+        writeLicense(meta.name, meta.url, licenseText)
     }
 }
 
-function writeLicense(schemeMeta: Meta, text: String) {
+function writeLicense(
+    themeName: string,
+    themeUrl: string,
+    licenseText: String
+) {
     process.stdout.write(
-        `## [${schemeMeta.name}](${schemeMeta.url})\n\n${text}\n********************************************************************************\n\n`
+        `## [${themeName}](${themeUrl})\n\n${licenseText}\n********************************************************************************\n\n`
     )
 }
 
-const accepted_licenses = parseAcceptedToml(accepted_licenses_file)
-checkLicenses(schemeMeta, accepted_licenses)
-
-getLicenseText(schemeMeta, (meta, text) => {
-    writeLicense(meta, text)
-})
+const acceptedLicenses = parseAcceptedToml(ACCEPTED_LICENSES_FILE)
+checkLicenses(schemeMeta, acceptedLicenses)
+generateLicenseFile(schemeMeta)

styles/src/buildThemes.ts πŸ”—

@@ -1,7 +1,7 @@
 import * as fs from "fs"
 import { tmpdir } from "os"
 import * as path from "path"
-import colorSchemes, { staffColorSchemes } from "./colorSchemes"
+import { colorSchemes, staffColorSchemes } from "./colorSchemes"
 import app from "./styleTree/app"
 import { ColorScheme } from "./themes/common/colorScheme"
 import snakeCase from "./utils/snakeCase"

styles/src/colorSchemes.ts πŸ”—

@@ -1,54 +1,79 @@
 import fs from "fs"
 import path from "path"
-import { ColorScheme, Meta } from "./themes/common/colorScheme"
+import { ColorScheme, MetaAndLicense } from "./themes/common/colorScheme"
 
-const colorSchemes: ColorScheme[] = []
-export default colorSchemes
+const THEMES_DIRECTORY = path.resolve(`${__dirname}/themes`)
+const STAFF_DIRECTORY = path.resolve(`${__dirname}/themes/staff`)
+const IGNORE_ITEMS = ["staff", "common", "common.ts"]
+const ACCEPT_EXTENSION = ".ts"
+const LICENSE_FILE_NAME = "LICENSE"
 
-const schemeMeta: Meta[] = []
-export { schemeMeta }
+function getAllTsFiles(directoryPath: string) {
+    const files = fs.readdirSync(directoryPath)
+    const fileList: string[] = []
 
-const staffColorSchemes: ColorScheme[] = []
-export { staffColorSchemes }
+    for (const file of files) {
+        if (!IGNORE_ITEMS.includes(file)) {
+            const filePath = path.join(directoryPath, file)
 
-const experimentalColorSchemes: ColorScheme[] = []
-export { experimentalColorSchemes }
-
-const themes_directory = path.resolve(`${__dirname}/themes`)
-
-function for_all_color_schemes_in(
-    themesPath: string,
-    callback: (module: any, path: string) => void
-) {
-    for (const fileName of fs.readdirSync(themesPath)) {
-        if (fileName == "template.ts") continue
-        const filePath = path.join(themesPath, fileName)
-
-        if (fs.statSync(filePath).isFile()) {
-            const colorScheme = require(filePath)
-            callback(colorScheme, path.basename(filePath))
+            if (fs.statSync(filePath).isDirectory()) {
+                fileList.push(...getAllTsFiles(filePath))
+            } else if (path.extname(file) === ACCEPT_EXTENSION) {
+                fileList.push(filePath)
+            }
         }
     }
+
+    return fileList
 }
 
-function fillColorSchemes(themesPath: string, colorSchemes: ColorScheme[]) {
-    for_all_color_schemes_in(themesPath, (colorScheme, _path) => {
+function getAllColorSchemes(directoryPath: string) {
+    const files = getAllTsFiles(directoryPath)
+    return files.map((filePath) => ({
+        colorScheme: require(filePath),
+        filePath,
+        fileName: path.basename(filePath),
+        licenseFile: `${path.dirname(filePath)}/${LICENSE_FILE_NAME}`,
+    }))
+}
+
+function getColorSchemes(directoryPath: string) {
+    const colorSchemes: ColorScheme[] = []
+
+    for (const { colorScheme } of getAllColorSchemes(directoryPath)) {
         if (colorScheme.dark) colorSchemes.push(colorScheme.dark)
-        if (colorScheme.light) colorSchemes.push(colorScheme.light)
-    })
+        else if (colorScheme.light) colorSchemes.push(colorScheme.light)
+    }
+
+    return colorSchemes
 }
 
-fillColorSchemes(themes_directory, colorSchemes)
-fillColorSchemes(path.resolve(`${themes_directory}/staff`), staffColorSchemes)
+function getMetaAndLicense(directoryPath: string) {
+    const meta: MetaAndLicense[] = []
+
+    for (const { colorScheme, filePath, licenseFile } of getAllColorSchemes(
+        directoryPath
+    )) {
+        const licenseExists = fs.existsSync(licenseFile)
+        if (!licenseExists) {
+            throw Error(
+                `Public theme should have a LICENSE file ${licenseFile}`
+            )
+        }
 
-function fillMeta(themesPath: string, meta: Meta[]) {
-    for_all_color_schemes_in(themesPath, (colorScheme, path) => {
-        if (colorScheme.meta) {
-            meta.push(colorScheme.meta)
-        } else {
-            throw Error(`Public theme ${path} must have a meta field`)
+        if (!colorScheme.meta) {
+            throw Error(`Public theme ${filePath} must have a meta field`)
         }
-    })
+
+        meta.push({
+            meta: colorScheme.meta,
+            licenseFile,
+        })
+    }
+
+    return meta
 }
 
-fillMeta(themes_directory, schemeMeta)
+export const colorSchemes = getColorSchemes(THEMES_DIRECTORY)
+export const staffColorSchemes = getColorSchemes(STAFF_DIRECTORY)
+export const schemeMeta = getMetaAndLicense(THEMES_DIRECTORY)

styles/src/styleTree/tabBar.ts πŸ”—

@@ -94,6 +94,9 @@ export default function tabBar(colorScheme: ColorScheme) {
             hover: {
                 color: foreground(layer, "hovered"),
             },
+            active: {
+                color: foreground(layer, "accent"),
+            }
         },
         paneButtonContainer: {
             background: tab.background,

styles/src/styleTree/workspace.ts πŸ”—

@@ -13,6 +13,7 @@ import tabBar from "./tabBar"
 
 export default function workspace(colorScheme: ColorScheme) {
     const layer = colorScheme.lowest
+    const isLight = colorScheme.isLight
     const itemSpacing = 8
     const titlebarButton = {
         cornerRadius: 6,
@@ -119,13 +120,19 @@ export default function workspace(colorScheme: ColorScheme) {
             cursor: "Arrow",
         },
         zoomedBackground: {
-            padding: 10,
             cursor: "Arrow",
-            background: withOpacity(background(colorScheme.lowest), 0.5)
+            background: isLight
+                ? withOpacity(background(colorScheme.lowest), 0.8)
+                : withOpacity(background(colorScheme.highest), 0.6)
         },
-        zoomedForeground: {
+        zoomedPaneForeground: {
+            margin: 16,
             shadow: colorScheme.modalShadow,
-            border: border(colorScheme.highest, { overlay: true }),
+            border: border(colorScheme.lowest, { overlay: true }),
+        },
+        zoomedPanelForeground: {
+            margin: 16,
+            border: border(colorScheme.lowest, { overlay: true }),
         },
         dock: {
             left: {

styles/src/themes/andromeda/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 <eliverlara@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/andromeda.ts β†’ styles/src/themes/andromeda/andromeda.ts πŸ”—

@@ -1,6 +1,6 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "Andromeda"
 
@@ -34,12 +34,6 @@ export const meta: Meta = {
     author: "EliverLara",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/EliverLara/Andromeda/master/LICENSE.md",
-            license_checksum:
-                "2f7886f1a05cefc2c26f5e49de1a39fa4466413c1ccb06fc80960e73f5ed4b89",
-        },
     },
     url: "https://github.com/EliverLara/Andromeda",
 }

styles/src/themes/atelier/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2023 Bram de Haan, http://atelierbramdehaan.nl
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/atelier-cave-dark.ts β†’ styles/src/themes/atelier/atelier-cave-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-cave-light.ts β†’ styles/src/themes/atelier/atelier-cave-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-dune-dark.ts β†’ styles/src/themes/atelier/atelier-dune-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-dune-light.ts β†’ styles/src/themes/atelier/atelier-dune-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-estuary-dark.ts β†’ styles/src/themes/atelier/atelier-estuary-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-estuary-light.ts β†’ styles/src/themes/atelier/atelier-estuary-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-forest-dark.ts β†’ styles/src/themes/atelier/atelier-forest-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-forest-light.ts β†’ styles/src/themes/atelier/atelier-forest-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-heath-dark.ts β†’ styles/src/themes/atelier/atelier-heath-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-heath-light.ts β†’ styles/src/themes/atelier/atelier-heath-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-lakeside-dark.ts β†’ styles/src/themes/atelier/atelier-lakeside-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-lakeside-light.ts β†’ styles/src/themes/atelier/atelier-lakeside-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-plateau-dark.ts β†’ styles/src/themes/atelier/atelier-plateau-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-plateau-light.ts β†’ styles/src/themes/atelier/atelier-plateau-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-savanna-dark.ts β†’ styles/src/themes/atelier/atelier-savanna-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-savanna-light.ts β†’ styles/src/themes/atelier/atelier-savanna-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-seaside-dark.ts β†’ styles/src/themes/atelier/atelier-seaside-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-seaside-light.ts β†’ styles/src/themes/atelier/atelier-seaside-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-sulphurpool-dark.ts β†’ styles/src/themes/atelier/atelier-sulphurpool-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/atelier-sulphurpool-light.ts β†’ styles/src/themes/atelier/atelier-sulphurpool-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
-import { metaCommon, name, buildSyntax, Variant } from "./common/atelier-common"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
+import { metaCommon, name, buildSyntax, Variant } from "./common"
 
 const variant: Variant = {
     meta: {

styles/src/themes/common/atelier-common.ts β†’ styles/src/themes/atelier/common.ts πŸ”—

@@ -1,4 +1,4 @@
-import { License, Meta, ThemeSyntax } from "./colorScheme"
+import { License, Meta, ThemeSyntax } from "../common/colorScheme"
 
 export interface Variant {
     meta: Meta
@@ -29,11 +29,6 @@ export const metaCommon: {
     author: "Bram de Haan (http://atelierbramdehaan.nl)",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url: "https://atelierbram.mit-license.org/license.txt",
-            license_checksum:
-                "f95ce526ef4e7eecf7a832bba0e3451cc1000f9ce63eb01ed6f64f8109f5d0a5",
-        },
     },
 }
 

styles/src/themes/ayu/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 Ike Ku
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/ayu-dark.ts β†’ styles/src/themes/ayu/ayu-dark.ts πŸ”—

@@ -1,5 +1,5 @@
-import { createColorScheme } from "./common/ramps"
-import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common"
+import { createColorScheme } from "../common/ramps"
+import { ayu, meta as themeMeta, buildTheme } from "./common"
 
 export const meta = {
     ...themeMeta,

styles/src/themes/ayu-light.ts β†’ styles/src/themes/ayu/ayu-light.ts πŸ”—

@@ -1,5 +1,5 @@
-import { createColorScheme } from "./common/ramps"
-import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common"
+import { createColorScheme } from "../common/ramps"
+import { ayu, meta as themeMeta, buildTheme } from "./common"
 
 export const meta = {
     ...themeMeta,

styles/src/themes/ayu-mirage.ts β†’ styles/src/themes/ayu/ayu-mirage.ts πŸ”—

@@ -1,5 +1,5 @@
-import { createColorScheme } from "./common/ramps"
-import { ayu, meta as themeMeta, buildTheme } from "./common/ayu-common"
+import { createColorScheme } from "../common/ramps"
+import { ayu, meta as themeMeta, buildTheme } from "./common"
 
 export const meta = {
     ...themeMeta,

styles/src/themes/common/ayu-common.ts β†’ styles/src/themes/ayu/common.ts πŸ”—

@@ -1,8 +1,8 @@
 import { dark, light, mirage } from "ayu"
-import { ThemeSyntax } from "./syntax"
+import { ThemeSyntax } from "../common/syntax"
 import chroma from "chroma-js"
-import { colorRamp } from "./ramps"
-import { Meta } from "./colorScheme"
+import { colorRamp } from "../common/ramps"
+import { Meta } from "../common/colorScheme"
 
 export const ayu = {
     dark,
@@ -79,12 +79,6 @@ export const meta: Meta = {
     author: "dempfi",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/dempfi/ayu/master/LICENSE",
-            license_checksum:
-                "e0af0e0d1754c18ca075649d42f5c6d9a60f8bdc03c20dfd97105f2253a94173",
-        },
     },
     url: "https://github.com/dempfi/ayu",
 }

styles/src/themes/common/colorScheme.ts πŸ”—

@@ -19,6 +19,11 @@ export interface ColorScheme {
     syntax?: Partial<ThemeSyntax>
 }
 
+export interface MetaAndLicense {
+    meta: Meta
+    licenseFile: string
+}
+
 export interface Meta {
     name: string
     author: string
@@ -28,13 +33,6 @@ export interface Meta {
 
 export interface License {
     SPDX: SPDXExpression
-    /// A url where we can download the license's text
-    license_text: Verification | string
-}
-
-export interface Verification {
-    https_url: string
-    license_checksum: string
 }
 
 // License name -> License text

styles/src/themes/gruvbox/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) <YEAR> <COPYRIGHT HOLDER>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/gruvbox-common.ts β†’ styles/src/themes/gruvbox/gruvbox-common.ts πŸ”—

@@ -1,6 +1,6 @@
 import chroma from "chroma-js"
-import { Meta, ThemeSyntax } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { Meta, ThemeSyntax } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "Gruvbox"
 
@@ -248,8 +248,6 @@ export const meta: Meta = {
     name,
     license: {
         SPDX: "MIT", // "MIT/X11"
-        license_text:

styles/src/themes/one/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/one-dark.ts β†’ styles/src/themes/one/one-dark.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { fontWeights } from "../common"
-import { Meta, ThemeSyntax } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { fontWeights } from "../../common"
+import { Meta, ThemeSyntax } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "One Dark"
 
@@ -74,12 +74,6 @@ export const meta: Meta = {
     author: "simurai",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
-            license_checksum:
-                "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8",
-        },
     },
     url: "https://github.com/atom/atom/tree/master/packages/one-dark-ui",
 }

styles/src/themes/one-light.ts β†’ styles/src/themes/one/one-light.ts πŸ”—

@@ -1,7 +1,7 @@
 import chroma from "chroma-js"
-import { fontWeights } from "../common"
-import { Meta, ThemeSyntax } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { fontWeights } from "../../common"
+import { Meta, ThemeSyntax } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "One Light"
 
@@ -73,12 +73,6 @@ export const meta: Meta = {
     author: "simurai",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/atom/atom/master/packages/one-light-ui/LICENSE.md",
-            license_checksum:
-                "d5af8fc171f6f600c0ab4e7597dca398dda80dbe6821ce01cef78e859e7a00f8",
-        },
     },
     url: "https://github.com/atom/atom/tree/master/packages/one-light-ui",
 }

styles/src/themes/rose-pine/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2021 Emilia Dunfelt
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/rose-pine-dawn.ts β†’ styles/src/themes/rose-pine/rose-pine-dawn.ts πŸ”—

@@ -1,6 +1,6 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "RosΓ© Pine Dawn"
 
@@ -34,12 +34,6 @@ export const meta: Meta = {
     author: "edunfelt",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-            license_checksum:
-                "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
-        },
     },
     url: "https://github.com/edunfelt/base16-rose-pine-scheme",
 }

styles/src/themes/rose-pine-moon.ts β†’ styles/src/themes/rose-pine/rose-pine-moon.ts πŸ”—

@@ -1,6 +1,6 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "RosΓ© Pine Moon"
 
@@ -34,12 +34,6 @@ export const meta: Meta = {
     author: "edunfelt",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-            license_checksum:
-                "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
-        },
     },
     url: "https://github.com/edunfelt/base16-rose-pine-scheme",
 }

styles/src/themes/rose-pine.ts β†’ styles/src/themes/rose-pine/rose-pine.ts πŸ”—

@@ -1,6 +1,6 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "RosΓ© Pine"
 
@@ -32,12 +32,6 @@ export const meta: Meta = {
     author: "edunfelt",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/edunfelt/base16-rose-pine-scheme/main/LICENSE",
-            license_checksum:
-                "6ca1b9da8c78c8441c5aa43d024a4e4a7bf59d1ecca1480196e94fda0f91ee4a",
-        },
     },
     url: "https://github.com/edunfelt/base16-rose-pine-scheme",
 }

styles/src/themes/sandcastle/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 George Essig
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/sandcastle.ts β†’ styles/src/themes/sandcastle/sandcastle.ts πŸ”—

@@ -1,6 +1,6 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "Sandcastle"
 
@@ -32,12 +32,6 @@ export const meta: Meta = {
     author: "gessig",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/gessig/base16-sandcastle-scheme/master/LICENSE",
-            license_checksum:
-                "8399d44b4d935b60be9fee0a76d7cc9a817b4f3f11574c9d6d1e8fd57e72ffdc",
-        },
     },
     url: "https://github.com/gessig/base16-sandcastle-scheme",
 }

styles/src/themes/solarized/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2011 Ethan Schoonover
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/solarized.ts β†’ styles/src/themes/solarized/solarized.ts πŸ”—

@@ -1,6 +1,6 @@
 import chroma from "chroma-js"
-import { Meta as Metadata } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { Meta as Metadata } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "Solarized"
 
@@ -35,12 +35,6 @@ export const meta: Metadata = {
     author: "Ethan Schoonover",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/altercation/solarized/master/LICENSE",
-            license_checksum:
-                "494aefdabf86acce06bd63001ad8aedad4ee38da23509d3f917d95aa3368b9a6",
-        },
     },
     url: "https://github.com/altercation/solarized",
 }

styles/src/themes/summercamp/LICENSE πŸ”—

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Zoe FiriH
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

styles/src/themes/summercamp.ts β†’ styles/src/themes/summercamp/summercamp.ts πŸ”—

@@ -1,6 +1,6 @@
 import chroma from "chroma-js"
-import { Meta } from "./common/colorScheme"
-import { colorRamp, createColorScheme } from "./common/ramps"
+import { Meta } from "../common/colorScheme"
+import { colorRamp, createColorScheme } from "../common/ramps"
 
 const name = "Summercamp"
 
@@ -34,11 +34,5 @@ export const meta: Meta = {
     url: "https://github.com/zoefiri/base16-sc",
     license: {
         SPDX: "MIT",
-        license_text: {
-            https_url:
-                "https://raw.githubusercontent.com/zoefiri/base16-sc/master/LICENSE",
-            license_checksum:
-                "fadcc834b7eaf2943800956600e8aeea4b495ecf6490f4c4b6c91556a90accaf",
-        },
     },
 }