Merge remote-tracking branch 'origin/main' into invite-codes-2

Nathan Sobo created

Change summary

.github/workflows/ci.yml                      |    5 
.gitignore                                    |    1 
.zed.toml                                     |    1 
Cargo.lock                                    |    1 
assets/keymaps/default.json                   |    6 
assets/themes/.gitkeep                        |    0 
crates/collab/src/rpc.rs                      |   75 
crates/command_palette/Cargo.toml             |    2 
crates/command_palette/src/command_palette.rs |   10 
crates/contacts_panel/src/contacts_panel.rs   |   43 
crates/diagnostics/src/diagnostics.rs         |   45 
crates/editor/src/editor.rs                   |   50 
crates/editor/src/element.rs                  |    4 
crates/editor/src/multi_buffer.rs             |   12 
crates/editor/src/selections_collection.rs    |    1 
crates/file_finder/src/file_finder.rs         |  118 -
crates/fsevent/src/fsevent.rs                 |  167 +
crates/gpui/src/app.rs                        |   38 
crates/gpui/src/elements/uniform_list.rs      |    4 
crates/gpui/src/keymap.rs                     |    4 
crates/gpui/src/platform.rs                   |    5 
crates/gpui/src/platform/mac/event.rs         |   30 
crates/gpui/src/platform/mac/platform.rs      |   89 +
crates/gpui/src/platform/test.rs              |    7 
crates/project/src/project.rs                 |   52 
crates/project_panel/src/project_panel.rs     |   19 
crates/project_symbols/src/project_symbols.rs |    2 
crates/search/src/project_search.rs           |    2 
crates/text/src/tests.rs                      |   10 
crates/text/src/text.rs                       |    2 
crates/theme_selector/src/theme_selector.rs   |   23 
crates/vim/src/vim_test_context.rs            |   11 
crates/workspace/src/pane.rs                  |   12 
crates/workspace/src/waiting_room.rs          |  151 +-
crates/workspace/src/workspace.rs             |  175 +-
crates/zed/build.rs                           |   28 
crates/zed/src/main.rs                        |   62 
crates/zed/src/menus.rs                       |  162 ++
crates/zed/src/test.rs                        |   39 
crates/zed/src/zed.rs                         |  273 +--
script/build-themes                           |    7 
styles/nodemon.json                           |    8 
styles/package-lock.json                      | 1341 +-------------------
styles/package.json                           |    6 
styles/src/buildThemes.ts                     |   17 
styles/src/themes.ts                          |    6 
styles/src/themes/andromeda.ts                |   27 
styles/src/themes/brushtrees.ts               |   28 
styles/src/themes/cave.ts                     |   15 
styles/src/themes/common/base16.ts            |   24 
styles/src/themes/common/theme.ts             |   14 
styles/src/themes/rose-pine-dawn.ts           |   27 
styles/src/themes/rose-pine.ts                |   27 
styles/src/themes/sandcastle.ts               |   27 
styles/src/themes/solarized.ts                |   15 
styles/src/themes/sulphurpool.ts              |   15 
styles/src/themes/summercamp.ts               |   27 
styles/src/themes/summerfruit.ts              |   28 
styles/src/themes/template.ts                 |   69 +
59 files changed, 1,461 insertions(+), 2,008 deletions(-)

Detailed changes

.github/workflows/ci.yml πŸ”—

@@ -31,6 +31,11 @@ jobs:
           target: x86_64-apple-darwin
           profile: minimal
 
+      - name: Install Node
+        uses: actions/setup-node@v2
+        with:
+          node-version: '16'
+
       - name: Checkout repo
         uses: actions/checkout@v2
         with:

.gitignore πŸ”—

@@ -6,3 +6,4 @@
 /crates/collab/.env.toml
 /crates/collab/static/styles.css
 /vendor/bin
+/assets/themes/*.json

.zed.toml πŸ”—

@@ -1 +0,0 @@
-collaborators = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler", "gibusu", "Kethku"]

Cargo.lock πŸ”—

@@ -909,6 +909,7 @@ dependencies = [
  "fuzzy",
  "gpui",
  "picker",
+ "project",
  "serde_json",
  "settings",
  "theme",

assets/keymaps/default.json πŸ”—

@@ -18,7 +18,10 @@
             "cmd-s": "workspace::Save",
             "cmd-=": "zed::IncreaseBufferFontSize",
             "cmd--": "zed::DecreaseBufferFontSize",
-            "cmd-,": "zed::OpenSettings"
+            "cmd-,": "zed::OpenSettings",
+            "cmd-q": "zed::Quit",
+            "cmd-n": "workspace::OpenNew",
+            "cmd-o": "workspace::Open"
         }
     },
     {
@@ -210,7 +213,6 @@
         "bindings": {
             "cmd-shift-F": "project_search::Deploy",
             "cmd-k cmd-t": "theme_selector::Toggle",
-            "cmd-k t": "theme_selector::Reload",
             "cmd-k cmd-s": "zed::OpenKeymap",
             "cmd-t": "project_symbols::Toggle",
             "cmd-p": "file_finder::Toggle",

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

@@ -470,6 +470,19 @@ impl Server {
                 state.unregister_project(request.payload.project_id, request.sender_id)?;
             (state.user_id_for_connection(request.sender_id)?, project)
         };
+
+        broadcast(
+            request.sender_id,
+            project.guests.keys().copied(),
+            |conn_id| {
+                self.peer.send(
+                    conn_id,
+                    proto::UnregisterProject {
+                        project_id: request.payload.project_id,
+                    },
+                )
+            },
+        );
         for (_, receipts) in project.join_requests {
             for receipt in receipts {
                 self.peer.respond(
@@ -1665,7 +1678,7 @@ mod tests {
     use gpui::{
         executor::{self, Deterministic},
         geometry::vector::vec2f,
-        ModelHandle, TestAppContext, ViewHandle,
+        ModelHandle, Task, TestAppContext, ViewHandle,
     };
     use language::{
         range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
@@ -1697,7 +1710,7 @@ mod tests {
         time::Duration,
     };
     use theme::ThemeRegistry;
-    use workspace::{Item, SplitDirection, ToggleFollow, Workspace, WorkspaceParams};
+    use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
 
     #[cfg(test)]
     #[ctor::ctor]
@@ -1930,6 +1943,14 @@ mod tests {
             .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
             .await
             .unwrap();
+
+        // When client A (the host) leaves, the project gets unshared and guests are notified.
+        cx_a.update(|_| drop(project_a));
+        deterministic.run_until_parked();
+        project_b2.read_with(cx_b, |project, _| {
+            assert!(project.is_read_only());
+            assert!(project.collaborators().is_empty());
+        });
     }
 
     #[gpui::test(iterations = 10)]
@@ -4357,13 +4378,7 @@ mod tests {
 
         // Join the project as client B.
         let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let mut params = cx_b.update(WorkspaceParams::test);
-        params.languages = lang_registry.clone();
-        params.project = project_b.clone();
-        params.client = client_b.client.clone();
-        params.user_store = client_b.user_store.clone();
-
-        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
+        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
                 workspace.open_path((worktree_id, "main.rs"), true, cx)
@@ -4598,13 +4613,7 @@ mod tests {
 
         // Join the worktree as client B.
         let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
-        let mut params = cx_b.update(WorkspaceParams::test);
-        params.languages = lang_registry.clone();
-        params.project = project_b.clone();
-        params.client = client_b.client.clone();
-        params.user_store = client_b.user_store.clone();
-
-        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(&params, cx));
+        let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
         let editor_b = workspace_b
             .update(cx_b, |workspace, cx| {
                 workspace.open_path((worktree_id, "one.rs"), true, cx)
@@ -6637,13 +6646,21 @@ mod tests {
                     })
                 });
 
+            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
+            let app_state = Arc::new(workspace::AppState {
+                client: client.clone(),
+                user_store: user_store.clone(),
+                languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
+                themes: ThemeRegistry::new((), cx.font_cache()),
+                fs: FakeFs::new(cx.background()),
+                build_window_options: || Default::default(),
+                initialize_workspace: |_, _, _| unimplemented!(),
+            });
+
             Channel::init(&client);
             Project::init(&client);
-            cx.update(|cx| {
-                workspace::init(&client, cx);
-            });
+            cx.update(|cx| workspace::init(app_state.clone(), cx));
 
-            let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
             client
                 .authenticate_and_connect(false, &cx.to_async())
                 .await
@@ -6882,23 +6899,7 @@ mod tests {
             cx: &mut TestAppContext,
         ) -> ViewHandle<Workspace> {
             let (window_id, _) = cx.add_window(|_| EmptyView);
-            cx.add_view(window_id, |cx| {
-                let fs = project.read(cx).fs().clone();
-                Workspace::new(
-                    &WorkspaceParams {
-                        fs,
-                        project: project.clone(),
-                        user_store: self.user_store.clone(),
-                        languages: self.language_registry.clone(),
-                        themes: ThemeRegistry::new((), cx.font_cache().clone()),
-                        channel_list: cx.add_model(|cx| {
-                            ChannelList::new(self.user_store.clone(), self.client.clone(), cx)
-                        }),
-                        client: self.client.clone(),
-                    },
-                    cx,
-                )
-            })
+            cx.add_view(window_id, |cx| Workspace::new(project.clone(), cx))
         }
 
         async fn simulate_host(

crates/command_palette/Cargo.toml πŸ”—

@@ -12,6 +12,7 @@ editor = { path = "../editor" }
 fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 picker = { path = "../picker" }
+project = { path = "../project" }
 settings = { path = "../settings" }
 util = { path = "../util" }
 theme = { path = "../theme" }
@@ -20,6 +21,7 @@ workspace = { path = "../workspace" }
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }
 editor = { path = "../editor", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
 serde_json = { version = "1.0.64", features = ["preserve_order"] }
 workspace = { path = "../workspace", features = ["test-support"] }
 ctor = "0.1"

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

@@ -299,7 +299,8 @@ mod tests {
     use super::*;
     use editor::Editor;
     use gpui::TestAppContext;
-    use workspace::{Workspace, WorkspaceParams};
+    use project::Project;
+    use workspace::{AppState, Workspace};
 
     #[test]
     fn test_humanize_action_name() {
@@ -319,15 +320,16 @@ mod tests {
 
     #[gpui::test]
     async fn test_command_palette(cx: &mut TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
+        let app_state = cx.update(AppState::test);
 
         cx.update(|cx| {
             editor::init(cx);
-            workspace::init(&params.client, cx);
+            workspace::init(app_state.clone(), cx);
             init(cx);
         });
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let editor = cx.add_view(window_id, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", cx);

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

@@ -23,7 +23,7 @@ use theme::IconButton;
 use workspace::{
     menu::{Confirm, SelectNext, SelectPrev},
     sidebar::SidebarItem,
-    AppState, JoinProject, Workspace,
+    JoinProject, Workspace,
 };
 
 impl_actions!(
@@ -60,7 +60,6 @@ pub struct ContactsPanel {
     filter_editor: ViewHandle<Editor>,
     collapsed_sections: Vec<Section>,
     selection: Option<usize>,
-    app_state: Arc<AppState>,
     _maintain_contacts: Subscription,
 }
 
@@ -92,7 +91,7 @@ pub fn init(cx: &mut MutableAppContext) {
 
 impl ContactsPanel {
     pub fn new(
-        app_state: Arc<AppState>,
+        user_store: ModelHandle<UserStore>,
         workspace: WeakViewHandle<Workspace>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
@@ -152,8 +151,8 @@ impl ContactsPanel {
             }
         });
 
-        cx.subscribe(&app_state.user_store, {
-            let user_store = app_state.user_store.downgrade();
+        cx.subscribe(&user_store, {
+            let user_store = user_store.downgrade();
             move |_, _, event, cx| {
                 if let Some((workspace, user_store)) =
                     workspace.upgrade(cx).zip(user_store.upgrade(cx))
@@ -187,7 +186,6 @@ impl ContactsPanel {
         let mut this = Self {
             list_state: ListState::new(0, Orientation::Top, 1000., {
                 let this = cx.weak_handle();
-                let app_state = app_state.clone();
                 move |ix, cx| {
                     let this = this.upgrade(cx).unwrap();
                     let this = this.read(cx);
@@ -234,7 +232,6 @@ impl ContactsPanel {
                                 contact.clone(),
                                 current_user_id,
                                 *project_ix,
-                                app_state.clone(),
                                 theme,
                                 is_last_project_for_contact,
                                 is_selected,
@@ -249,10 +246,8 @@ impl ContactsPanel {
             entries: Default::default(),
             match_candidates: Default::default(),
             filter_editor,
-            _maintain_contacts: cx
-                .observe(&app_state.user_store, |this, _, cx| this.update_entries(cx)),
-            user_store: app_state.user_store.clone(),
-            app_state,
+            _maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)),
+            user_store,
         };
         this.update_entries(cx);
         this
@@ -351,7 +346,6 @@ impl ContactsPanel {
         contact: Arc<Contact>,
         current_user_id: Option<u64>,
         project_index: usize,
-        app_state: Arc<AppState>,
         theme: &theme::ContactsPanel,
         is_last_project: bool,
         is_selected: bool,
@@ -456,7 +450,6 @@ impl ContactsPanel {
                 cx.dispatch_global_action(JoinProject {
                     contact: contact.clone(),
                     project_index,
-                    app_state: app_state.clone(),
                 });
             }
         })
@@ -782,7 +775,6 @@ impl ContactsPanel {
                         .dispatch_global_action(JoinProject {
                             contact: contact.clone(),
                             project_index: *project_index,
-                            app_state: self.app_state.clone(),
                         }),
                     _ => {}
                 }
@@ -987,19 +979,20 @@ impl PartialEq for ContactEntry {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use client::{proto, test::FakeServer, ChannelList, Client};
+    use client::{proto, test::FakeServer, Client};
     use gpui::TestAppContext;
     use language::LanguageRegistry;
+    use project::Project;
     use theme::ThemeRegistry;
-    use workspace::WorkspaceParams;
+    use workspace::AppState;
 
     #[gpui::test]
     async fn test_contact_panel(cx: &mut TestAppContext) {
         let (app_state, server) = init(cx).await;
-        let workspace_params = cx.update(WorkspaceParams::test);
-        let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let workspace = cx.add_view(0, |cx| Workspace::new(project, cx));
         let panel = cx.add_view(0, |cx| {
-            ContactsPanel::new(app_state.clone(), workspace.downgrade(), cx)
+            ContactsPanel::new(app_state.user_store.clone(), workspace.downgrade(), cx)
         });
 
         let get_users_request = server.receive::<proto::GetUsers>().await.unwrap();
@@ -1181,13 +1174,6 @@ mod tests {
         let mut client = Client::new(http_client.clone());
         let server = FakeServer::for_client(100, &mut client, &cx).await;
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
-        let channel_list =
-            cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));
-
-        let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
-        server
-            .respond(get_channels.receipt(), Default::default())
-            .await;
 
         (
             Arc::new(AppState {
@@ -1196,9 +1182,8 @@ mod tests {
                 client,
                 user_store: user_store.clone(),
                 fs,
-                channel_list,
-                build_window_options: || unimplemented!(),
-                build_workspace: |_, _, _| unimplemented!(),
+                build_window_options: || Default::default(),
+                initialize_workspace: |_, _, _| {},
             }),
             server,
         )

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

@@ -707,49 +707,42 @@ mod tests {
     use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
     use serde_json::json;
     use unindent::Unindent as _;
-    use workspace::WorkspaceParams;
+    use workspace::AppState;
 
     #[gpui::test]
     async fn test_diagnostics(cx: &mut TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
-        let project = params.project.clone();
-        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
-
-        params
+        let app_state = cx.update(AppState::test);
+        app_state
             .fs
             .as_fake()
             .insert_tree(
                 "/test",
                 json!({
                     "consts.rs": "
-                    const a: i32 = 'a';
-                    const b: i32 = c;
-                "
+                        const a: i32 = 'a';
+                        const b: i32 = c;
+                    "
                     .unindent(),
 
                     "main.rs": "
-                    fn main() {
-                        let x = vec![];
-                        let y = vec![];
-                        a(x);
-                        b(y);
-                        // comment 1
-                        // comment 2
-                        c(y);
-                        d(x);
-                    }
-                "
+                        fn main() {
+                            let x = vec![];
+                            let y = vec![];
+                            a(x);
+                            b(y);
+                            // comment 1
+                            // comment 2
+                            c(y);
+                            d(x);
+                        }
+                    "
                     .unindent(),
                 }),
             )
             .await;
 
-        project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/test", true, cx)
-            })
-            .await
-            .unwrap();
+        let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
+        let workspace = cx.add_view(0, |cx| Workspace::new(project.clone(), cx));
 
         // Create some diagnostics
         project.update(cx, |project, cx| {

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

@@ -862,7 +862,14 @@ impl Editor {
     ) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx)
+        Self::new(
+            EditorMode::SingleLine,
+            buffer,
+            None,
+            field_editor_style,
+            None,
+            cx,
+        )
     }
 
     pub fn auto_height(
@@ -877,6 +884,7 @@ impl Editor {
             buffer,
             None,
             field_editor_style,
+            None,
             cx,
         )
     }
@@ -887,7 +895,7 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        Self::new(EditorMode::Full, buffer, project, None, cx)
+        Self::new(EditorMode::Full, buffer, project, None, None, cx)
     }
 
     pub fn for_multibuffer(
@@ -895,7 +903,7 @@ impl Editor {
         project: Option<ModelHandle<Project>>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
-        Self::new(EditorMode::Full, buffer, project, None, cx)
+        Self::new(EditorMode::Full, buffer, project, None, None, cx)
     }
 
     pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
@@ -904,6 +912,7 @@ impl Editor {
             self.buffer.clone(),
             self.project.clone(),
             self.get_field_editor_theme,
+            Some(self.selections.clone()),
             cx,
         );
         clone.scroll_position = self.scroll_position;
@@ -917,6 +926,7 @@ impl Editor {
         buffer: ModelHandle<MultiBuffer>,
         project: Option<ModelHandle<Project>>,
         get_field_editor_theme: Option<GetFieldEditorTheme>,
+        selections: Option<SelectionsCollection>,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let display_map = cx.add_model(|cx| {
@@ -937,7 +947,8 @@ impl Editor {
         cx.observe(&display_map, Self::on_display_map_changed)
             .detach();
 
-        let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
+        let selections = selections
+            .unwrap_or_else(|| SelectionsCollection::new(display_map.clone(), buffer.clone()));
 
         let mut this = Self {
             handle: cx.weak_handle(),
@@ -6025,7 +6036,10 @@ mod tests {
     use std::{cell::RefCell, rc::Rc, time::Instant};
     use text::Point;
     use unindent::Unindent;
-    use util::test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text};
+    use util::{
+        assert_set_eq,
+        test::{marked_text_by, marked_text_ranges, marked_text_ranges_by, sample_text},
+    };
     use workspace::{FollowableItem, ItemHandle};
 
     #[gpui::test]
@@ -6304,6 +6318,26 @@ mod tests {
         });
     }
 
+    #[gpui::test]
+    fn test_clone_with_selections(cx: &mut gpui::MutableAppContext) {
+        let (text, selection_ranges) = marked_text_ranges(indoc! {"
+            The qu[ick brown
+            fox jum]ps over
+            the lazy dog
+        "});
+        cx.set_global(Settings::test(cx));
+        let buffer = MultiBuffer::build_simple(&text, cx);
+
+        let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx));
+
+        let cloned_editor = view.update(cx, |view, cx| {
+            view.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
+            view.clone(cx)
+        });
+
+        assert_set_eq!(cloned_editor.selections.ranges(cx), selection_ranges);
+    }
+
     #[gpui::test]
     fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
         cx.set_global(Settings::test(cx));
@@ -8815,7 +8849,7 @@ mod tests {
         let fs = FakeFs::new(cx.background().clone());
         fs.insert_file("/file.rs", Default::default()).await;
 
-        let project = Project::test(fs, ["/file.rs"], cx).await;
+        let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
@@ -8937,7 +8971,7 @@ mod tests {
         let fs = FakeFs::new(cx.background().clone());
         fs.insert_file("/file.rs", text).await;
 
-        let project = Project::test(fs, ["/file.rs"], cx).await;
+        let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
@@ -9765,7 +9799,7 @@ mod tests {
     }
 
     fn build_editor(buffer: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
-        Editor::new(EditorMode::Full, buffer, None, None, cx)
+        Editor::new(EditorMode::Full, buffer, None, None, None, cx)
     }
 
     fn assert_selection_ranges(

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

@@ -1517,7 +1517,7 @@ mod tests {
         cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
+            Editor::new(EditorMode::Full, buffer, None, None, None, cx)
         });
         let element = EditorElement::new(
             editor.downgrade(),
@@ -1539,7 +1539,7 @@ mod tests {
         cx.set_global(Settings::test(cx));
         let buffer = MultiBuffer::build_simple("", cx);
         let (window_id, editor) = cx.add_window(Default::default(), |cx| {
-            Editor::new(EditorMode::Full, buffer, None, None, cx)
+            Editor::new(EditorMode::Full, buffer, None, None, None, cx)
         });
 
         editor.update(cx, |editor, cx| {

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

@@ -2479,6 +2479,7 @@ impl History {
                 self.undo_stack.pop();
                 false
             } else {
+                self.redo_stack.clear();
                 let transaction = self.undo_stack.last_mut().unwrap();
                 transaction.last_edit_at = now;
                 for (buffer_id, transaction_id) in buffer_transactions {
@@ -2511,6 +2512,7 @@ impl History {
         };
         if !transaction.buffer_transactions.is_empty() {
             self.undo_stack.push(transaction);
+            self.redo_stack.clear();
         }
     }
 
@@ -3935,6 +3937,16 @@ mod tests {
             buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx));
             assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
 
+            // Redo stack gets cleared after an edit.
+            now += 2 * group_interval;
+            multibuffer.start_transaction_at(now, cx);
+            multibuffer.edit([(0..0, "X")], cx);
+            multibuffer.end_transaction_at(now, cx);
+            assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
+            multibuffer.redo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678");
+            multibuffer.undo(cx);
+            assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678");
             multibuffer.undo(cx);
             assert_eq!(multibuffer.read(cx).text(), "1234\n5678");
         });

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

@@ -258,9 +258,10 @@ mod tests {
     use super::*;
     use editor::{Editor, Input};
     use serde_json::json;
-    use std::path::PathBuf;
-    use workspace::menu::{Confirm, SelectNext};
-    use workspace::{Workspace, WorkspaceParams};
+    use workspace::{
+        menu::{Confirm, SelectNext},
+        AppState, Workspace,
+    };
 
     #[ctor::ctor]
     fn init_logger() {
@@ -271,13 +272,13 @@ mod tests {
 
     #[gpui::test]
     async fn test_matching_paths(cx: &mut gpui::TestAppContext) {
-        cx.update(|cx| {
+        let app_state = cx.update(|cx| {
             super::init(cx);
             editor::init(cx);
+            AppState::test(cx)
         });
 
-        let params = cx.update(WorkspaceParams::test);
-        params
+        app_state
             .fs
             .as_fake()
             .insert_tree(
@@ -291,16 +292,8 @@ mod tests {
             )
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         cx.dispatch_action(window_id, Toggle);
 
         let finder = cx.read(|cx| {
@@ -341,32 +334,26 @@ mod tests {
 
     #[gpui::test]
     async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
-        let fs = params.fs.as_fake();
-        fs.insert_tree(
-            "/dir",
-            json!({
-                "hello": "",
-                "goodbye": "",
-                "halogen-light": "",
-                "happiness": "",
-                "height": "",
-                "hi": "",
-                "hiccup": "",
-            }),
-        )
-        .await;
-
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/dir", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+        let app_state = cx.update(AppState::test);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/dir",
+                json!({
+                    "hello": "",
+                    "goodbye": "",
+                    "halogen-light": "",
+                    "happiness": "",
+                    "height": "",
+                    "hi": "",
+                    "hiccup": "",
+                }),
+            )
             .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -406,23 +393,20 @@ mod tests {
 
     #[gpui::test]
     async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
-        params
+        let app_state = cx.update(AppState::test);
+        app_state
             .fs
             .as_fake()
             .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
             .await;
 
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root/the-parent-dir/the-file", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(
+            app_state.fs.clone(),
+            ["/root/the-parent-dir/the-file".as_ref()],
+            cx,
+        )
+        .await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 
@@ -451,10 +435,12 @@ mod tests {
         finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 0));
     }
 
-    #[gpui::test(retries = 5)]
+    #[gpui::test]
     async fn test_multiple_matches_with_same_relative_path(cx: &mut gpui::TestAppContext) {
-        let params = cx.update(WorkspaceParams::test);
-        params
+        cx.foreground().forbid_parking();
+
+        let app_state = cx.update(AppState::test);
+        app_state
             .fs
             .as_fake()
             .insert_tree(
@@ -466,19 +452,13 @@ mod tests {
             )
             .await;
 
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-
-        workspace
-            .update(cx, |workspace, cx| {
-                workspace.open_paths(
-                    vec![PathBuf::from("/root/dir1"), PathBuf::from("/root/dir2")],
-                    cx,
-                )
-            })
-            .await;
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
-
+        let project = Project::test(
+            app_state.fs.clone(),
+            ["/root/dir1".as_ref(), "/root/dir2".as_ref()],
+            cx,
+        )
+        .await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), cx));
 

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

@@ -8,7 +8,7 @@ use std::{
     ffi::{c_void, CStr, OsStr},
     os::unix::ffi::OsStrExt,
     path::{Path, PathBuf},
-    slice,
+    ptr, slice,
     sync::Arc,
     time::Duration,
 };
@@ -21,12 +21,28 @@ pub struct Event {
 }
 
 pub struct EventStream {
+    lifecycle: Arc<Mutex<Lifecycle>>,
+    state: Box<State>,
+}
+
+struct State {
+    latency: Duration,
+    paths: cf::CFMutableArrayRef,
+    callback: Option<Box<dyn FnMut(Vec<Event>) -> bool>>,
+    last_valid_event_id: Option<fs::FSEventStreamEventId>,
     stream: fs::FSEventStreamRef,
-    state: Arc<Mutex<Lifecycle>>,
-    callback: Box<Option<RunCallback>>,
 }
 
-type RunCallback = Box<dyn FnMut(Vec<Event>) -> bool>;
+impl Drop for State {
+    fn drop(&mut self) {
+        unsafe {
+            cf::CFRelease(self.paths);
+            fs::FSEventStreamStop(self.stream);
+            fs::FSEventStreamInvalidate(self.stream);
+            fs::FSEventStreamRelease(self.stream);
+        }
+    }
+}
 
 enum Lifecycle {
     New,
@@ -42,15 +58,6 @@ unsafe impl Send for Lifecycle {}
 impl EventStream {
     pub fn new(paths: &[&Path], latency: Duration) -> (Self, Handle) {
         unsafe {
-            let callback = Box::new(None);
-            let stream_context = fs::FSEventStreamContext {
-                version: 0,
-                info: callback.as_ref() as *const _ as *mut c_void,
-                retain: None,
-                release: None,
-                copy_description: None,
-            };
-
             let cf_paths =
                 cf::CFArrayCreateMutable(cf::kCFAllocatorDefault, 0, &cf::kCFTypeArrayCallBacks);
             assert!(!cf_paths.is_null());
@@ -69,6 +76,20 @@ impl EventStream {
                 cf::CFRelease(cf_url);
             }
 
+            let mut state = Box::new(State {
+                latency,
+                paths: cf_paths,
+                callback: None,
+                last_valid_event_id: None,
+                stream: ptr::null_mut(),
+            });
+            let stream_context = fs::FSEventStreamContext {
+                version: 0,
+                info: state.as_ref() as *const _ as *mut c_void,
+                retain: None,
+                release: None,
+                copy_description: None,
+            };
             let stream = fs::FSEventStreamCreate(
                 cf::kCFAllocatorDefault,
                 Self::trampoline,
@@ -80,17 +101,15 @@ impl EventStream {
                     | fs::kFSEventStreamCreateFlagNoDefer
                     | fs::kFSEventStreamCreateFlagWatchRoot,
             );
-            cf::CFRelease(cf_paths);
-
-            let state = Arc::new(Mutex::new(Lifecycle::New));
+            state.stream = stream;
 
+            let lifecycle = Arc::new(Mutex::new(Lifecycle::New));
             (
                 EventStream {
-                    stream,
-                    state: state.clone(),
-                    callback,
+                    lifecycle: lifecycle.clone(),
+                    state,
                 },
-                Handle(state),
+                Handle(lifecycle),
             )
         }
     }
@@ -99,21 +118,24 @@ impl EventStream {
     where
         F: FnMut(Vec<Event>) -> bool + 'static,
     {
-        *self.callback = Some(Box::new(f));
+        self.state.callback = Some(Box::new(f));
         unsafe {
             let run_loop = cf::CFRunLoopGetCurrent();
             {
-                let mut state = self.state.lock();
+                let mut state = self.lifecycle.lock();
                 match *state {
                     Lifecycle::New => *state = Lifecycle::Running(run_loop),
                     Lifecycle::Running(_) => unreachable!(),
                     Lifecycle::Stopped => return,
                 }
             }
-            fs::FSEventStreamScheduleWithRunLoop(self.stream, run_loop, cf::kCFRunLoopDefaultMode);
-            fs::FSEventStreamStart(self.stream);
+            fs::FSEventStreamScheduleWithRunLoop(
+                self.state.stream,
+                run_loop,
+                cf::kCFRunLoopDefaultMode,
+            );
+            fs::FSEventStreamStart(self.state.stream);
             cf::CFRunLoopRun();
-            fs::FSEventStreamRelease(self.stream);
         }
     }
 
@@ -129,8 +151,8 @@ impl EventStream {
             let event_paths = event_paths as *const *const ::std::os::raw::c_char;
             let e_ptr = event_flags as *mut u32;
             let i_ptr = event_ids as *mut u64;
-            let callback_ptr = (info as *mut Option<RunCallback>).as_mut().unwrap();
-            let callback = if let Some(callback) = callback_ptr.as_mut() {
+            let state = (info as *mut State).as_mut().unwrap();
+            let callback = if let Some(callback) = state.callback.as_mut() {
                 callback
             } else {
                 return;
@@ -139,30 +161,83 @@ impl EventStream {
             let paths = slice::from_raw_parts(event_paths, num);
             let flags = slice::from_raw_parts_mut(e_ptr, num);
             let ids = slice::from_raw_parts_mut(i_ptr, num);
+            let mut stream_restarted = false;
+
+            // Sometimes FSEvents reports a "dropped" event, an indication that either the kernel
+            // or our code couldn't keep up with the sheer volume of file-system events that were
+            // generated. If we observed a valid event before this happens, we'll try to read the
+            // file-system journal by stopping the current stream and creating a new one starting at
+            // such event. Otherwise, we'll let invoke the callback with the dropped event, which
+            // will likely perform a re-scan of one of the root directories.
+            if flags
+                .iter()
+                .copied()
+                .filter_map(StreamFlags::from_bits)
+                .any(|flags| {
+                    flags.contains(StreamFlags::USER_DROPPED)
+                        || flags.contains(StreamFlags::KERNEL_DROPPED)
+                })
+            {
+                if let Some(last_valid_event_id) = state.last_valid_event_id.take() {
+                    fs::FSEventStreamStop(state.stream);
+                    fs::FSEventStreamInvalidate(state.stream);
+                    fs::FSEventStreamRelease(state.stream);
+
+                    let stream_context = fs::FSEventStreamContext {
+                        version: 0,
+                        info,
+                        retain: None,
+                        release: None,
+                        copy_description: None,
+                    };
+                    let stream = fs::FSEventStreamCreate(
+                        cf::kCFAllocatorDefault,
+                        Self::trampoline,
+                        &stream_context,
+                        state.paths,
+                        last_valid_event_id,
+                        state.latency.as_secs_f64(),
+                        fs::kFSEventStreamCreateFlagFileEvents
+                            | fs::kFSEventStreamCreateFlagNoDefer
+                            | fs::kFSEventStreamCreateFlagWatchRoot,
+                    );
+
+                    state.stream = stream;
+                    fs::FSEventStreamScheduleWithRunLoop(
+                        state.stream,
+                        cf::CFRunLoopGetCurrent(),
+                        cf::kCFRunLoopDefaultMode,
+                    );
+                    fs::FSEventStreamStart(state.stream);
+                    stream_restarted = true;
+                }
+            }
 
-            let mut events = Vec::with_capacity(num);
-            for p in 0..num {
-                let path_c_str = CStr::from_ptr(paths[p]);
-                let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes()));
-                if let Some(flag) = StreamFlags::from_bits(flags[p]) {
-                    if flag.contains(StreamFlags::HISTORY_DONE) {
-                        events.clear();
+            if !stream_restarted {
+                let mut events = Vec::with_capacity(num);
+                for p in 0..num {
+                    if let Some(flag) = StreamFlags::from_bits(flags[p]) {
+                        if !flag.contains(StreamFlags::HISTORY_DONE) {
+                            let path_c_str = CStr::from_ptr(paths[p]);
+                            let path = PathBuf::from(OsStr::from_bytes(path_c_str.to_bytes()));
+                            let event = Event {
+                                event_id: ids[p],
+                                flags: flag,
+                                path,
+                            };
+                            state.last_valid_event_id = Some(event.event_id);
+                            events.push(event);
+                        }
                     } else {
-                        events.push(Event {
-                            event_id: ids[p],
-                            flags: flag,
-                            path,
-                        });
+                        debug_assert!(false, "unknown flag set for fs event: {}", flags[p]);
                     }
-                } else {
-                    debug_assert!(false, "unknown flag set for fs event: {}", flags[p]);
                 }
-            }
 
-            if !events.is_empty() {
-                if !callback(events) {
-                    fs::FSEventStreamStop(stream_ref);
-                    cf::CFRunLoopStop(cf::CFRunLoopGetCurrent());
+                if !events.is_empty() {
+                    if !callback(events) {
+                        fs::FSEventStreamStop(stream_ref);
+                        cf::CFRunLoopStop(cf::CFRunLoopGetCurrent());
+                    }
                 }
             }
         }

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

@@ -154,7 +154,6 @@ pub struct Menu<'a> {
 pub enum MenuItem<'a> {
     Action {
         name: &'a str,
-        keystroke: Option<&'a str>,
         action: Box<dyn Action>,
     },
     Separator,
@@ -193,6 +192,20 @@ impl App {
                 cx.borrow_mut().quit();
             }
         }));
+        foreground_platform.on_will_open_menu(Box::new({
+            let cx = app.0.clone();
+            move || {
+                let mut cx = cx.borrow_mut();
+                cx.keystroke_matcher.clear_pending();
+            }
+        }));
+        foreground_platform.on_validate_menu_command(Box::new({
+            let cx = app.0.clone();
+            move |action| {
+                let cx = cx.borrow_mut();
+                !cx.keystroke_matcher.has_pending_keystrokes() && cx.is_action_available(action)
+            }
+        }));
         foreground_platform.on_menu_command(Box::new({
             let cx = app.0.clone();
             move |action| {
@@ -1070,7 +1083,8 @@ impl MutableAppContext {
     }
 
     pub fn set_menus(&mut self, menus: Vec<Menu>) {
-        self.foreground_platform.set_menus(menus);
+        self.foreground_platform
+            .set_menus(menus, &self.keystroke_matcher);
     }
 
     fn prompt(
@@ -1364,6 +1378,26 @@ impl MutableAppContext {
             })
     }
 
+    pub fn is_action_available(&self, action: &dyn Action) -> bool {
+        let action_type = action.as_any().type_id();
+        if let Some(window_id) = self.cx.platform.key_window_id() {
+            if let Some((presenter, _)) = self.presenters_and_platform_windows.get(&window_id) {
+                let dispatch_path = presenter.borrow().dispatch_path(&self.cx);
+                for view_id in dispatch_path {
+                    if let Some(view) = self.views.get(&(window_id, view_id)) {
+                        let view_type = view.as_any().type_id();
+                        if let Some(actions) = self.actions.get(&view_type) {
+                            if actions.contains_key(&action_type) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+        }
+        self.global_actions.contains_key(&action_type)
+    }
+
     pub fn dispatch_action_at(&mut self, window_id: usize, view_id: usize, action: &dyn Action) {
         let presenter = self
             .presenters_and_platform_windows

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

@@ -215,12 +215,12 @@ where
         self.autoscroll(scroll_max, size.y(), item_height);
 
         let start = cmp::min(
-            ((self.scroll_top() - self.padding_top) / item_height) as usize,
+            ((self.scroll_top() - self.padding_top) / item_height.max(1.)) as usize,
             self.item_count,
         );
         let end = cmp::min(
             self.item_count,
-            start + (size.y() / item_height).ceil() as usize + 1,
+            start + (size.y() / item_height.max(1.)).ceil() as usize + 1,
         );
 
         if (start..end).contains(&sample_item_ix) {

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

@@ -123,6 +123,10 @@ impl Matcher {
         self.pending.clear();
     }
 
+    pub fn has_pending_keystrokes(&self) -> bool {
+        !self.pending.is_empty()
+    }
+
     pub fn push_keystroke(
         &mut self,
         keystroke: Keystroke,

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

@@ -14,6 +14,7 @@ use crate::{
         rect::{RectF, RectI},
         vector::Vector2F,
     },
+    keymap,
     text_layout::{LineLayout, RunStyle},
     Action, ClipboardItem, Menu, Scene,
 };
@@ -72,7 +73,9 @@ pub(crate) trait ForegroundPlatform {
     fn run(&self, on_finish_launching: Box<dyn FnOnce() -> ()>);
 
     fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
-    fn set_menus(&self, menus: Vec<Menu>);
+    fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
+    fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
+    fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
     fn prompt_for_paths(
         &self,
         options: PathPromptOptions,

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

@@ -8,7 +8,35 @@ use cocoa::{
     base::{id, nil, YES},
     foundation::NSString as _,
 };
-use std::{ffi::CStr, os::raw::c_char};
+use std::{borrow::Cow, ffi::CStr, os::raw::c_char};
+
+pub fn key_to_native(key: &str) -> Cow<str> {
+    use cocoa::appkit::*;
+    let code = match key {
+        "backspace" => 0x7F,
+        "up" => NSUpArrowFunctionKey,
+        "down" => NSDownArrowFunctionKey,
+        "left" => NSLeftArrowFunctionKey,
+        "right" => NSRightArrowFunctionKey,
+        "pageup" => NSPageUpFunctionKey,
+        "pagedown" => NSPageDownFunctionKey,
+        "delete" => NSDeleteFunctionKey,
+        "f1" => NSF1FunctionKey,
+        "f2" => NSF2FunctionKey,
+        "f3" => NSF3FunctionKey,
+        "f4" => NSF4FunctionKey,
+        "f5" => NSF5FunctionKey,
+        "f6" => NSF6FunctionKey,
+        "f7" => NSF7FunctionKey,
+        "f8" => NSF8FunctionKey,
+        "f9" => NSF9FunctionKey,
+        "f10" => NSF10FunctionKey,
+        "f11" => NSF11FunctionKey,
+        "f12" => NSF12FunctionKey,
+        _ => return Cow::Borrowed(key),
+    };
+    Cow::Owned(String::from_utf16(&[code]).unwrap())
+}
 
 impl Event {
     pub unsafe fn from_native(native_event: id, window_height: Option<f32>) -> Option<Self> {

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

@@ -1,7 +1,6 @@
-use super::{BoolExt as _, Dispatcher, FontSystem, Window};
+use super::{event::key_to_native, BoolExt as _, Dispatcher, FontSystem, Window};
 use crate::{
-    executor,
-    keymap::Keystroke,
+    executor, keymap,
     platform::{self, CursorStyle},
     Action, ClipboardItem, Event, Menu, MenuItem,
 };
@@ -90,6 +89,14 @@ unsafe fn build_classes() {
             sel!(handleGPUIMenuItem:),
             handle_menu_item as extern "C" fn(&mut Object, Sel, id),
         );
+        decl.add_method(
+            sel!(validateMenuItem:),
+            validate_menu_item as extern "C" fn(&mut Object, Sel, id) -> bool,
+        );
+        decl.add_method(
+            sel!(menuWillOpen:),
+            menu_will_open as extern "C" fn(&mut Object, Sel, id),
+        );
         decl.add_method(
             sel!(application:openURLs:),
             open_urls as extern "C" fn(&mut Object, Sel, id, id),
@@ -108,14 +115,22 @@ pub struct MacForegroundPlatformState {
     quit: Option<Box<dyn FnMut()>>,
     event: Option<Box<dyn FnMut(crate::Event) -> bool>>,
     menu_command: Option<Box<dyn FnMut(&dyn Action)>>,
+    validate_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
+    will_open_menu: Option<Box<dyn FnMut()>>,
     open_urls: Option<Box<dyn FnMut(Vec<String>)>>,
     finish_launching: Option<Box<dyn FnOnce() -> ()>>,
     menu_actions: Vec<Box<dyn Action>>,
 }
 
 impl MacForegroundPlatform {
-    unsafe fn create_menu_bar(&self, menus: Vec<Menu>) -> id {
+    unsafe fn create_menu_bar(
+        &self,
+        menus: Vec<Menu>,
+        delegate: id,
+        keystroke_matcher: &keymap::Matcher,
+    ) -> id {
         let menu_bar = NSMenu::new(nil).autorelease();
+        menu_bar.setDelegate_(delegate);
         let mut state = self.0.borrow_mut();
 
         state.menu_actions.clear();
@@ -126,6 +141,7 @@ impl MacForegroundPlatform {
             let menu_name = menu_config.name;
 
             menu.setTitle_(ns_string(menu_name));
+            menu.setDelegate_(delegate);
 
             for item_config in menu_config.items {
                 let item;
@@ -134,19 +150,18 @@ impl MacForegroundPlatform {
                     MenuItem::Separator => {
                         item = NSMenuItem::separatorItem(nil);
                     }
-                    MenuItem::Action {
-                        name,
-                        keystroke,
-                        action,
-                    } => {
-                        if let Some(keystroke) = keystroke {
-                            let keystroke = Keystroke::parse(keystroke).unwrap_or_else(|err| {
-                                panic!(
-                                    "Invalid keystroke for menu item {}:{} - {:?}",
-                                    menu_name, name, err
-                                )
-                            });
+                    MenuItem::Action { name, action } => {
+                        let mut keystroke = None;
+                        if let Some(binding) = keystroke_matcher
+                            .bindings_for_action_type(action.as_any().type_id())
+                            .next()
+                        {
+                            if binding.keystrokes().len() == 1 {
+                                keystroke = binding.keystrokes().first()
+                            }
+                        }
 
+                        if let Some(keystroke) = keystroke {
                             let mut mask = NSEventModifierFlags::empty();
                             for (modifier, flag) in &[
                                 (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask),
@@ -162,7 +177,7 @@ impl MacForegroundPlatform {
                                 .initWithTitle_action_keyEquivalent_(
                                     ns_string(name),
                                     selector("handleGPUIMenuItem:"),
-                                    ns_string(&keystroke.key),
+                                    ns_string(key_to_native(&keystroke.key).as_ref()),
                                 )
                                 .autorelease();
                             item.setKeyEquivalentModifierMask_(mask);
@@ -239,10 +254,18 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
         self.0.borrow_mut().menu_command = Some(callback);
     }
 
-    fn set_menus(&self, menus: Vec<Menu>) {
+    fn on_will_open_menu(&self, callback: Box<dyn FnMut()>) {
+        self.0.borrow_mut().will_open_menu = Some(callback);
+    }
+
+    fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>) {
+        self.0.borrow_mut().validate_menu_command = Some(callback);
+    }
+
+    fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
         unsafe {
             let app: id = msg_send![APP_CLASS, sharedApplication];
-            app.setMainMenu_(self.create_menu_bar(menus));
+            app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), keystroke_matcher));
         }
     }
 
@@ -740,6 +763,34 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) {
     }
 }
 
+extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool {
+    unsafe {
+        let mut result = false;
+        let platform = get_foreground_platform(this);
+        let mut platform = platform.0.borrow_mut();
+        if let Some(mut callback) = platform.validate_menu_command.take() {
+            let tag: NSInteger = msg_send![item, tag];
+            let index = tag as usize;
+            if let Some(action) = platform.menu_actions.get(index) {
+                result = callback(action.as_ref());
+            }
+            platform.validate_menu_command = Some(callback);
+        }
+        result
+    }
+}
+
+extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) {
+    unsafe {
+        let platform = get_foreground_platform(this);
+        let mut platform = platform.0.borrow_mut();
+        if let Some(mut callback) = platform.will_open_menu.take() {
+            callback();
+            platform.will_open_menu = Some(callback);
+        }
+    }
+}
+
 unsafe fn ns_string(string: &str) -> id {
     NSString::alloc(nil).init_str(string).autorelease()
 }

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

@@ -1,7 +1,7 @@
 use super::{AppVersion, CursorStyle, WindowBounds};
 use crate::{
     geometry::vector::{vec2f, Vector2F},
-    Action, ClipboardItem,
+    keymap, Action, ClipboardItem,
 };
 use anyhow::{anyhow, Result};
 use parking_lot::Mutex;
@@ -73,8 +73,9 @@ impl super::ForegroundPlatform for ForegroundPlatform {
     }
 
     fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
-
-    fn set_menus(&self, _: Vec<crate::Menu>) {}
+    fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
+    fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
+    fn set_menus(&self, _: Vec<crate::Menu>, _: &keymap::Matcher) {}
 
     fn prompt_for_paths(
         &self,

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

@@ -497,7 +497,7 @@ impl Project {
     #[cfg(any(test, feature = "test-support"))]
     pub async fn test(
         fs: Arc<dyn Fs>,
-        root_paths: impl IntoIterator<Item = impl AsRef<Path>>,
+        root_paths: impl IntoIterator<Item = &Path>,
         cx: &mut gpui::TestAppContext,
     ) -> ModelHandle<Project> {
         let languages = Arc::new(LanguageRegistry::test());
@@ -528,6 +528,14 @@ impl Project {
         &self.languages
     }
 
+    pub fn client(&self) -> Arc<Client> {
+        self.client.clone()
+    }
+
+    pub fn user_store(&self) -> ModelHandle<UserStore> {
+        self.user_store.clone()
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn check_invariants(&self, cx: &AppContext) {
         if self.is_local() {
@@ -5294,7 +5302,7 @@ mod tests {
         )
         .unwrap();
 
-        let project = Project::test(Arc::new(RealFs), [root_link_path], cx).await;
+        let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
 
         project.read_with(cx, |project, cx| {
             let tree = project.worktrees(cx).next().unwrap().read(cx);
@@ -5378,7 +5386,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/the-root"], cx).await;
+        let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
         project.update(cx, |project, _| {
             project.languages.add(Arc::new(rust_language));
             project.languages.add(Arc::new(json_language));
@@ -5714,7 +5722,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir/a.rs", "/dir/b.rs"], cx).await;
+        let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
 
         let buffer_a = project
             .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -5825,7 +5833,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let worktree_id =
             project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
@@ -5947,7 +5955,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 
         let buffer = project
@@ -6016,7 +6024,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({ "a.rs": text })).await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 
         let buffer = project
@@ -6285,7 +6293,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({ "a.rs": text })).await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
             .await
@@ -6376,7 +6384,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
@@ -6530,7 +6538,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
             .await
@@ -6686,7 +6694,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir/b.rs"], cx).await;
+        let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
 
         let buffer = project
@@ -6780,7 +6788,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
@@ -6838,7 +6846,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs, ["/dir"], cx).await;
+        let project = Project::test(fs, ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
@@ -6944,7 +6952,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
             .await
@@ -6973,7 +6981,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir/file1"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
             .await
@@ -6995,7 +7003,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({})).await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let buffer = project.update(cx, |project, cx| {
             project.create_buffer("", None, cx).unwrap()
         });
@@ -7182,7 +7190,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 
         // Spawn multiple tasks to open paths, repeating some paths.
         let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
@@ -7227,7 +7235,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
 
         let buffer1 = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
@@ -7359,7 +7367,7 @@ mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
             .await
@@ -7444,7 +7452,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/the-dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
         let buffer = project
             .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
             .await
@@ -7708,7 +7716,7 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages.add(Arc::new(language)));
         let buffer = project
             .update(cx, |project, cx| {
@@ -7827,7 +7835,7 @@ mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         assert_eq!(
             search(&project, SearchQuery::text("TWO", false, true), cx)
                 .await

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

@@ -913,11 +913,14 @@ mod tests {
     use project::FakeFs;
     use serde_json::json;
     use std::{collections::HashSet, path::Path};
-    use workspace::WorkspaceParams;
 
     #[gpui::test]
     async fn test_visible_list(cx: &mut gpui::TestAppContext) {
         cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            let settings = Settings::test(cx);
+            cx.set_global(settings);
+        });
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -956,9 +959,8 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await;
-        let params = cx.update(WorkspaceParams::test);
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..50, cx),
@@ -1005,6 +1007,10 @@ mod tests {
     #[gpui::test(iterations = 30)]
     async fn test_editing_files(cx: &mut gpui::TestAppContext) {
         cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            let settings = Settings::test(cx);
+            cx.set_global(settings);
+        });
 
         let fs = FakeFs::new(cx.background());
         fs.insert_tree(
@@ -1043,9 +1049,8 @@ mod tests {
         )
         .await;
 
-        let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await;
-        let params = cx.update(WorkspaceParams::test);
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
 
         select_path(&panel, "root1", cx);

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

@@ -295,7 +295,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
         fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
 
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(Arc::new(language)));
 
         let _buffer = project

crates/search/src/project_search.rs πŸ”—

@@ -848,7 +848,7 @@ mod tests {
             }),
         )
         .await;
-        let project = Project::test(fs.clone(), ["/dir"], cx).await;
+        let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
         let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
         let search_view = cx.add_view(Default::default(), |cx| {
             ProjectSearchView::new(search.clone(), cx)

crates/text/src/tests.rs πŸ”—

@@ -529,6 +529,16 @@ fn test_history() {
     assert!(buffer.end_transaction_at(now).is_none());
     buffer.undo();
     assert_eq!(buffer.text(), "12cde6");
+
+    // Redo stack gets cleared after performing an edit.
+    buffer.edit([(0..0, "X")]);
+    assert_eq!(buffer.text(), "X12cde6");
+    buffer.redo();
+    assert_eq!(buffer.text(), "X12cde6");
+    buffer.undo();
+    assert_eq!(buffer.text(), "12cde6");
+    buffer.undo();
+    assert_eq!(buffer.text(), "123456");
 }
 
 #[test]

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

@@ -216,6 +216,7 @@ impl History {
                 self.undo_stack.pop();
                 None
             } else {
+                self.redo_stack.clear();
                 let entry = self.undo_stack.last_mut().unwrap();
                 entry.last_edit_at = now;
                 Some(entry)
@@ -276,6 +277,7 @@ impl History {
             last_edit_at: now,
             suppress_grouping: false,
         });
+        self.redo_stack.clear();
     }
 
     fn push_undo(&mut self, op_id: clock::Local) {

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

@@ -7,7 +7,7 @@ use picker::{Picker, PickerDelegate};
 use settings::Settings;
 use std::sync::Arc;
 use theme::{Theme, ThemeRegistry};
-use workspace::Workspace;
+use workspace::{AppState, Workspace};
 
 pub struct ThemeSelector {
     registry: Arc<ThemeRegistry>,
@@ -21,10 +21,14 @@ pub struct ThemeSelector {
 
 actions!(theme_selector, [Toggle, Reload]);
 
-pub fn init(cx: &mut MutableAppContext) {
-    cx.add_action(ThemeSelector::toggle);
-    cx.add_action(ThemeSelector::reload);
+pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     Picker::<ThemeSelector>::init(cx);
+    cx.add_action({
+        let theme_registry = app_state.themes.clone();
+        move |workspace, _: &Toggle, cx| {
+            ThemeSelector::toggle(workspace, theme_registry.clone(), cx)
+        }
+    });
 }
 
 pub enum Event {
@@ -64,8 +68,11 @@ impl ThemeSelector {
         this
     }
 
-    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
-        let themes = workspace.themes();
+    fn toggle(
+        workspace: &mut Workspace,
+        themes: Arc<ThemeRegistry>,
+        cx: &mut ViewContext<Workspace>,
+    ) {
         workspace.toggle_modal(cx, |_, cx| {
             let this = cx.add_view(|cx| Self::new(themes, cx));
             cx.subscribe(&this, Self::on_event).detach();
@@ -73,9 +80,9 @@ impl ThemeSelector {
         });
     }
 
-    fn reload(workspace: &mut Workspace, _: &Reload, cx: &mut ViewContext<Workspace>) {
+    #[cfg(debug_assertions)]
+    pub fn reload(themes: Arc<ThemeRegistry>, cx: &mut MutableAppContext) {
         let current_theme_name = cx.global::<Settings>().theme.name.clone();
-        let themes = workspace.themes();
         themes.clear();
         match themes.get(&current_theme_name) {
             Ok(theme) => {

crates/vim/src/vim_test_context.rs πŸ”—

@@ -7,11 +7,12 @@ use editor::{display_map::ToDisplayPoint, Autoscroll};
 use gpui::{json::json, keymap::Keystroke, ViewHandle};
 use indoc::indoc;
 use language::Selection;
+use project::Project;
 use util::{
     set_eq,
     test::{marked_text, marked_text_ranges_by, SetEqError},
 };
-use workspace::{WorkspaceHandle, WorkspaceParams};
+use workspace::{AppState, WorkspaceHandle};
 
 use crate::{state::Operator, *};
 
@@ -30,7 +31,8 @@ impl<'a> VimTestContext<'a> {
             settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap();
         });
 
-        let params = cx.update(WorkspaceParams::test);
+        let params = cx.update(AppState::test);
+        let project = Project::test(params.fs.clone(), [], cx).await;
 
         cx.update(|cx| {
             cx.update_global(|settings: &mut Settings, _| {
@@ -44,9 +46,8 @@ impl<'a> VimTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { "test.txt": "" } }))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+        project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)
             })

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

@@ -920,7 +920,7 @@ impl NavHistory {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::WorkspaceParams;
+    use crate::AppState;
     use gpui::{ModelHandle, TestAppContext, ViewContext};
     use project::Project;
     use std::sync::atomic::AtomicUsize;
@@ -929,8 +929,9 @@ mod tests {
     async fn test_close_items(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
 
-        let params = cx.update(WorkspaceParams::test);
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let app_state = cx.update(AppState::test);
+        let project = Project::test(app_state.fs.clone(), None, cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let item1 = cx.add_view(window_id, |_| {
             let mut item = TestItem::new();
             item.is_dirty = true;
@@ -1019,8 +1020,9 @@ mod tests {
     async fn test_prompting_only_on_last_item_for_entry(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
 
-        let params = cx.update(WorkspaceParams::test);
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+        let app_state = cx.update(AppState::test);
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let item = cx.add_view(window_id, |_| {
             let mut item = TestItem::new();
             item.is_dirty = true;

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

@@ -1,6 +1,6 @@
 use crate::{
     sidebar::{Side, ToggleSidebarItem},
-    AppState, ToggleFollow,
+    AppState, ToggleFollow, Workspace,
 };
 use anyhow::Result;
 use client::{proto, Client, Contact};
@@ -77,86 +77,87 @@ impl WaitingRoom {
     ) -> Self {
         let project_id = contact.projects[project_index].id;
         let client = app_state.client.clone();
-        let _join_task = cx.spawn_weak({
-            let contact = contact.clone();
-            |this, mut cx| async move {
-                let project = Project::remote(
-                    project_id,
-                    app_state.client.clone(),
-                    app_state.user_store.clone(),
-                    app_state.languages.clone(),
-                    app_state.fs.clone(),
-                    &mut cx,
-                )
-                .await;
+        let _join_task =
+            cx.spawn_weak({
+                let contact = contact.clone();
+                |this, mut cx| async move {
+                    let project = Project::remote(
+                        project_id,
+                        app_state.client.clone(),
+                        app_state.user_store.clone(),
+                        app_state.languages.clone(),
+                        app_state.fs.clone(),
+                        &mut cx,
+                    )
+                    .await;
 
-                if let Some(this) = this.upgrade(&cx) {
-                    this.update(&mut cx, |this, cx| {
-                        this.waiting = false;
-                        match project {
-                            Ok(project) => {
-                                cx.replace_root_view(|cx| {
-                                    let mut workspace = (app_state.build_workspace)(
-                                        project.clone(),
-                                        &app_state,
-                                        cx,
-                                    );
-                                    workspace.toggle_sidebar_item(
-                                        &ToggleSidebarItem {
-                                            side: Side::Left,
-                                            item_index: 0,
-                                        },
-                                        cx,
-                                    );
-                                    if let Some((host_peer_id, _)) = project
-                                        .read(cx)
-                                        .collaborators()
-                                        .iter()
-                                        .find(|(_, collaborator)| collaborator.replica_id == 0)
-                                    {
-                                        if let Some(follow) = workspace
-                                            .toggle_follow(&ToggleFollow(*host_peer_id), cx)
+                    if let Some(this) = this.upgrade(&cx) {
+                        this.update(&mut cx, |this, cx| {
+                            this.waiting = false;
+                            match project {
+                                Ok(project) => {
+                                    cx.replace_root_view(|cx| {
+                                        let mut workspace = Workspace::new(project, cx);
+                                        (app_state.initialize_workspace)(
+                                            &mut workspace,
+                                            &app_state,
+                                            cx,
+                                        );
+                                        workspace.toggle_sidebar_item(
+                                            &ToggleSidebarItem {
+                                                side: Side::Left,
+                                                item_index: 0,
+                                            },
+                                            cx,
+                                        );
+                                        if let Some((host_peer_id, _)) =
+                                            workspace.project.read(cx).collaborators().iter().find(
+                                                |(_, collaborator)| collaborator.replica_id == 0,
+                                            )
                                         {
-                                            follow.detach_and_log_err(cx);
+                                            if let Some(follow) = workspace
+                                                .toggle_follow(&ToggleFollow(*host_peer_id), cx)
+                                            {
+                                                follow.detach_and_log_err(cx);
+                                            }
                                         }
-                                    }
-                                    workspace
-                                });
-                            }
-                            Err(error @ _) => {
-                                let login = &contact.user.github_login;
-                                let message = match error {
-                                    project::JoinProjectError::HostDeclined => {
-                                        format!("@{} declined your request.", login)
-                                    }
-                                    project::JoinProjectError::HostClosedProject => {
-                                        format!(
-                                            "@{} closed their copy of {}.",
-                                            login,
-                                            humanize_list(
-                                                &contact.projects[project_index]
-                                                    .worktree_root_names
+                                        workspace
+                                    });
+                                }
+                                Err(error @ _) => {
+                                    let login = &contact.user.github_login;
+                                    let message = match error {
+                                        project::JoinProjectError::HostDeclined => {
+                                            format!("@{} declined your request.", login)
+                                        }
+                                        project::JoinProjectError::HostClosedProject => {
+                                            format!(
+                                                "@{} closed their copy of {}.",
+                                                login,
+                                                humanize_list(
+                                                    &contact.projects[project_index]
+                                                        .worktree_root_names
+                                                )
                                             )
-                                        )
-                                    }
-                                    project::JoinProjectError::HostWentOffline => {
-                                        format!("@{} went offline.", login)
-                                    }
-                                    project::JoinProjectError::Other(error) => {
-                                        log::error!("error joining project: {}", error);
-                                        "An error occurred.".to_string()
-                                    }
-                                };
-                                this.message = message;
-                                cx.notify();
+                                        }
+                                        project::JoinProjectError::HostWentOffline => {
+                                            format!("@{} went offline.", login)
+                                        }
+                                        project::JoinProjectError::Other(error) => {
+                                            log::error!("error joining project: {}", error);
+                                            "An error occurred.".to_string()
+                                        }
+                                    };
+                                    this.message = message;
+                                    cx.notify();
+                                }
                             }
-                        }
-                    })
-                }
+                        })
+                    }
 
-                Ok(())
-            }
-        });
+                    Ok(())
+                }
+            });
 
         Self {
             project_id,

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

@@ -9,8 +9,7 @@ mod waiting_room;
 
 use anyhow::{anyhow, Context, Result};
 use client::{
-    proto, Authenticate, ChannelList, Client, Contact, PeerId, Subscription, TypedEnvelope, User,
-    UserStore,
+    proto, Authenticate, Client, Contact, PeerId, Subscription, TypedEnvelope, User, UserStore,
 };
 use clock::ReplicaId;
 use collections::{hash_map, HashMap, HashSet};
@@ -75,6 +74,8 @@ type FollowableItemBuilders = HashMap<
 actions!(
     workspace,
     [
+        Open,
+        OpenNew,
         Unfollow,
         Save,
         ActivatePreviousPane,
@@ -83,16 +84,9 @@ actions!(
     ]
 );
 
-#[derive(Clone)]
-pub struct Open(pub Arc<AppState>);
-
-#[derive(Clone)]
-pub struct OpenNew(pub Arc<AppState>);
-
 #[derive(Clone)]
 pub struct OpenPaths {
     pub paths: Vec<PathBuf>,
-    pub app_state: Arc<AppState>,
 }
 
 #[derive(Clone)]
@@ -102,31 +96,37 @@ pub struct ToggleFollow(pub PeerId);
 pub struct JoinProject {
     pub contact: Arc<Contact>,
     pub project_index: usize,
-    pub app_state: Arc<AppState>,
 }
 
-impl_internal_actions!(
-    workspace,
-    [Open, OpenNew, OpenPaths, ToggleFollow, JoinProject]
-);
+impl_internal_actions!(workspace, [OpenPaths, ToggleFollow, JoinProject]);
 
-pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
+pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
     pane::init(cx);
 
     cx.add_global_action(open);
-    cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
-        open_paths(&action.paths, &action.app_state, cx).detach();
+    cx.add_global_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |action: &OpenPaths, cx: &mut MutableAppContext| {
+            if let Some(app_state) = app_state.upgrade() {
+                open_paths(&action.paths, &app_state, cx).detach();
+            }
+        }
     });
-    cx.add_global_action(move |action: &OpenNew, cx: &mut MutableAppContext| {
-        open_new(&action.0, cx)
+    cx.add_global_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |_: &OpenNew, cx: &mut MutableAppContext| {
+            if let Some(app_state) = app_state.upgrade() {
+                open_new(&app_state, cx)
+            }
+        }
     });
-    cx.add_global_action(move |action: &JoinProject, cx: &mut MutableAppContext| {
-        join_project(
-            action.contact.clone(),
-            action.project_index,
-            &action.app_state,
-            cx,
-        );
+    cx.add_global_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |action: &JoinProject, cx: &mut MutableAppContext| {
+            if let Some(app_state) = app_state.upgrade() {
+                join_project(action.contact.clone(), action.project_index, &app_state, cx);
+            }
+        }
     });
 
     cx.add_async_action(Workspace::toggle_follow);
@@ -151,6 +151,7 @@ pub fn init(client: &Arc<Client>, cx: &mut MutableAppContext) {
         workspace.activate_next_pane(cx)
     });
 
+    let client = &app_state.client;
     client.add_view_request_handler(Workspace::handle_follow);
     client.add_view_message_handler(Workspace::handle_unfollow);
     client.add_view_message_handler(Workspace::handle_update_followers);
@@ -188,10 +189,8 @@ pub struct AppState {
     pub client: Arc<client::Client>,
     pub user_store: ModelHandle<client::UserStore>,
     pub fs: Arc<dyn fs::Fs>,
-    pub channel_list: ModelHandle<client::ChannelList>,
     pub build_window_options: fn() -> WindowOptions<'static>,
-    pub build_workspace:
-        fn(ModelHandle<Project>, &Arc<AppState>, &mut ViewContext<Workspace>) -> Workspace,
+    pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
 }
 
 pub trait Item: View {
@@ -636,20 +635,9 @@ impl Into<AnyViewHandle> for &dyn NotificationHandle {
     }
 }
 
-#[derive(Clone)]
-pub struct WorkspaceParams {
-    pub project: ModelHandle<Project>,
-    pub client: Arc<Client>,
-    pub fs: Arc<dyn Fs>,
-    pub languages: Arc<LanguageRegistry>,
-    pub themes: Arc<ThemeRegistry>,
-    pub user_store: ModelHandle<UserStore>,
-    pub channel_list: ModelHandle<ChannelList>,
-}
-
-impl WorkspaceParams {
+impl AppState {
     #[cfg(any(test, feature = "test-support"))]
-    pub fn test(cx: &mut MutableAppContext) -> Self {
+    pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
         let settings = Settings::test(cx);
         cx.set_global(settings);
 
@@ -658,42 +646,16 @@ impl WorkspaceParams {
         let http_client = client::test::FakeHttpClient::with_404_response();
         let client = Client::new(http_client.clone());
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
-        let project = Project::local(
-            client.clone(),
-            user_store.clone(),
-            languages.clone(),
-            fs.clone(),
-            cx,
-        );
-        Self {
-            project,
-            channel_list: cx
-                .add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
+        let themes = ThemeRegistry::new((), cx.font_cache().clone());
+        Arc::new(Self {
             client,
-            themes: ThemeRegistry::new((), cx.font_cache().clone()),
+            themes,
             fs,
             languages,
             user_store,
-        }
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn local(app_state: &Arc<AppState>, cx: &mut MutableAppContext) -> Self {
-        Self {
-            project: Project::local(
-                app_state.client.clone(),
-                app_state.user_store.clone(),
-                app_state.languages.clone(),
-                app_state.fs.clone(),
-                cx,
-            ),
-            client: app_state.client.clone(),
-            fs: app_state.fs.clone(),
-            themes: app_state.themes.clone(),
-            languages: app_state.languages.clone(),
-            user_store: app_state.user_store.clone(),
-            channel_list: app_state.channel_list.clone(),
-        }
+            initialize_workspace: |_, _, _| {},
+            build_window_options: || Default::default(),
+        })
     }
 }
 
@@ -708,7 +670,6 @@ pub struct Workspace {
     user_store: ModelHandle<client::UserStore>,
     remote_entity_subscription: Option<Subscription>,
     fs: Arc<dyn Fs>,
-    themes: Arc<ThemeRegistry>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     left_sidebar: ViewHandle<Sidebar>,
@@ -744,8 +705,8 @@ enum FollowerItem {
 }
 
 impl Workspace {
-    pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
-        cx.observe(&params.project, |_, project, cx| {
+    pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+        cx.observe(&project, |_, project, cx| {
             if project.read(cx).is_read_only() {
                 cx.blur();
             }
@@ -753,7 +714,7 @@ impl Workspace {
         })
         .detach();
 
-        cx.subscribe(&params.project, move |this, project, event, cx| {
+        cx.subscribe(&project, move |this, project, event, cx| {
             match event {
                 project::Event::RemoteIdChanged(remote_id) => {
                     this.project_remote_id_changed(*remote_id, cx);
@@ -785,8 +746,11 @@ impl Workspace {
         cx.focus(&pane);
         cx.emit(Event::PaneAdded(pane.clone()));
 
-        let mut current_user = params.user_store.read(cx).watch_current_user().clone();
-        let mut connection_status = params.client.status().clone();
+        let fs = project.read(cx).fs().clone();
+        let user_store = project.read(cx).user_store();
+        let client = project.read(cx).client();
+        let mut current_user = user_store.read(cx).watch_current_user().clone();
+        let mut connection_status = client.status().clone();
         let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
             current_user.recv().await;
             connection_status.recv().await;
@@ -826,14 +790,13 @@ impl Workspace {
             active_pane: pane.clone(),
             status_bar,
             notifications: Default::default(),
-            client: params.client.clone(),
+            client,
             remote_entity_subscription: None,
-            user_store: params.user_store.clone(),
-            fs: params.fs.clone(),
-            themes: params.themes.clone(),
+            user_store,
+            fs,
             left_sidebar,
             right_sidebar,
-            project: params.project.clone(),
+            project,
             leader_state: Default::default(),
             follower_states_by_leader: Default::default(),
             last_leaders_by_pane: Default::default(),
@@ -867,10 +830,6 @@ impl Workspace {
         &self.project
     }
 
-    pub fn themes(&self) -> Arc<ThemeRegistry> {
-        self.themes.clone()
-    }
-
     pub fn worktrees<'a>(
         &self,
         cx: &'a AppContext,
@@ -2203,8 +2162,7 @@ impl std::fmt::Debug for OpenPaths {
     }
 }
 
-fn open(action: &Open, cx: &mut MutableAppContext) {
-    let app_state = action.0.clone();
+fn open(_: &Open, cx: &mut MutableAppContext) {
     let mut paths = cx.prompt_for_paths(PathPromptOptions {
         files: true,
         directories: true,
@@ -2212,7 +2170,7 @@ fn open(action: &Open, cx: &mut MutableAppContext) {
     });
     cx.spawn(|mut cx| async move {
         if let Some(paths) = paths.recv().await.flatten() {
-            cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths, app_state }));
+            cx.update(|cx| cx.dispatch_global_action(OpenPaths { paths }));
         }
     })
     .detach();
@@ -2260,14 +2218,17 @@ pub fn open_paths(
                     .contains(&false);
 
             cx.add_window((app_state.build_window_options)(), |cx| {
-                let project = Project::local(
-                    app_state.client.clone(),
-                    app_state.user_store.clone(),
-                    app_state.languages.clone(),
-                    app_state.fs.clone(),
+                let mut workspace = Workspace::new(
+                    Project::local(
+                        app_state.client.clone(),
+                        app_state.user_store.clone(),
+                        app_state.languages.clone(),
+                        app_state.fs.clone(),
+                        cx,
+                    ),
                     cx,
                 );
-                let mut workspace = (app_state.build_workspace)(project, &app_state, cx);
+                (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
                 if contains_directory {
                     workspace.toggle_sidebar_item(
                         &ToggleSidebarItem {
@@ -2313,14 +2274,18 @@ pub fn join_project(
 
 fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
     let (window_id, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
-        let project = Project::local(
-            app_state.client.clone(),
-            app_state.user_store.clone(),
-            app_state.languages.clone(),
-            app_state.fs.clone(),
+        let mut workspace = Workspace::new(
+            Project::local(
+                app_state.client.clone(),
+                app_state.user_store.clone(),
+                app_state.languages.clone(),
+                app_state.fs.clone(),
+                cx,
+            ),
             cx,
         );
-        (app_state.build_workspace)(project, &app_state, cx)
+        (app_state.initialize_workspace)(&mut workspace, app_state, cx);
+        workspace
     });
-    cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone()));
+    cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew);
 }

crates/zed/build.rs πŸ”—

@@ -1,3 +1,31 @@
+use std::process::Command;
+
 fn main() {
     println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.14");
+
+    let output = Command::new("npm")
+        .current_dir("../../styles")
+        .args(["ci"])
+        .output()
+        .expect("failed to run npm");
+    if !output.status.success() {
+        panic!(
+            "failed to install theme dependencies {}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+    }
+
+    let output = Command::new("npm")
+        .current_dir("../../styles")
+        .args(["run", "build-themes"])
+        .output()
+        .expect("failed to run npm");
+    if !output.status.success() {
+        panic!(
+            "build-themes script failed {}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+    }
+
+    println!("cargo:rerun-if-changed=../../styles");
 }

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

@@ -12,7 +12,7 @@ use cli::{
 use client::{
     self,
     http::{self, HttpClient},
-    ChannelList, UserStore, ZED_SECRET_CLIENT_TOKEN,
+    UserStore, ZED_SECRET_CLIENT_TOKEN,
 };
 use fs::OpenOptions;
 use futures::{
@@ -40,9 +40,9 @@ use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
 use util::{ResultExt, TryFutureExt};
 use workspace::{self, AppState, OpenNew, OpenPaths};
 use zed::{
-    self, build_window_options, build_workspace,
+    self, build_window_options,
     fs::RealFs,
-    languages, menus,
+    initialize_workspace, languages, menus,
     settings_file::{settings_from_files, watch_keymap_file, WatchedJsonFile},
 };
 
@@ -133,15 +133,12 @@ fn main() {
         let client = client::Client::new(http.clone());
         let mut languages = languages::build_language_registry(login_shell_env_loaded);
         let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
-        let channel_list =
-            cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx));
 
         auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
         project::Project::init(&client);
         client::Channel::init(&client);
         client::init(client.clone(), cx);
         command_palette::init(cx);
-        workspace::init(&client, cx);
         editor::init(cx);
         go_to_line::init(cx);
         file_finder::init(cx);
@@ -162,6 +159,8 @@ fn main() {
             cx.font_cache().clone(),
         );
 
+        cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
+            .detach();
         cx.spawn(|cx| watch_keymap_file(keymap_file, cx)).detach();
 
         let settings = cx.background().block(settings_rx.next()).unwrap();
@@ -190,33 +189,33 @@ fn main() {
         let app_state = Arc::new(AppState {
             languages,
             themes,
-            channel_list,
             client: client.clone(),
             user_store,
             fs,
             build_window_options,
-            build_workspace,
+            initialize_workspace,
         });
+        workspace::init(app_state.clone(), cx);
         journal::init(app_state.clone(), cx);
-        theme_selector::init(cx);
+        theme_selector::init(app_state.clone(), cx);
         zed::init(&app_state, cx);
 
-        cx.set_menus(menus::menus(&app_state.clone()));
+        cx.set_menus(menus::menus());
 
         if stdout_is_a_pty() {
             cx.platform().activate(true);
             let paths = collect_path_args();
             if paths.is_empty() {
-                cx.dispatch_global_action(OpenNew(app_state.clone()));
+                cx.dispatch_global_action(OpenNew);
             } else {
-                cx.dispatch_global_action(OpenPaths { paths, app_state });
+                cx.dispatch_global_action(OpenPaths { paths });
             }
         } else {
             if let Ok(Some(connection)) = cli_connections_rx.try_next() {
                 cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
                     .detach();
             } else {
-                cx.dispatch_global_action(OpenNew(app_state.clone()));
+                cx.dispatch_global_action(OpenNew);
             }
             cx.spawn(|cx| async move {
                 while let Some(connection) = cli_connections_rx.next().await {
@@ -440,6 +439,43 @@ fn load_embedded_fonts(app: &App) {
         .unwrap();
 }
 
+#[cfg(debug_assertions)]
+async fn watch_themes(
+    fs: Arc<dyn Fs>,
+    themes: Arc<ThemeRegistry>,
+    mut cx: AsyncAppContext,
+) -> Option<()> {
+    let mut events = fs
+        .watch("styles/src".as_ref(), Duration::from_millis(100))
+        .await;
+    while let Some(_) = events.next().await {
+        let output = Command::new("npm")
+            .current_dir("styles")
+            .args(["run", "build-themes"])
+            .output()
+            .await
+            .log_err()?;
+        if output.status.success() {
+            cx.update(|cx| theme_selector::ThemeSelector::reload(themes.clone(), cx))
+        } else {
+            eprintln!(
+                "build-themes script failed {}",
+                String::from_utf8_lossy(&output.stderr)
+            );
+        }
+    }
+    Some(())
+}
+
+#[cfg(not(debug_assertions))]
+async fn watch_themes(
+    _fs: Arc<dyn Fs>,
+    _themes: Arc<ThemeRegistry>,
+    _cx: AsyncAppContext,
+) -> Option<()> {
+    None
+}
+
 fn load_config_files(
     app: &App,
     fs: Arc<dyn Fs>,

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

@@ -1,33 +1,27 @@
-use crate::AppState;
 use gpui::{Menu, MenuItem};
-use std::sync::Arc;
 
 #[cfg(target_os = "macos")]
-pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
+pub fn menus() -> Vec<Menu<'static>> {
     vec![
         Menu {
             name: "Zed",
             items: vec![
                 MenuItem::Action {
                     name: "About Zed…",
-                    keystroke: None,
                     action: Box::new(super::About),
                 },
                 MenuItem::Action {
                     name: "Check for Updates",
-                    keystroke: None,
                     action: Box::new(auto_update::Check),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Install CLI",
-                    keystroke: None,
                     action: Box::new(super::InstallCommandLineInterface),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Quit",
-                    keystroke: Some("cmd-q"),
                     action: Box::new(super::Quit),
                 },
             ],
@@ -37,14 +31,20 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
             items: vec![
                 MenuItem::Action {
                     name: "New",
-                    keystroke: Some("cmd-n"),
-                    action: Box::new(workspace::OpenNew(state.clone())),
+                    action: Box::new(workspace::OpenNew),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Open…",
-                    keystroke: Some("cmd-o"),
-                    action: Box::new(workspace::Open(state.clone())),
+                    action: Box::new(workspace::Open),
+                },
+                MenuItem::Action {
+                    name: "Save",
+                    action: Box::new(workspace::Save),
+                },
+                MenuItem::Action {
+                    name: "Close Editor",
+                    action: Box::new(workspace::CloseActiveItem),
                 },
             ],
         },
@@ -53,30 +53,160 @@ pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
             items: vec![
                 MenuItem::Action {
                     name: "Undo",
-                    keystroke: Some("cmd-z"),
                     action: Box::new(editor::Undo),
                 },
                 MenuItem::Action {
                     name: "Redo",
-                    keystroke: Some("cmd-Z"),
                     action: Box::new(editor::Redo),
                 },
                 MenuItem::Separator,
                 MenuItem::Action {
                     name: "Cut",
-                    keystroke: Some("cmd-x"),
                     action: Box::new(editor::Cut),
                 },
                 MenuItem::Action {
                     name: "Copy",
-                    keystroke: Some("cmd-c"),
                     action: Box::new(editor::Copy),
                 },
                 MenuItem::Action {
                     name: "Paste",
-                    keystroke: Some("cmd-v"),
                     action: Box::new(editor::Paste),
                 },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Find",
+                    action: Box::new(search::buffer_search::Deploy { focus: true }),
+                },
+                MenuItem::Action {
+                    name: "Find In Project",
+                    action: Box::new(search::project_search::Deploy),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Toggle Line Comment",
+                    action: Box::new(editor::ToggleComments),
+                },
+            ],
+        },
+        Menu {
+            name: "Selection",
+            items: vec![
+                MenuItem::Action {
+                    name: "Select All",
+                    action: Box::new(editor::SelectAll),
+                },
+                MenuItem::Action {
+                    name: "Expand Selection",
+                    action: Box::new(editor::SelectLargerSyntaxNode),
+                },
+                MenuItem::Action {
+                    name: "Shrink Selection",
+                    action: Box::new(editor::SelectSmallerSyntaxNode),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Add Cursor Above",
+                    action: Box::new(editor::AddSelectionAbove),
+                },
+                MenuItem::Action {
+                    name: "Add Cursor Below",
+                    action: Box::new(editor::AddSelectionBelow),
+                },
+                MenuItem::Action {
+                    name: "Select Next Occurrence",
+                    action: Box::new(editor::SelectNext {
+                        replace_newest: false,
+                    }),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Move Line Up",
+                    action: Box::new(editor::MoveLineUp),
+                },
+                MenuItem::Action {
+                    name: "Move Line Down",
+                    action: Box::new(editor::MoveLineDown),
+                },
+                MenuItem::Action {
+                    name: "Duplicate Selection",
+                    action: Box::new(editor::DuplicateLine),
+                },
+            ],
+        },
+        Menu {
+            name: "View",
+            items: vec![
+                MenuItem::Action {
+                    name: "Zoom In",
+                    action: Box::new(super::IncreaseBufferFontSize),
+                },
+                MenuItem::Action {
+                    name: "Zoom Out",
+                    action: Box::new(super::DecreaseBufferFontSize),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Project Browser",
+                    action: Box::new(workspace::sidebar::ToggleSidebarItemFocus {
+                        side: workspace::sidebar::Side::Left,
+                        item_index: 0,
+                    }),
+                },
+                MenuItem::Action {
+                    name: "Command Palette",
+                    action: Box::new(command_palette::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Diagnostics",
+                    action: Box::new(diagnostics::Deploy),
+                },
+            ],
+        },
+        Menu {
+            name: "Go",
+            items: vec![
+                MenuItem::Action {
+                    name: "Back",
+                    action: Box::new(workspace::GoBack { pane: None }),
+                },
+                MenuItem::Action {
+                    name: "Forward",
+                    action: Box::new(workspace::GoForward { pane: None }),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Go to File",
+                    action: Box::new(file_finder::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Go to Symbol in Project",
+                    action: Box::new(project_symbols::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Go to Symbol in Editor",
+                    action: Box::new(outline::Toggle),
+                },
+                MenuItem::Action {
+                    name: "Go to Definition",
+                    action: Box::new(editor::GoToDefinition),
+                },
+                MenuItem::Action {
+                    name: "Go to References",
+                    action: Box::new(editor::FindAllReferences),
+                },
+                MenuItem::Action {
+                    name: "Go to Line/Column",
+                    action: Box::new(go_to_line::Toggle),
+                },
+                MenuItem::Separator,
+                MenuItem::Action {
+                    name: "Next Problem",
+                    action: Box::new(editor::GoToNextDiagnostic),
+                },
+                MenuItem::Action {
+                    name: "Previous Problem",
+                    action: Box::new(editor::GoToPrevDiagnostic),
+                },
             ],
         },
     ]

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

@@ -1,13 +1,3 @@
-use crate::{build_window_options, build_workspace, AppState};
-use assets::Assets;
-use client::{test::FakeHttpClient, ChannelList, Client, UserStore};
-use gpui::MutableAppContext;
-use language::LanguageRegistry;
-use project::fs::FakeFs;
-use settings::Settings;
-use std::sync::Arc;
-use theme::ThemeRegistry;
-
 #[cfg(test)]
 #[ctor::ctor]
 fn init_logger() {
@@ -15,32 +5,3 @@ fn init_logger() {
         env_logger::init();
     }
 }
-
-pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
-    let settings = Settings::test(cx);
-    editor::init(cx);
-    cx.set_global(settings);
-    let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
-    let http = FakeHttpClient::with_404_response();
-    let client = Client::new(http.clone());
-    let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
-    let languages = LanguageRegistry::test();
-    languages.add(Arc::new(language::Language::new(
-        language::LanguageConfig {
-            name: "Rust".into(),
-            path_suffixes: vec!["rs".to_string()],
-            ..Default::default()
-        },
-        Some(tree_sitter_rust::language()),
-    )));
-    Arc::new(AppState {
-        themes,
-        languages: Arc::new(languages),
-        channel_list: cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)),
-        client,
-        user_store,
-        fs: FakeFs::new(cx.background().clone()),
-        build_window_options,
-        build_workspace,
-    })
-}

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

@@ -15,7 +15,7 @@ use gpui::{
     actions,
     geometry::vector::vec2f,
     platform::{WindowBounds, WindowOptions},
-    AsyncAppContext, ModelHandle, ViewContext,
+    AsyncAppContext, ViewContext,
 };
 use lazy_static::lazy_static;
 pub use lsp;
@@ -31,7 +31,7 @@ use std::{
 };
 use util::ResultExt;
 pub use workspace;
-use workspace::{AppState, Workspace, WorkspaceParams};
+use workspace::{AppState, Workspace};
 
 actions!(
     zed,
@@ -115,13 +115,13 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
     settings::KeymapFileContent::load_defaults(cx);
 }
 
-pub fn build_workspace(
-    project: ModelHandle<Project>,
+pub fn initialize_workspace(
+    workspace: &mut Workspace,
     app_state: &Arc<AppState>,
     cx: &mut ViewContext<Workspace>,
-) -> Workspace {
+) {
     cx.subscribe(&cx.handle(), {
-        let project = project.clone();
+        let project = workspace.project().clone();
         move |_, _, event, cx| {
             if let workspace::Event::PaneAdded(pane) = event {
                 pane.update(cx, |pane, cx| {
@@ -139,22 +139,12 @@ pub fn build_workspace(
     })
     .detach();
 
-    let workspace_params = WorkspaceParams {
-        project,
-        client: app_state.client.clone(),
-        fs: app_state.fs.clone(),
-        languages: app_state.languages.clone(),
-        themes: app_state.themes.clone(),
-        user_store: app_state.user_store.clone(),
-        channel_list: app_state.channel_list.clone(),
-    };
-    let workspace = Workspace::new(&workspace_params, cx);
-    let project = workspace.project().clone();
+    cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
 
     let theme_names = app_state.themes.list().collect();
     let language_names = app_state.languages.language_names();
 
-    project.update(cx, |project, cx| {
+    workspace.project().update(cx, |project, cx| {
         let action_names = cx.all_action_names().collect::<Vec<_>>();
         project.set_language_server_settings(serde_json::json!({
             "json": {
@@ -172,9 +162,10 @@ pub fn build_workspace(
         }));
     });
 
-    let project_panel = ProjectPanel::new(project, cx);
-    let contact_panel =
-        cx.add_view(|cx| ContactsPanel::new(app_state.clone(), workspace.weak_handle(), cx));
+    let project_panel = ProjectPanel::new(workspace.project().clone(), cx);
+    let contact_panel = cx.add_view(|cx| {
+        ContactsPanel::new(app_state.user_store.clone(), workspace.weak_handle(), cx)
+    });
 
     workspace.left_sidebar().update(cx, |sidebar, cx| {
         sidebar.add_item("icons/folder-tree-solid-14.svg", project_panel.into(), cx)
@@ -196,8 +187,6 @@ pub fn build_workspace(
         status_bar.add_right_item(cursor_position, cx);
         status_bar.add_right_item(auto_update, cx);
     });
-
-    workspace
 }
 
 pub fn build_window_options() -> WindowOptions<'static> {
@@ -287,14 +276,18 @@ fn open_config_file(
                     workspace.open_paths(vec![path.to_path_buf()], cx)
                 } else {
                     let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
-                        let project = Project::local(
-                            app_state.client.clone(),
-                            app_state.user_store.clone(),
-                            app_state.languages.clone(),
-                            app_state.fs.clone(),
+                        let mut workspace = Workspace::new(
+                            Project::local(
+                                app_state.client.clone(),
+                                app_state.user_store.clone(),
+                                app_state.languages.clone(),
+                                app_state.fs.clone(),
+                                cx,
+                            ),
                             cx,
                         );
-                        (app_state.build_workspace)(project, &app_state, cx)
+                        (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
+                        workspace
                     });
                     workspace.update(cx, |workspace, cx| {
                         workspace.open_paths(vec![path.to_path_buf()], cx)
@@ -313,43 +306,45 @@ mod tests {
     use assets::Assets;
     use editor::{Autoscroll, DisplayPoint, Editor};
     use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
-    use project::{Fs, ProjectPath};
+    use project::ProjectPath;
     use serde_json::json;
     use std::{
         collections::HashSet,
         path::{Path, PathBuf},
     };
-    use test::test_app_state;
     use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
-    use util::test::temp_tree;
     use workspace::{
         open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
     };
 
     #[gpui::test]
     async fn test_open_paths_action(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
-        let dir = temp_tree(json!({
-            "a": {
-                "aa": null,
-                "ab": null,
-            },
-            "b": {
-                "ba": null,
-                "bb": null,
-            },
-            "c": {
-                "ca": null,
-                "cb": null,
-            },
-        }));
+        let app_state = init(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(
+                "/root",
+                json!({
+                    "a": {
+                        "aa": null,
+                        "ab": null,
+                    },
+                    "b": {
+                        "ba": null,
+                        "bb": null,
+                    },
+                    "c": {
+                        "ca": null,
+                        "cb": null,
+                    },
+                }),
+            )
+            .await;
 
         cx.update(|cx| {
             open_paths(
-                &[
-                    dir.path().join("a").to_path_buf(),
-                    dir.path().join("b").to_path_buf(),
-                ],
+                &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
                 &app_state,
                 cx,
             )
@@ -357,7 +352,7 @@ mod tests {
         .await;
         assert_eq!(cx.window_ids().len(), 1);
 
-        cx.update(|cx| open_paths(&[dir.path().join("a").to_path_buf()], &app_state, cx))
+        cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, cx))
             .await;
         assert_eq!(cx.window_ids().len(), 1);
         let workspace_1 = cx.root_view::<Workspace>(cx.window_ids()[0]).unwrap();
@@ -369,10 +364,7 @@ mod tests {
 
         cx.update(|cx| {
             open_paths(
-                &[
-                    dir.path().join("b").to_path_buf(),
-                    dir.path().join("c").to_path_buf(),
-                ],
+                &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
                 &app_state,
                 cx,
             )
@@ -383,11 +375,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_new_empty_workspace(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
-        cx.update(|cx| {
-            workspace::init(&app_state.client, cx);
-        });
-        cx.dispatch_global_action(workspace::OpenNew(app_state.clone()));
+        let app_state = init(cx);
+        cx.dispatch_global_action(workspace::OpenNew);
         let window_id = *cx.window_ids().first().unwrap();
         let workspace = cx.root_view::<Workspace>(window_id).unwrap();
         let editor = workspace.update(cx, |workspace, cx| {
@@ -414,7 +403,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_entry(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
         app_state
             .fs
             .as_fake()
@@ -429,18 +418,10 @@ mod tests {
                 }),
             )
             .await;
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
 
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
         let file2 = entries[1].clone();
@@ -535,7 +516,8 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_paths(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
+
         let fs = app_state.fs.as_fake();
         fs.insert_dir("/dir1").await;
         fs.insert_dir("/dir2").await;
@@ -544,17 +526,8 @@ mod tests {
         fs.insert_file("/dir2/b.txt", "".into()).await;
         fs.insert_file("/dir3/c.txt", "".into()).await;
 
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/dir1", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -655,19 +628,15 @@ mod tests {
 
     #[gpui::test]
     async fn test_save_conflicting_item(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
-        let fs = app_state.fs.as_fake();
-        fs.insert_tree("/root", json!({ "a.txt": "" })).await;
-
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
+        let app_state = init(cx);
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree("/root", json!({ "a.txt": "" }))
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -687,7 +656,11 @@ mod tests {
                 editor.handle_input(&editor::Input("x".into()), cx)
             })
         });
-        fs.insert_file("/root/a.txt", "changed".to_string()).await;
+        app_state
+            .fs
+            .as_fake()
+            .insert_file("/root/a.txt", "changed".to_string())
+            .await;
         editor
             .condition(&cx, |editor, cx| editor.has_conflict(cx))
             .await;
@@ -704,21 +677,16 @@ mod tests {
 
     #[gpui::test]
     async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
         app_state.fs.as_fake().insert_dir("/root").await;
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        project.update(cx, |project, _| project.languages().add(rust_lang()));
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew);
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -773,18 +741,11 @@ mod tests {
 
         // Open the same newly-created file in another pane item. The new editor should reuse
         // the same buffer.
-        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew);
         workspace
             .update(cx, |workspace, cx| {
                 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
-                workspace.open_path(
-                    ProjectPath {
-                        worktree_id: worktree.read(cx).id(),
-                        path: Path::new("the-new-name.rs").into(),
-                    },
-                    true,
-                    cx,
-                )
+                workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
             })
             .await
             .unwrap();
@@ -805,13 +766,15 @@ mod tests {
 
     #[gpui::test]
     async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
         app_state.fs.as_fake().insert_dir("/root").await;
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
+
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        project.update(cx, |project, _| project.languages().add(rust_lang()));
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
 
         // Create a new untitled buffer
-        cx.dispatch_action(window_id, OpenNew(app_state.clone()));
+        cx.dispatch_action(window_id, OpenNew);
         let editor = workspace.read_with(cx, |workspace, cx| {
             workspace
                 .active_item(cx)
@@ -842,10 +805,9 @@ mod tests {
 
     #[gpui::test]
     async fn test_pane_actions(cx: &mut TestAppContext) {
-        cx.foreground().forbid_parking();
+        init(cx);
 
-        cx.update(|cx| pane::init(cx));
-        let app_state = cx.update(test_app_state);
+        let app_state = cx.update(AppState::test);
         app_state
             .fs
             .as_fake()
@@ -861,17 +823,9 @@ mod tests {
             )
             .await;
 
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
+
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
 
@@ -926,7 +880,7 @@ mod tests {
 
     #[gpui::test]
     async fn test_navigation(cx: &mut TestAppContext) {
-        let app_state = cx.update(test_app_state);
+        let app_state = init(cx);
         app_state
             .fs
             .as_fake()
@@ -941,17 +895,10 @@ mod tests {
                 }),
             )
             .await;
-        let params = cx.update(|cx| WorkspaceParams::local(&app_state, cx));
-        let (_, workspace) = cx.add_window(|cx| Workspace::new(&params, cx));
-        params
-            .project
-            .update(cx, |project, cx| {
-                project.find_or_create_local_worktree("/root", true, cx)
-            })
-            .await
-            .unwrap();
-        cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
-            .await;
+
+        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+        let (_, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
+
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
         let file2 = entries[1].clone();
@@ -990,7 +937,7 @@ mod tests {
                 editor.newline(&Default::default(), cx);
                 editor.move_down(&Default::default(), cx);
                 editor.move_down(&Default::default(), cx);
-                editor.save(params.project.clone(), cx)
+                editor.save(project.clone(), cx)
             })
             .await
             .unwrap();
@@ -1104,7 +1051,6 @@ mod tests {
             .unwrap();
         app_state
             .fs
-            .as_fake()
             .remove_file(Path::new("/root/a/file2"), Default::default())
             .await
             .unwrap();
@@ -1219,4 +1165,29 @@ mod tests {
         }
         assert!(has_default_theme);
     }
+
+    fn init(cx: &mut TestAppContext) -> Arc<AppState> {
+        cx.foreground().forbid_parking();
+        cx.update(|cx| {
+            let mut app_state = AppState::test(cx);
+            let state = Arc::get_mut(&mut app_state).unwrap();
+            state.initialize_workspace = initialize_workspace;
+            state.build_window_options = build_window_options;
+            workspace::init(app_state.clone(), cx);
+            editor::init(cx);
+            pane::init(cx);
+            app_state
+        })
+    }
+
+    fn rust_lang() -> Arc<language::Language> {
+        Arc::new(language::Language::new(
+            language::LanguageConfig {
+                name: "Rust".into(),
+                path_suffixes: vec!["rs".to_string()],
+                ..Default::default()
+            },
+            Some(tree_sitter_rust::language()),
+        ))
+    }
 }

styles/nodemon.json πŸ”—

@@ -1,8 +0,0 @@
-{
-    "watch": [
-        "./**/*"
-    ],
-    "ext": "ts",
-    "ignore": [],
-    "exec": "ts-node src/buildThemes.ts"
-}

styles/package-lock.json πŸ”—

@@ -13,7 +13,6 @@
                 "@types/node": "^17.0.23",
                 "case-anything": "^2.1.10",
                 "chroma-js": "^2.4.2",
-                "nodemon": "^2.0.15",
                 "ts-node": "^10.7.0"
             }
         },
@@ -36,25 +35,6 @@
                 "node": ">=12"
             }
         },
-        "node_modules/@sindresorhus/is": {
-            "version": "0.14.0",
-            "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
-            "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/@szmarczak/http-timer": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
-            "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
-            "dependencies": {
-                "defer-to-connect": "^1.0.1"
-            },
-            "engines": {
-                "node": ">=6"
-            }
-        },
         "node_modules/@tsconfig/node10": {
             "version": "1.0.8",
             "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
@@ -85,11 +65,6 @@
             "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz",
             "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw=="
         },
-        "node_modules/abbrev": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
-            "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
-        },
         "node_modules/acorn": {
             "version": "8.7.0",
             "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
@@ -109,157 +84,11 @@
                 "node": ">=0.4.0"
             }
         },
-        "node_modules/ansi-align": {
-            "version": "3.0.1",
-            "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
-            "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
-            "dependencies": {
-                "string-width": "^4.1.0"
-            }
-        },
-        "node_modules/ansi-regex": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
-            "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/ansi-styles": {
-            "version": "4.3.0",
-            "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
-            "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-            "dependencies": {
-                "color-convert": "^2.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/ansi-styles?sponsor=1"
-            }
-        },
-        "node_modules/anymatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
-            "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
-            "dependencies": {
-                "normalize-path": "^3.0.0",
-                "picomatch": "^2.0.4"
-            },
-            "engines": {
-                "node": ">= 8"
-            }
-        },
         "node_modules/arg": {
             "version": "4.1.3",
             "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
             "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
         },
-        "node_modules/balanced-match": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-            "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
-        },
-        "node_modules/binary-extensions": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
-            "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/boxen": {
-            "version": "5.1.2",
-            "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
-            "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
-            "dependencies": {
-                "ansi-align": "^3.0.0",
-                "camelcase": "^6.2.0",
-                "chalk": "^4.1.0",
-                "cli-boxes": "^2.2.1",
-                "string-width": "^4.2.2",
-                "type-fest": "^0.20.2",
-                "widest-line": "^3.1.0",
-                "wrap-ansi": "^7.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/brace-expansion": {
-            "version": "1.1.11",
-            "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-            "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-            "dependencies": {
-                "balanced-match": "^1.0.0",
-                "concat-map": "0.0.1"
-            }
-        },
-        "node_modules/braces": {
-            "version": "3.0.2",
-            "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-            "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
-            "dependencies": {
-                "fill-range": "^7.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/cacheable-request": {
-            "version": "6.1.0",
-            "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz",
-            "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==",
-            "dependencies": {
-                "clone-response": "^1.0.2",
-                "get-stream": "^5.1.0",
-                "http-cache-semantics": "^4.0.0",
-                "keyv": "^3.0.0",
-                "lowercase-keys": "^2.0.0",
-                "normalize-url": "^4.1.0",
-                "responselike": "^1.0.2"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/cacheable-request/node_modules/get-stream": {
-            "version": "5.2.0",
-            "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
-            "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
-            "dependencies": {
-                "pump": "^3.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/cacheable-request/node_modules/lowercase-keys": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
-            "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/camelcase": {
-            "version": "6.3.0",
-            "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
-            "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
         "node_modules/case-anything": {
             "version": "2.1.10",
             "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz",
@@ -271,177 +100,16 @@
                 "url": "https://github.com/sponsors/mesqueeb"
             }
         },
-        "node_modules/chalk": {
-            "version": "4.1.2",
-            "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-            "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-            "dependencies": {
-                "ansi-styles": "^4.1.0",
-                "supports-color": "^7.1.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/chalk?sponsor=1"
-            }
-        },
-        "node_modules/chalk/node_modules/has-flag": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
-            "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/chalk/node_modules/supports-color": {
-            "version": "7.2.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
-            "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
-            "dependencies": {
-                "has-flag": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/chokidar": {
-            "version": "3.5.3",
-            "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
-            "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
-            "funding": [
-                {
-                    "type": "individual",
-                    "url": "https://paulmillr.com/funding/"
-                }
-            ],
-            "dependencies": {
-                "anymatch": "~3.1.2",
-                "braces": "~3.0.2",
-                "glob-parent": "~5.1.2",
-                "is-binary-path": "~2.1.0",
-                "is-glob": "~4.0.1",
-                "normalize-path": "~3.0.0",
-                "readdirp": "~3.6.0"
-            },
-            "engines": {
-                "node": ">= 8.10.0"
-            },
-            "optionalDependencies": {
-                "fsevents": "~2.3.2"
-            }
-        },
         "node_modules/chroma-js": {
             "version": "2.4.2",
             "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz",
             "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A=="
         },
-        "node_modules/ci-info": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz",
-            "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="
-        },
-        "node_modules/cli-boxes": {
-            "version": "2.2.1",
-            "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
-            "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
-            "engines": {
-                "node": ">=6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/clone-response": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
-            "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=",
-            "dependencies": {
-                "mimic-response": "^1.0.0"
-            }
-        },
-        "node_modules/color-convert": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
-            "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-            "dependencies": {
-                "color-name": "~1.1.4"
-            },
-            "engines": {
-                "node": ">=7.0.0"
-            }
-        },
-        "node_modules/color-name": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
-            "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
-        },
-        "node_modules/concat-map": {
-            "version": "0.0.1",
-            "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-            "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
-        },
-        "node_modules/configstore": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
-            "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==",
-            "dependencies": {
-                "dot-prop": "^5.2.0",
-                "graceful-fs": "^4.1.2",
-                "make-dir": "^3.0.0",
-                "unique-string": "^2.0.0",
-                "write-file-atomic": "^3.0.0",
-                "xdg-basedir": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
         "node_modules/create-require": {
             "version": "1.1.1",
             "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
             "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
         },
-        "node_modules/crypto-random-string": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
-            "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/debug": {
-            "version": "3.2.7",
-            "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
-            "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
-            "dependencies": {
-                "ms": "^2.1.1"
-            }
-        },
-        "node_modules/decompress-response": {
-            "version": "3.3.0",
-            "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
-            "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=",
-            "dependencies": {
-                "mimic-response": "^1.0.0"
-            },
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/deep-extend": {
-            "version": "0.6.0",
-            "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
-            "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
-            "engines": {
-                "node": ">=4.0.0"
-            }
-        },
-        "node_modules/defer-to-connect": {
-            "version": "1.1.3",
-            "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz",
-            "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ=="
-        },
         "node_modules/diff": {
             "version": "4.0.2",
             "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -450,950 +118,113 @@
                 "node": ">=0.3.1"
             }
         },
-        "node_modules/dot-prop": {
-            "version": "5.3.0",
-            "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
-            "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==",
-            "dependencies": {
-                "is-obj": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/duplexer3": {
-            "version": "0.1.4",
-            "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
-            "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
-        },
-        "node_modules/emoji-regex": {
-            "version": "8.0.0",
-            "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
-            "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
-        },
-        "node_modules/end-of-stream": {
-            "version": "1.4.4",
-            "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
-            "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
-            "dependencies": {
-                "once": "^1.4.0"
-            }
-        },
-        "node_modules/escape-goat": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz",
-            "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==",
-            "engines": {
-                "node": ">=8"
-            }
+        "node_modules/make-error": {
+            "version": "1.3.6",
+            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
         },
-        "node_modules/fill-range": {
-            "version": "7.0.1",
-            "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-            "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+        "node_modules/ts-node": {
+            "version": "10.7.0",
+            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
+            "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
             "dependencies": {
-                "to-regex-range": "^5.0.1"
+                "@cspotcode/source-map-support": "0.7.0",
+                "@tsconfig/node10": "^1.0.7",
+                "@tsconfig/node12": "^1.0.7",
+                "@tsconfig/node14": "^1.0.0",
+                "@tsconfig/node16": "^1.0.2",
+                "acorn": "^8.4.1",
+                "acorn-walk": "^8.1.1",
+                "arg": "^4.1.0",
+                "create-require": "^1.1.0",
+                "diff": "^4.0.1",
+                "make-error": "^1.1.1",
+                "v8-compile-cache-lib": "^3.0.0",
+                "yn": "3.1.1"
             },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/fsevents": {
-            "version": "2.3.2",
-            "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
-            "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
-            "hasInstallScript": true,
-            "optional": true,
-            "os": [
-                "darwin"
-            ],
-            "engines": {
-                "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-            }
-        },
-        "node_modules/get-stream": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
-            "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
-            "dependencies": {
-                "pump": "^3.0.0"
+            "bin": {
+                "ts-node": "dist/bin.js",
+                "ts-node-cwd": "dist/bin-cwd.js",
+                "ts-node-esm": "dist/bin-esm.js",
+                "ts-node-script": "dist/bin-script.js",
+                "ts-node-transpile-only": "dist/bin-transpile.js",
+                "ts-script": "dist/bin-script-deprecated.js"
             },
-            "engines": {
-                "node": ">=6"
+            "peerDependencies": {
+                "@swc/core": ">=1.2.50",
+                "@swc/wasm": ">=1.2.50",
+                "@types/node": "*",
+                "typescript": ">=2.7"
+            },
+            "peerDependenciesMeta": {
+                "@swc/core": {
+                    "optional": true
+                },
+                "@swc/wasm": {
+                    "optional": true
+                }
             }
         },
-        "node_modules/glob-parent": {
-            "version": "5.1.2",
-            "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
-            "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
-            "dependencies": {
-                "is-glob": "^4.0.1"
+        "node_modules/typescript": {
+            "version": "4.6.3",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
+            "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
+            "peer": true,
+            "bin": {
+                "tsc": "bin/tsc",
+                "tsserver": "bin/tsserver"
             },
             "engines": {
-                "node": ">= 6"
+                "node": ">=4.2.0"
             }
         },
-        "node_modules/global-dirs": {
+        "node_modules/v8-compile-cache-lib": {
             "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz",
-            "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==",
-            "dependencies": {
-                "ini": "2.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
+            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
+            "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
         },
-        "node_modules/got": {
-            "version": "9.6.0",
-            "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
-            "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==",
-            "dependencies": {
-                "@sindresorhus/is": "^0.14.0",
-                "@szmarczak/http-timer": "^1.1.2",
-                "cacheable-request": "^6.0.0",
-                "decompress-response": "^3.3.0",
-                "duplexer3": "^0.1.4",
-                "get-stream": "^4.1.0",
-                "lowercase-keys": "^1.0.1",
-                "mimic-response": "^1.0.1",
-                "p-cancelable": "^1.0.0",
-                "to-readable-stream": "^1.0.0",
-                "url-parse-lax": "^3.0.0"
-            },
+        "node_modules/yn": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
             "engines": {
-                "node": ">=8.6"
+                "node": ">=6"
             }
+        }
+    },
+    "dependencies": {
+        "@cspotcode/source-map-consumer": {
+            "version": "0.8.0",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
+            "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
         },
-        "node_modules/graceful-fs": {
-            "version": "4.2.9",
-            "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
-            "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
-        },
-        "node_modules/has-flag": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
-            "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
-            "engines": {
-                "node": ">=4"
+        "@cspotcode/source-map-support": {
+            "version": "0.7.0",
+            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
+            "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
+            "requires": {
+                "@cspotcode/source-map-consumer": "0.8.0"
             }
         },
-        "node_modules/has-yarn": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz",
-            "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==",
-            "engines": {
-                "node": ">=8"
-            }
+        "@tsconfig/node10": {
+            "version": "1.0.8",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
+            "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
         },
-        "node_modules/http-cache-semantics": {
-            "version": "4.1.0",
-            "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
-            "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
+        "@tsconfig/node12": {
+            "version": "1.0.9",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
+            "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
         },
-        "node_modules/ignore-by-default": {
+        "@tsconfig/node14": {
             "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
-            "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk="
-        },
-        "node_modules/import-lazy": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
-            "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=",
-            "engines": {
-                "node": ">=4"
-            }
+            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
+            "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
         },
-        "node_modules/imurmurhash": {
-            "version": "0.1.4",
-            "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
-            "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
-            "engines": {
-                "node": ">=0.8.19"
-            }
-        },
-        "node_modules/ini": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz",
-            "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==",
-            "engines": {
-                "node": ">=10"
-            }
-        },
-        "node_modules/is-binary-path": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
-            "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
-            "dependencies": {
-                "binary-extensions": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/is-ci": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
-            "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==",
-            "dependencies": {
-                "ci-info": "^2.0.0"
-            },
-            "bin": {
-                "is-ci": "bin.js"
-            }
-        },
-        "node_modules/is-extglob": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
-            "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-fullwidth-code-point": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
-            "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/is-glob": {
-            "version": "4.0.3",
-            "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
-            "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
-            "dependencies": {
-                "is-extglob": "^2.1.1"
-            },
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/is-installed-globally": {
-            "version": "0.4.0",
-            "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
-            "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==",
-            "dependencies": {
-                "global-dirs": "^3.0.0",
-                "is-path-inside": "^3.0.2"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/is-npm": {
-            "version": "5.0.0",
-            "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz",
-            "integrity": "sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==",
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/is-number": {
-            "version": "7.0.0",
-            "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
-            "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
-            "engines": {
-                "node": ">=0.12.0"
-            }
-        },
-        "node_modules/is-obj": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
-            "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/is-path-inside": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-            "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/is-typedarray": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
-            "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
-        },
-        "node_modules/is-yarn-global": {
-            "version": "0.3.0",
-            "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz",
-            "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw=="
-        },
-        "node_modules/json-buffer": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
-            "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg="
-        },
-        "node_modules/keyv": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
-            "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==",
-            "dependencies": {
-                "json-buffer": "3.0.0"
-            }
-        },
-        "node_modules/latest-version": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz",
-            "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==",
-            "dependencies": {
-                "package-json": "^6.3.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/lowercase-keys": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
-            "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/lru-cache": {
-            "version": "6.0.0",
-            "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
-            "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
-            "dependencies": {
-                "yallist": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            }
-        },
-        "node_modules/make-dir": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-            "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
-            "dependencies": {
-                "semver": "^6.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/make-dir/node_modules/semver": {
-            "version": "6.3.0",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/make-error": {
-            "version": "1.3.6",
-            "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
-            "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
-        },
-        "node_modules/mimic-response": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
-            "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==",
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/minimatch": {
-            "version": "3.1.2",
-            "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
-            "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
-            "dependencies": {
-                "brace-expansion": "^1.1.7"
-            },
-            "engines": {
-                "node": "*"
-            }
-        },
-        "node_modules/minimist": {
-            "version": "1.2.6",
-            "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
-            "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
-        },
-        "node_modules/ms": {
-            "version": "2.1.3",
-            "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-            "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
-        },
-        "node_modules/nodemon": {
-            "version": "2.0.15",
-            "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz",
-            "integrity": "sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA==",
-            "hasInstallScript": true,
-            "dependencies": {
-                "chokidar": "^3.5.2",
-                "debug": "^3.2.7",
-                "ignore-by-default": "^1.0.1",
-                "minimatch": "^3.0.4",
-                "pstree.remy": "^1.1.8",
-                "semver": "^5.7.1",
-                "supports-color": "^5.5.0",
-                "touch": "^3.1.0",
-                "undefsafe": "^2.0.5",
-                "update-notifier": "^5.1.0"
-            },
-            "bin": {
-                "nodemon": "bin/nodemon.js"
-            },
-            "engines": {
-                "node": ">=8.10.0"
-            },
-            "funding": {
-                "type": "opencollective",
-                "url": "https://opencollective.com/nodemon"
-            }
-        },
-        "node_modules/nopt": {
-            "version": "1.0.10",
-            "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
-            "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=",
-            "dependencies": {
-                "abbrev": "1"
-            },
-            "bin": {
-                "nopt": "bin/nopt.js"
-            },
-            "engines": {
-                "node": "*"
-            }
-        },
-        "node_modules/normalize-path": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
-            "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/normalize-url": {
-            "version": "4.5.1",
-            "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz",
-            "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/once": {
-            "version": "1.4.0",
-            "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-            "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-            "dependencies": {
-                "wrappy": "1"
-            }
-        },
-        "node_modules/p-cancelable": {
-            "version": "1.1.0",
-            "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz",
-            "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/package-json": {
-            "version": "6.5.0",
-            "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz",
-            "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==",
-            "dependencies": {
-                "got": "^9.6.0",
-                "registry-auth-token": "^4.0.0",
-                "registry-url": "^5.0.0",
-                "semver": "^6.2.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/package-json/node_modules/semver": {
-            "version": "6.3.0",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/picomatch": {
-            "version": "2.3.1",
-            "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
-            "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
-            "engines": {
-                "node": ">=8.6"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/jonschlinkert"
-            }
-        },
-        "node_modules/prepend-http": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
-            "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/pstree.remy": {
-            "version": "1.1.8",
-            "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
-            "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
-        },
-        "node_modules/pump": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
-            "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
-            "dependencies": {
-                "end-of-stream": "^1.1.0",
-                "once": "^1.3.1"
-            }
-        },
-        "node_modules/pupa": {
-            "version": "2.1.1",
-            "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
-            "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
-            "dependencies": {
-                "escape-goat": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/rc": {
-            "version": "1.2.8",
-            "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
-            "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
-            "dependencies": {
-                "deep-extend": "^0.6.0",
-                "ini": "~1.3.0",
-                "minimist": "^1.2.0",
-                "strip-json-comments": "~2.0.1"
-            },
-            "bin": {
-                "rc": "cli.js"
-            }
-        },
-        "node_modules/rc/node_modules/ini": {
-            "version": "1.3.8",
-            "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
-            "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
-        },
-        "node_modules/readdirp": {
-            "version": "3.6.0",
-            "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-            "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
-            "dependencies": {
-                "picomatch": "^2.2.1"
-            },
-            "engines": {
-                "node": ">=8.10.0"
-            }
-        },
-        "node_modules/registry-auth-token": {
-            "version": "4.2.1",
-            "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
-            "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==",
-            "dependencies": {
-                "rc": "^1.2.8"
-            },
-            "engines": {
-                "node": ">=6.0.0"
-            }
-        },
-        "node_modules/registry-url": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz",
-            "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==",
-            "dependencies": {
-                "rc": "^1.2.8"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/responselike": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz",
-            "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=",
-            "dependencies": {
-                "lowercase-keys": "^1.0.0"
-            }
-        },
-        "node_modules/semver": {
-            "version": "5.7.1",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-            "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
-            "bin": {
-                "semver": "bin/semver"
-            }
-        },
-        "node_modules/semver-diff": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz",
-            "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==",
-            "dependencies": {
-                "semver": "^6.3.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/semver-diff/node_modules/semver": {
-            "version": "6.3.0",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-            "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
-            "bin": {
-                "semver": "bin/semver.js"
-            }
-        },
-        "node_modules/signal-exit": {
-            "version": "3.0.7",
-            "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-            "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
-        },
-        "node_modules/string-width": {
-            "version": "4.2.3",
-            "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
-            "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-            "dependencies": {
-                "emoji-regex": "^8.0.0",
-                "is-fullwidth-code-point": "^3.0.0",
-                "strip-ansi": "^6.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/strip-ansi": {
-            "version": "6.0.1",
-            "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
-            "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-            "dependencies": {
-                "ansi-regex": "^5.0.1"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/strip-json-comments": {
-            "version": "2.0.1",
-            "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
-            "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
-            "engines": {
-                "node": ">=0.10.0"
-            }
-        },
-        "node_modules/supports-color": {
-            "version": "5.5.0",
-            "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
-            "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
-            "dependencies": {
-                "has-flag": "^3.0.0"
-            },
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/to-readable-stream": {
-            "version": "1.0.0",
-            "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz",
-            "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==",
-            "engines": {
-                "node": ">=6"
-            }
-        },
-        "node_modules/to-regex-range": {
-            "version": "5.0.1",
-            "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
-            "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
-            "dependencies": {
-                "is-number": "^7.0.0"
-            },
-            "engines": {
-                "node": ">=8.0"
-            }
-        },
-        "node_modules/touch": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
-            "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
-            "dependencies": {
-                "nopt": "~1.0.10"
-            },
-            "bin": {
-                "nodetouch": "bin/nodetouch.js"
-            }
-        },
-        "node_modules/ts-node": {
-            "version": "10.7.0",
-            "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.7.0.tgz",
-            "integrity": "sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A==",
-            "dependencies": {
-                "@cspotcode/source-map-support": "0.7.0",
-                "@tsconfig/node10": "^1.0.7",
-                "@tsconfig/node12": "^1.0.7",
-                "@tsconfig/node14": "^1.0.0",
-                "@tsconfig/node16": "^1.0.2",
-                "acorn": "^8.4.1",
-                "acorn-walk": "^8.1.1",
-                "arg": "^4.1.0",
-                "create-require": "^1.1.0",
-                "diff": "^4.0.1",
-                "make-error": "^1.1.1",
-                "v8-compile-cache-lib": "^3.0.0",
-                "yn": "3.1.1"
-            },
-            "bin": {
-                "ts-node": "dist/bin.js",
-                "ts-node-cwd": "dist/bin-cwd.js",
-                "ts-node-esm": "dist/bin-esm.js",
-                "ts-node-script": "dist/bin-script.js",
-                "ts-node-transpile-only": "dist/bin-transpile.js",
-                "ts-script": "dist/bin-script-deprecated.js"
-            },
-            "peerDependencies": {
-                "@swc/core": ">=1.2.50",
-                "@swc/wasm": ">=1.2.50",
-                "@types/node": "*",
-                "typescript": ">=2.7"
-            },
-            "peerDependenciesMeta": {
-                "@swc/core": {
-                    "optional": true
-                },
-                "@swc/wasm": {
-                    "optional": true
-                }
-            }
-        },
-        "node_modules/type-fest": {
-            "version": "0.20.2",
-            "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
-            "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/sindresorhus"
-            }
-        },
-        "node_modules/typedarray-to-buffer": {
-            "version": "3.1.5",
-            "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
-            "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
-            "dependencies": {
-                "is-typedarray": "^1.0.0"
-            }
-        },
-        "node_modules/typescript": {
-            "version": "4.6.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
-            "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
-            "peer": true,
-            "bin": {
-                "tsc": "bin/tsc",
-                "tsserver": "bin/tsserver"
-            },
-            "engines": {
-                "node": ">=4.2.0"
-            }
-        },
-        "node_modules/undefsafe": {
-            "version": "2.0.5",
-            "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
-            "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
-        },
-        "node_modules/unique-string": {
-            "version": "2.0.0",
-            "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz",
-            "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==",
-            "dependencies": {
-                "crypto-random-string": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/update-notifier": {
-            "version": "5.1.0",
-            "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz",
-            "integrity": "sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==",
-            "dependencies": {
-                "boxen": "^5.0.0",
-                "chalk": "^4.1.0",
-                "configstore": "^5.0.1",
-                "has-yarn": "^2.1.0",
-                "import-lazy": "^2.1.0",
-                "is-ci": "^2.0.0",
-                "is-installed-globally": "^0.4.0",
-                "is-npm": "^5.0.0",
-                "is-yarn-global": "^0.3.0",
-                "latest-version": "^5.1.0",
-                "pupa": "^2.1.1",
-                "semver": "^7.3.4",
-                "semver-diff": "^3.1.1",
-                "xdg-basedir": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/yeoman/update-notifier?sponsor=1"
-            }
-        },
-        "node_modules/update-notifier/node_modules/semver": {
-            "version": "7.3.5",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
-            "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
-            "dependencies": {
-                "lru-cache": "^6.0.0"
-            },
-            "bin": {
-                "semver": "bin/semver.js"
-            },
-            "engines": {
-                "node": ">=10"
-            }
-        },
-        "node_modules/url-parse-lax": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
-            "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
-            "dependencies": {
-                "prepend-http": "^2.0.0"
-            },
-            "engines": {
-                "node": ">=4"
-            }
-        },
-        "node_modules/v8-compile-cache-lib": {
-            "version": "3.0.0",
-            "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz",
-            "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA=="
-        },
-        "node_modules/widest-line": {
-            "version": "3.1.0",
-            "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
-            "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
-            "dependencies": {
-                "string-width": "^4.0.0"
-            },
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/wrap-ansi": {
-            "version": "7.0.0",
-            "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
-            "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
-            "dependencies": {
-                "ansi-styles": "^4.0.0",
-                "string-width": "^4.1.0",
-                "strip-ansi": "^6.0.0"
-            },
-            "engines": {
-                "node": ">=10"
-            },
-            "funding": {
-                "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
-            }
-        },
-        "node_modules/wrappy": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-            "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
-        },
-        "node_modules/write-file-atomic": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
-            "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
-            "dependencies": {
-                "imurmurhash": "^0.1.4",
-                "is-typedarray": "^1.0.0",
-                "signal-exit": "^3.0.2",
-                "typedarray-to-buffer": "^3.1.5"
-            }
-        },
-        "node_modules/xdg-basedir": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
-            "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==",
-            "engines": {
-                "node": ">=8"
-            }
-        },
-        "node_modules/yallist": {
-            "version": "4.0.0",
-            "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
-            "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
-        },
-        "node_modules/yn": {
-            "version": "3.1.1",
-            "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
-            "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
-            "engines": {
-                "node": ">=6"
-            }
-        }
-    },
-    "dependencies": {
-        "@cspotcode/source-map-consumer": {
-            "version": "0.8.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
-            "integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
-        },
-        "@cspotcode/source-map-support": {
-            "version": "0.7.0",
-            "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
-            "integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
-            "requires": {
-                "@cspotcode/source-map-consumer": "0.8.0"
-            }
-        },
-        "@sindresorhus/is": {
-            "version": "0.14.0",
-            "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
-            "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ=="
-        },
-        "@szmarczak/http-timer": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
-            "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==",
-            "requires": {
-                "defer-to-connect": "^1.0.1"
-            }
-        },
-        "@tsconfig/node10": {
-            "version": "1.0.8",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
-            "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
-        },
-        "@tsconfig/node12": {
-            "version": "1.0.9",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
-            "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
-        },
-        "@tsconfig/node14": {
-            "version": "1.0.1",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
-            "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
-        },
-        "@tsconfig/node16": {
-            "version": "1.0.2",
-            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
-            "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
+        "@tsconfig/node16": {
+            "version": "1.0.2",
+            "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
+            "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
         },
         "@types/chroma-js": {
             "version": "2.1.3",

styles/package.json πŸ”—

@@ -6,8 +6,7 @@
     "scripts": {
         "build": "npm run build-themes && npm run build-tokens",
         "build-themes": "ts-node ./src/buildThemes.ts",
-        "build-tokens": "ts-node ./src/buildTokens.ts",
-        "watch": "nodemon"
+        "build-tokens": "ts-node ./src/buildTokens.ts"
     },
     "author": "",
     "license": "ISC",
@@ -16,7 +15,6 @@
         "@types/node": "^17.0.23",
         "case-anything": "^2.1.10",
         "chroma-js": "^2.4.2",
-        "ts-node": "^10.7.0",
-        "nodemon": "^2.0.15"
+        "ts-node": "^10.7.0"
     }
 }

styles/src/buildThemes.ts πŸ”—

@@ -1,23 +1,30 @@
 import * as fs from "fs";
 import * as path from "path";
+import { tmpdir } from 'os';
 import app from "./styleTree/app";
 import themes from "./themes";
 import snakeCase from "./utils/snakeCase";
 
 const themeDirectory = `${__dirname}/../../assets/themes/`;
+const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), 'build-themes'));
 
 // Clear existing themes
 for (const file of fs.readdirSync(themeDirectory)) {
-  fs.unlinkSync(path.join(themeDirectory, file));
+  if (file.endsWith('.json')) {
+    const name = file.replace(/\.json$/, '');
+    if (!themes.find(theme => theme.name === name)) {
+      fs.unlinkSync(path.join(themeDirectory, file));
+    }
+  }
 }
 
 // Write new themes to theme directory
 for (let theme of themes) {
   let styleTree = snakeCase(app(theme));
   let styleTreeJSON = JSON.stringify(styleTree, null, 2);
-  let outPath = path.resolve(
-    `${__dirname}/../../assets/themes/${theme.name}.json`
-  );
-  fs.writeFileSync(outPath, styleTreeJSON);
+  let tempPath = path.join(tempDirectory, `${theme.name}.json`);
+  let outPath = path.join(themeDirectory, `${theme.name}.json`);
+  fs.writeFileSync(tempPath, styleTreeJSON);
+  fs.renameSync(tempPath, outPath);
   console.log(`- ${outPath} created`);
 }

styles/src/themes.ts πŸ”—

@@ -7,10 +7,12 @@ export default themes;
 
 const themesPath = path.resolve(`${__dirname}/themes`);
 for (const fileName of fs.readdirSync(themesPath)) {
+  if (fileName == "template.ts") continue;
   const filePath = path.join(themesPath, fileName);
+
   if (fs.statSync(filePath).isFile()) {
     const theme = require(filePath);
-    themes.push(theme.dark);
-    themes.push(theme.light);
+    if (theme.dark) themes.push(theme.dark);
+    if (theme.light) themes.push(theme.light);
   }
 }

styles/src/themes/andromeda.ts πŸ”—

@@ -0,0 +1,27 @@
+import chroma from "chroma-js";
+import { colorRamp, createTheme } from "./common/base16";
+
+const name = "andromeda";
+
+const ramps = {
+  neutral: chroma.scale([
+    "#1E2025",
+    "#23262E",
+    "#292E38",
+    "#2E323C",
+    "#ACA8AE",
+    "#CBC9CF",
+    "#E1DDE4",
+    "#F7F7F8",
+  ]),
+  red: colorRamp(chroma("#F92672")),
+  orange: colorRamp(chroma("#F39C12")),
+  yellow: colorRamp(chroma("#FFE66D")),
+  green: colorRamp(chroma("#96E072")),
+  cyan: colorRamp(chroma("#00E8C6")),
+  blue: colorRamp(chroma("#0CA793")),
+  violet: colorRamp(chroma("#8A3FA6")),
+  magenta: colorRamp(chroma("#C74DED")),
+};
+
+export const dark = createTheme(`${name}`, false, ramps);

styles/src/themes/brushtrees.ts πŸ”—

@@ -0,0 +1,28 @@
+import chroma from "chroma-js";
+import { colorRamp, createTheme } from "./common/base16";
+
+const name = "brush-tree";
+
+const ramps = {
+  neutral: chroma.scale([
+    "#485867",
+    "#5A6D7A",
+    "#6D828E",
+    "#8299A1",
+    "#98AFB5",
+    "#B0C5C8",
+    "#C9DBDC",
+    "#E3EFEF",
+  ]),
+  red: colorRamp(chroma("#b38686")),
+  orange: colorRamp(chroma("#d8bba2")),
+  yellow: colorRamp(chroma("#aab386")),
+  green: colorRamp(chroma("#87b386")),
+  cyan: colorRamp(chroma("#86b3b3")),
+  blue: colorRamp(chroma("#868cb3")),
+  violet: colorRamp(chroma("#b386b2")),
+  magenta: colorRamp(chroma("#b39f9f")),
+};
+
+export const dark = createTheme(`${name}-dark`, false, ramps);
+export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/cave.ts πŸ”—

@@ -4,7 +4,16 @@ import { colorRamp, createTheme } from "./common/base16";
 const name = "cave";
 
 const ramps = {
-  neutral: chroma.scale(["#19171c", "#26232a", "#585260", "#655f6d", "#7e7887", "#8b8792", "#e2dfe7", "#efecf4"]),
+  neutral: chroma.scale([
+    "#19171c",
+    "#26232a",
+    "#585260",
+    "#655f6d",
+    "#7e7887",
+    "#8b8792",
+    "#e2dfe7",
+    "#efecf4",
+  ]),
   red: colorRamp(chroma("#be4678")),
   orange: colorRamp(chroma("#aa573c")),
   yellow: colorRamp(chroma("#a06e3b")),
@@ -13,7 +22,7 @@ const ramps = {
   blue: colorRamp(chroma("#576ddb")),
   violet: colorRamp(chroma("#955ae7")),
   magenta: colorRamp(chroma("#bf40bf")),
-}
+};
 
 export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const light = createTheme(`${name}-light`, true, ramps);

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

@@ -1,5 +1,4 @@
-import chroma from "chroma-js";
-import { Scale, Color } from "chroma-js";
+import chroma, { Color, Scale } from "chroma-js";
 import { color, ColorToken, fontWeights, NumberToken } from "../../tokens";
 import { withOpacity } from "../../utils/color";
 import Theme, { buildPlayer, Syntax } from "./theme";
@@ -8,12 +7,15 @@ export function colorRamp(color: Color): Scale {
   let hue = color.hsl()[0];
   let endColor = chroma.hsl(hue, 0.88, 0.96);
   let startColor = chroma.hsl(hue, 0.68, 0.12);
-  return chroma
-    .scale([startColor, color, endColor])
-    .mode("hsl");
+  return chroma.scale([startColor, color, endColor]).mode("hsl");
 }
 
-export function createTheme(name: string, isLight: boolean, ramps: { [rampName: string]: Scale }, blend?: number): Theme {
+export function createTheme(
+  name: string,
+  isLight: boolean,
+  ramps: { [rampName: string]: Scale },
+  blend?: number
+): Theme {
   if (isLight) {
     for (var rampName in ramps) {
       ramps[rampName] = ramps[rampName].domain([1, 0]);
@@ -62,22 +64,22 @@ export function createTheme(name: string, isLight: boolean, ramps: { [rampName:
     },
     ok: {
       base: withOpacity(rampColor(ramps.green, 0.5), 0.15),
-      hovered: withOpacity(rampColor(ramps.green, 0.5), 0.20),
+      hovered: withOpacity(rampColor(ramps.green, 0.5), 0.2),
       active: withOpacity(rampColor(ramps.green, 0.5), 0.25),
     },
     error: {
       base: withOpacity(rampColor(ramps.red, 0.5), 0.15),
-      hovered: withOpacity(rampColor(ramps.red, 0.5), 0.20),
+      hovered: withOpacity(rampColor(ramps.red, 0.5), 0.2),
       active: withOpacity(rampColor(ramps.red, 0.5), 0.25),
     },
     warning: {
       base: withOpacity(rampColor(ramps.yellow, 0.5), 0.15),
-      hovered: withOpacity(rampColor(ramps.yellow, 0.5), 0.20),
+      hovered: withOpacity(rampColor(ramps.yellow, 0.5), 0.2),
       active: withOpacity(rampColor(ramps.yellow, 0.5), 0.25),
     },
     info: {
       base: withOpacity(rampColor(ramps.blue, 0.5), 0.15),
-      hovered: withOpacity(rampColor(ramps.blue, 0.5), 0.20),
+      hovered: withOpacity(rampColor(ramps.blue, 0.5), 0.2),
       active: withOpacity(rampColor(ramps.blue, 0.5), 0.25),
     },
   };
@@ -242,4 +244,4 @@ export function createTheme(name: string, isLight: boolean, ramps: { [rampName:
     player,
     shadowAlpha,
   };
-}
+}

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

@@ -4,8 +4,8 @@ import { withOpacity } from "../../utils/color";
 export interface SyntaxHighlightStyle {
   color: ColorToken;
   weight?: FontWeightToken;
-  underline?: boolean,
-  italic?: boolean,
+  underline?: boolean;
+  italic?: boolean;
 }
 
 export interface Player {
@@ -25,7 +25,7 @@ export function buildPlayer(
     cursorColor: withOpacity(color, cursorOpacity || 1.0),
     selectionColor: withOpacity(color, selectionOpacity || 0.24),
     borderColor: withOpacity(color, borderOpacity || 0.8),
-  }
+  };
 }
 
 export interface BackgroundColorSet {
@@ -56,7 +56,7 @@ export interface Syntax {
   linkText: SyntaxHighlightStyle;
 
   [key: string]: SyntaxHighlightStyle;
-};
+}
 
 export default interface Theme {
   name: string;
@@ -86,8 +86,8 @@ export default interface Theme {
     muted: ColorToken;
     active: ColorToken;
     /**
-    * Used for rendering borders on top of media like avatars, images, video, etc.
-    */
+     * Used for rendering borders on top of media like avatars, images, video, etc.
+     */
     onMedia: ColorToken;
     ok: ColorToken;
     error: ColorToken;
@@ -141,7 +141,7 @@ export default interface Theme {
     };
   };
 
-  syntax: Syntax,
+  syntax: Syntax;
 
   player: {
     1: Player;

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

@@ -0,0 +1,27 @@
+import chroma from "chroma-js";
+import { colorRamp, createTheme } from "./common/base16";
+
+const name = "rosΓ©-pine-dawn";
+
+const ramps = {
+  neutral: chroma.scale([
+    "#26233a",
+    "#555169",
+    "#575279",
+    "#6e6a86",
+    "#9893a5",
+    "#f2e9de",
+    "#fffaf3",
+    "#faf4ed",
+  ]),
+  red: colorRamp(chroma("#1f1d2e")),
+  orange: colorRamp(chroma("#b4637a")),
+  yellow: colorRamp(chroma("#ea9d34")),
+  green: colorRamp(chroma("#d7827e")),
+  cyan: colorRamp(chroma("#286983")),
+  blue: colorRamp(chroma("#56949f")),
+  violet: colorRamp(chroma("#907aa9")),
+  magenta: colorRamp(chroma("#c5c3ce")),
+};
+
+export const light = createTheme(`${name}`, true, ramps);

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

@@ -0,0 +1,27 @@
+import chroma from "chroma-js";
+import { colorRamp, createTheme } from "./common/base16";
+
+const name = "rosΓ©-pine";
+
+const ramps = {
+  neutral: chroma.scale([
+    "#191724",
+    "#1f1d2e",
+    "#26233a",
+    "#555169",
+    "#6e6a86",
+    "#e0def4",
+    "#f0f0f3",
+    "#c5c3ce",
+  ]),
+  red: colorRamp(chroma("#e2e1e7")),
+  orange: colorRamp(chroma("#eb6f92")),
+  yellow: colorRamp(chroma("#f6c177")),
+  green: colorRamp(chroma("#ebbcba")),
+  cyan: colorRamp(chroma("#31748f")),
+  blue: colorRamp(chroma("#0CA793")),
+  violet: colorRamp(chroma("#8A3FA6")),
+  magenta: colorRamp(chroma("#C74DED")),
+};
+
+export const dark = createTheme(`${name}`, false, ramps);

styles/src/themes/sandcastle.ts πŸ”—

@@ -0,0 +1,27 @@
+import chroma from "chroma-js";
+import { colorRamp, createTheme } from "./common/base16";
+
+const name = "sandcastle";
+
+const ramps = {
+  neutral: chroma.scale([
+    "#282c34",
+    "#2c323b",
+    "#3e4451",
+    "#665c54",
+    "#928374",
+    "#a89984",
+    "#d5c4a1",
+    "#fdf4c1",
+  ]),
+  red: colorRamp(chroma("#83a598")),
+  orange: colorRamp(chroma("#a07e3b")),
+  yellow: colorRamp(chroma("#a07e3b")),
+  green: colorRamp(chroma("#528b8b")),
+  cyan: colorRamp(chroma("#83a598")),
+  blue: colorRamp(chroma("#83a598")),
+  violet: colorRamp(chroma("#d75f5f")),
+  magenta: colorRamp(chroma("#a87322")),
+};
+
+export const dark = createTheme(`${name}`, false, ramps);

styles/src/themes/solarized.ts πŸ”—

@@ -4,7 +4,16 @@ import { colorRamp, createTheme } from "./common/base16";
 const name = "solarized";
 
 const ramps = {
-  neutral: chroma.scale(["#002b36", "#073642", "#586e75", "#657b83", "#839496", "#93a1a1", "#eee8d5", "#fdf6e3"]),
+  neutral: chroma.scale([
+    "#002b36",
+    "#073642",
+    "#586e75",
+    "#657b83",
+    "#839496",
+    "#93a1a1",
+    "#eee8d5",
+    "#fdf6e3",
+  ]),
   red: colorRamp(chroma("#dc322f")),
   orange: colorRamp(chroma("#cb4b16")),
   yellow: colorRamp(chroma("#b58900")),
@@ -13,7 +22,7 @@ const ramps = {
   blue: colorRamp(chroma("#268bd2")),
   violet: colorRamp(chroma("#6c71c4")),
   magenta: colorRamp(chroma("#d33682")),
-}
+};
 
 export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/sulphurpool.ts πŸ”—

@@ -4,7 +4,16 @@ import { colorRamp, createTheme } from "./common/base16";
 const name = "sulphurpool";
 
 const ramps = {
-  neutral: chroma.scale(["#202746", "#293256", "#5e6687", "#6b7394", "#898ea4", "#979db4", "#dfe2f1", "#f5f7ff"]),
+  neutral: chroma.scale([
+    "#202746",
+    "#293256",
+    "#5e6687",
+    "#6b7394",
+    "#898ea4",
+    "#979db4",
+    "#dfe2f1",
+    "#f5f7ff",
+  ]),
   red: colorRamp(chroma("#c94922")),
   orange: colorRamp(chroma("#c76b29")),
   yellow: colorRamp(chroma("#c08b30")),
@@ -13,7 +22,7 @@ const ramps = {
   blue: colorRamp(chroma("#3d8fd1")),
   violet: colorRamp(chroma("#6679cc")),
   magenta: colorRamp(chroma("#9c637a")),
-}
+};
 
 export const dark = createTheme(`${name}-dark`, false, ramps);
-export const light = createTheme(`${name}-light`, true, ramps);
+export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/summercamp.ts πŸ”—

@@ -0,0 +1,27 @@
+import chroma from "chroma-js";
+import { colorRamp, createTheme } from "./common/base16";
+
+const name = "summercamp";
+
+const ramps = {
+  neutral: chroma.scale([
+    "#1c1810",
+    "#2a261c",
+    "#3a3527",
+    "#3a3527",
+    "#5f5b45",
+    "#736e55",
+    "#bab696",
+    "#f8f5de",
+  ]),
+  red: colorRamp(chroma("#e35142")),
+  orange: colorRamp(chroma("#fba11b")),
+  yellow: colorRamp(chroma("#f2ff27")),
+  green: colorRamp(chroma("#5ceb5a")),
+  cyan: colorRamp(chroma("#5aebbc")),
+  blue: colorRamp(chroma("#489bf0")),
+  violet: colorRamp(chroma("#FF8080")),
+  magenta: colorRamp(chroma("#F69BE7")),
+};
+
+export const dark = createTheme(`${name}`, false, ramps);

styles/src/themes/summerfruit.ts πŸ”—

@@ -0,0 +1,28 @@
+import chroma from "chroma-js";
+import { colorRamp, createTheme } from "./common/base16";
+
+const name = "summerfruit";
+
+const ramps = {
+  neutral: chroma.scale([
+    "#151515",
+    "#202020",
+    "#303030",
+    "#505050",
+    "#B0B0B0",
+    "#D0D0D0",
+    "#E0E0E0",
+    "#FFFFFF",
+  ]),
+  red: colorRamp(chroma("#FF0086")),
+  orange: colorRamp(chroma("#FD8900")),
+  yellow: colorRamp(chroma("#ABA800")),
+  green: colorRamp(chroma("#00C918")),
+  cyan: colorRamp(chroma("#1FAAAA")),
+  blue: colorRamp(chroma("#3777E6")),
+  violet: colorRamp(chroma("#AD00A1")),
+  magenta: colorRamp(chroma("#CC6633")),
+};
+
+export const dark = createTheme(`${name}-dark`, false, ramps);
+export const light = createTheme(`${name}-light`, true, ramps);

styles/src/themes/template.ts πŸ”—

@@ -0,0 +1,69 @@
+/**
+ * To create a new theme duplicate this file and move into templates
+ **/
+
+import chroma from "chroma-js";
+import { colorRamp, createTheme } from "./common/base16";
+
+/**
+ * Theme Name
+ *
+ * What the theme will be called in the UI
+ * Also used to generate filenames, etc
+ **/
+
+const name = "themeName";
+
+/**
+ * Theme Colors
+ *
+ * Zed themes are based on [base16](https://github.com/chriskempson/base16)
+ * The first 8 colors ("Neutrals") are used to construct the UI background, panels, etc.
+ * The latter 8 colors ("Accents") are used for syntax themes, semantic colors, and UI states.
+ **/
+
+/**
+ * Color Ramps
+ *
+ * We use (chroma-js)[https://gka.github.io/chroma.js/] to minipulate color in themes and to build color ramps.
+ *
+ * You can use chroma-js operations on the ramps here.
+ * For example, you could use chroma.scale(...).correctLightness if your color ramps seem washed out near the ends.
+ **/
+
+// TODO: Express accents without refering to them directly by color name.
+// See common/base16.ts for where color tokens are used.
+
+const ramps = {
+  neutral: chroma.scale([
+    "#19171c", // Dark: darkest backgrounds, inputs | Light: Lightest text, active states
+    "#26232a",
+    "#585260",
+    "#655f6d",
+    "#7e7887",
+    "#8b8792",
+    "#e2dfe7",
+    "#efecf4", // Light: darkest backgrounds, inputs | Dark: Lightest text, active states
+  ]),
+  red: colorRamp(chroma("#be4678")), // Errors
+  orange: colorRamp(chroma("#aa573c")),
+  yellow: colorRamp(chroma("#a06e3b")), // Warnings
+  green: colorRamp(chroma("#2a9292")), // Positive
+  cyan: colorRamp(chroma("#398bc6")), // Player 1 (Host)
+  blue: colorRamp(chroma("#576ddb")), // Info
+  violet: colorRamp(chroma("#955ae7")),
+  magenta: colorRamp(chroma("#bf40bf")),
+};
+
+/**
+ * Theme Variants
+ *
+ * Currently we only support (and require) dark and light themes
+ * Eventually you will be able to have only a light or dark theme,
+ * and define other variants here.
+ *
+ * createTheme([name], [isLight], [arrayOfRamps])
+ **/
+
+export const dark = createTheme(`${name}-dark`, false, ramps);
+export const light = createTheme(`${name}-light`, true, ramps);