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

Antonio Scandurra created

Change summary

Cargo.lock                                       | 21 ---
Cargo.toml                                       |  3 
Dockerfile                                       |  2 
assets/settings/initial_user_settings.json       |  6 
crates/ai/Cargo.toml                             |  1 
crates/assets/Cargo.toml                         | 14 --
crates/assets/build.rs                           | 29 ------
crates/copilot_button/Cargo.toml                 |  1 
crates/copilot_button/src/copilot_button.rs      |  4 
crates/language/src/buffer.rs                    | 13 ++
crates/language/src/buffer_tests.rs              | 46 +++++++++
crates/language/src/language.rs                  | 85 ++++++++++++-----
crates/project/src/project.rs                    | 27 +++++
crates/settings/Cargo.toml                       |  2 
crates/settings/src/keymap_file.rs               | 76 +++++++--------
crates/settings/src/settings.rs                  | 38 ++++---
crates/settings/src/settings_file.rs             |  3 
crates/util/Cargo.toml                           |  1 
crates/util/src/util.rs                          |  9 +
crates/vim/Cargo.toml                            |  1 
crates/vim/src/test/vim_test_context.rs          |  2 
crates/workspace/Cargo.toml                      |  1 
crates/zed/Cargo.toml                            |  3 
crates/zed/build.rs                              | 30 ++++++
crates/zed/src/assets.rs                         |  4 
crates/zed/src/languages.rs                      |  6 
crates/zed/src/languages/elixir/highlights.scm   | 47 ++++-----
crates/zed/src/languages/elixir/indents.scm      |  4 
crates/zed/src/languages/elixir/outline.scm      | 14 ++
crates/zed/src/languages/json.rs                 |  4 
crates/zed/src/languages/markdown/highlights.scm | 10 +-
crates/zed/src/languages/typescript.rs           |  6 
crates/zed/src/main.rs                           | 25 ++++
crates/zed/src/zed.rs                            | 39 +++-----
rust-toolchain.toml                              |  4 
35 files changed, 343 insertions(+), 238 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -100,7 +100,6 @@ name = "ai"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "assets",
  "chrono",
  "collections",
  "editor",
@@ -220,15 +219,6 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
 
-[[package]]
-name = "assets"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "gpui",
- "rust-embed",
-]
-
 [[package]]
 name = "async-broadcast"
 version = "0.4.1"
@@ -1429,7 +1419,6 @@ name = "copilot_button"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "assets",
  "context_menu",
  "copilot",
  "editor",
@@ -6151,7 +6140,6 @@ name = "settings"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "assets",
  "collections",
  "fs",
  "futures 0.3.28",
@@ -6160,6 +6148,7 @@ dependencies = [
  "lazy_static",
  "postage",
  "pretty_assertions",
+ "rust-embed",
  "schemars",
  "serde",
  "serde_derive",
@@ -7404,8 +7393,8 @@ dependencies = [
 
 [[package]]
 name = "tree-sitter-elixir"
-version = "0.19.0"
-source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=05e3631c6a0701c1fa518b0fee7be95a2ceef5e2#05e3631c6a0701c1fa518b0fee7be95a2ceef5e2"
+version = "0.1.0"
+source = "git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e#4ba9dab6e2602960d95b2b625f3386c27e08084e"
 dependencies = [
  "cc",
  "tree-sitter",
@@ -7801,6 +7790,7 @@ dependencies = [
  "lazy_static",
  "log",
  "rand 0.8.5",
+ "rust-embed",
  "serde",
  "serde_json",
  "smol",
@@ -7871,7 +7861,6 @@ name = "vim"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "assets",
  "async-compat",
  "async-trait",
  "collections",
@@ -8699,7 +8688,6 @@ name = "workspace"
 version = "0.1.0"
 dependencies = [
  "anyhow",
- "assets",
  "async-recursion 1.0.4",
  "bincode",
  "call",
@@ -8799,7 +8787,6 @@ dependencies = [
  "activity_indicator",
  "ai",
  "anyhow",
- "assets",
  "async-compression",
  "async-recursion 0.3.2",
  "async-tar",

Cargo.toml 🔗

@@ -2,7 +2,6 @@
 members = [
     "crates/activity_indicator",
     "crates/ai",
-    "crates/assets",
     "crates/auto_update",
     "crates/breadcrumbs",
     "crates/call",
@@ -88,6 +87,7 @@ parking_lot = { version = "0.11.1" }
 postage = { version = "0.5", features = ["futures-traits"] }
 rand = { version = "0.8.5" }
 regex = { version = "1.5" }
+rust-embed = { version = "6.3", features = ["include-exclude"] }
 schemars = { version = "0.8" }
 serde = { version = "1.0", features = ["derive", "rc"] }
 serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -116,3 +116,4 @@ split-debuginfo = "unpacked"
 
 [profile.release]
 debug = true
+lto = "thin"

Dockerfile 🔗

@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.2
 
-FROM rust:1.65-bullseye as builder
+FROM rust:1.70-bullseye as builder
 WORKDIR app
 COPY . .
 

assets/settings/initial_user_settings.json 🔗

@@ -1,7 +1,7 @@
-// Zed settings
+// Folder-specific settings
 //
-// For information on how to configure Zed, see the Zed
-// documentation: https://zed.dev/docs/configuring-zed
+// For a full list of overridable settings, and general information on folder-specific settings, see the documentation:
+// https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings
 //
 // To see all of Zed's default settings without changing your
 // custom settings, run the `open default settings` command

crates/ai/Cargo.toml 🔗

@@ -9,7 +9,6 @@ path = "src/ai.rs"
 doctest = false
 
 [dependencies]
-assets = { path = "../assets"}
 collections = { path = "../collections"}
 editor = { path = "../editor" }
 fs = { path = "../fs" }

crates/assets/Cargo.toml 🔗

@@ -1,14 +0,0 @@
-[package]
-name = "assets"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/assets.rs"
-doctest = false
-
-[dependencies]
-gpui = { path = "../gpui" }
-anyhow.workspace = true
-rust-embed = { version = "6.3", features = ["include-exclude"] }

crates/assets/build.rs 🔗

@@ -1,29 +0,0 @@
-use std::process::Command;
-
-fn main() {
-    let output = Command::new("npm")
-        .current_dir("../../styles")
-        .args(["install", "--no-save"])
-        .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"])
-        .output()
-        .expect("failed to run npm");
-    if !output.status.success() {
-        panic!(
-            "build script failed {}",
-            String::from_utf8_lossy(&output.stderr)
-        );
-    }
-
-    println!("cargo:rerun-if-changed=../../styles/src");
-}

crates/copilot_button/Cargo.toml 🔗

@@ -9,7 +9,6 @@ path = "src/copilot_button.rs"
 doctest = false
 
 [dependencies]
-assets = { path = "../assets" }
 copilot = { path = "../copilot" }
 editor = { path = "../editor" }
 fs = { path = "../fs" }

crates/copilot_button/src/copilot_button.rs 🔗

@@ -315,9 +315,7 @@ async fn configure_disabled_globs(
     let settings_editor = workspace
         .update(&mut cx, |_, cx| {
             create_and_open_local_file(&paths::SETTINGS, cx, || {
-                settings::initial_user_settings_content(&assets::Assets)
-                    .as_ref()
-                    .into()
+                settings::initial_user_settings_content().as_ref().into()
             })
         })?
         .await?

crates/language/src/buffer.rs 🔗

@@ -2253,7 +2253,7 @@ impl BufferSnapshot {
     }
 
     pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
-        self.outline_items_containing(0..self.len(), theme)
+        self.outline_items_containing(0..self.len(), true, theme)
             .map(Outline::new)
     }
 
@@ -2265,6 +2265,7 @@ impl BufferSnapshot {
         let position = position.to_offset(self);
         let mut items = self.outline_items_containing(
             position.saturating_sub(1)..self.len().min(position + 1),
+            false,
             theme,
         )?;
         let mut prev_depth = None;
@@ -2279,6 +2280,7 @@ impl BufferSnapshot {
     fn outline_items_containing(
         &self,
         range: Range<usize>,
+        include_extra_context: bool,
         theme: Option<&SyntaxTheme>,
     ) -> Option<Vec<OutlineItem<Anchor>>> {
         let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
@@ -2313,7 +2315,10 @@ impl BufferSnapshot {
                 let node_is_name;
                 if capture.index == config.name_capture_ix {
                     node_is_name = true;
-                } else if Some(capture.index) == config.context_capture_ix {
+                } else if Some(capture.index) == config.context_capture_ix
+                    || (Some(capture.index) == config.extra_context_capture_ix
+                        && include_extra_context)
+                {
                     node_is_name = false;
                 } else {
                     continue;
@@ -2340,10 +2345,12 @@ impl BufferSnapshot {
                 buffer_ranges.first().unwrap().0.start..buffer_ranges.last().unwrap().0.end,
                 true,
             );
+            let mut last_buffer_range_end = 0;
             for (buffer_range, is_name) in buffer_ranges {
-                if !text.is_empty() {
+                if !text.is_empty() && buffer_range.start > last_buffer_range_end {
                     text.push(' ');
                 }
+                last_buffer_range_end = buffer_range.end;
                 if is_name {
                     let mut start = text.len();
                     let end = start + buffer_range.len();

crates/language/src/buffer_tests.rs 🔗

@@ -592,6 +592,52 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
     );
 }
 
+#[gpui::test]
+async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
+    let language = javascript_lang()
+        .with_outline_query(
+            r#"
+            (function_declaration
+                "function" @context
+                name: (_) @name
+                parameters: (formal_parameters
+                    "(" @context.extra
+                    ")" @context.extra)) @item
+            "#,
+        )
+        .unwrap();
+
+    let text = r#"
+        function a() {}
+        function b(c) {}
+    "#
+    .unindent();
+
+    let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx));
+    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
+
+    // extra context nodes are included in the outline.
+    let outline = snapshot.outline(None).unwrap();
+    assert_eq!(
+        outline
+            .items
+            .iter()
+            .map(|item| (item.text.as_str(), item.depth))
+            .collect::<Vec<_>>(),
+        &[("function a()", 0), ("function b( )", 0),]
+    );
+
+    // extra context nodes do not appear in breadcrumbs.
+    let symbols = snapshot.symbols_containing(3, None).unwrap();
+    assert_eq!(
+        symbols
+            .iter()
+            .map(|item| (item.text.as_str(), item.depth))
+            .collect::<Vec<_>>(),
+        &[("function a", 0)]
+    );
+}
+
 #[gpui::test]
 async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
     let text = r#"

crates/language/src/language.rs 🔗

@@ -34,7 +34,7 @@ use std::{
     fmt::Debug,
     hash::Hash,
     mem,
-    ops::Range,
+    ops::{Not, Range},
     path::{Path, PathBuf},
     str,
     sync::{
@@ -455,6 +455,7 @@ struct OutlineConfig {
     item_capture_ix: u32,
     name_capture_ix: u32,
     context_capture_ix: Option<u32>,
+    extra_context_capture_ix: Option<u32>,
 }
 
 struct InjectionConfig {
@@ -500,6 +501,7 @@ struct AvailableLanguage {
     grammar: tree_sitter::Language,
     lsp_adapters: Vec<Arc<dyn LspAdapter>>,
     get_queries: fn(&str) -> LanguageQueries,
+    loaded: bool,
 }
 
 pub struct LanguageRegistry {
@@ -527,6 +529,7 @@ struct LanguageRegistryState {
     subscription: (watch::Sender<()>, watch::Receiver<()>),
     theme: Option<Arc<Theme>>,
     version: usize,
+    reload_count: usize,
 }
 
 pub struct PendingLanguageServer {
@@ -547,6 +550,7 @@ impl LanguageRegistry {
                 subscription: watch::channel(),
                 theme: Default::default(),
                 version: 0,
+                reload_count: 0,
             }),
             language_server_download_dir: None,
             lsp_binary_statuses_tx,
@@ -566,6 +570,14 @@ impl LanguageRegistry {
         self.executor = Some(executor);
     }
 
+    /// Clear out all of the loaded languages and reload them from scratch.
+    ///
+    /// This is useful in development, when queries have changed.
+    #[cfg(debug_assertions)]
+    pub fn reload(&self) {
+        self.state.write().reload();
+    }
+
     pub fn register(
         &self,
         path: &'static str,
@@ -582,6 +594,7 @@ impl LanguageRegistry {
             grammar,
             lsp_adapters,
             get_queries,
+            loaded: false,
         });
     }
 
@@ -590,7 +603,7 @@ impl LanguageRegistry {
         let mut result = state
             .available_languages
             .iter()
-            .map(|l| l.config.name.to_string())
+            .filter_map(|l| l.loaded.not().then_some(l.config.name.to_string()))
             .chain(state.languages.iter().map(|l| l.config.name.to_string()))
             .collect::<Vec<_>>();
         result.sort_unstable_by_key(|language_name| language_name.to_lowercase());
@@ -603,6 +616,7 @@ impl LanguageRegistry {
             state
                 .available_languages
                 .iter()
+                .filter(|l| !l.loaded)
                 .flat_map(|l| l.lsp_adapters.clone())
                 .chain(
                     state
@@ -639,10 +653,17 @@ impl LanguageRegistry {
         self.state.read().subscription.1.clone()
     }
 
+    /// The number of times that the registry has been changed,
+    /// by adding languages or reloading.
     pub fn version(&self) -> usize {
         self.state.read().version
     }
 
+    /// The number of times that the registry has been reloaded.
+    pub fn reload_count(&self) -> usize {
+        self.state.read().reload_count
+    }
+
     pub fn set_theme(&self, theme: Arc<Theme>) {
         let mut state = self.state.write();
         state.theme = Some(theme.clone());
@@ -721,7 +742,7 @@ impl LanguageRegistry {
             if let Some(language) = state
                 .available_languages
                 .iter()
-                .find(|l| callback(&l.config))
+                .find(|l| !l.loaded && callback(&l.config))
                 .cloned()
             {
                 let txs = state
@@ -743,9 +764,7 @@ impl LanguageRegistry {
                                         let language = Arc::new(language);
                                         let mut state = this.state.write();
                                         state.add(language.clone());
-                                        state
-                                            .available_languages
-                                            .retain(|language| language.id != id);
+                                        state.mark_language_loaded(id);
                                         if let Some(mut txs) = state.loading_languages.remove(&id) {
                                             for tx in txs.drain(..) {
                                                 let _ = tx.send(Ok(language.clone()));
@@ -753,10 +772,9 @@ impl LanguageRegistry {
                                         }
                                     }
                                     Err(err) => {
+                                        log::error!("failed to load language {name} - {err}");
                                         let mut state = this.state.write();
-                                        state
-                                            .available_languages
-                                            .retain(|language| language.id != id);
+                                        state.mark_language_loaded(id);
                                         if let Some(mut txs) = state.loading_languages.remove(&id) {
                                             for tx in txs.drain(..) {
                                                 let _ = tx.send(Err(anyhow!(
@@ -905,6 +923,28 @@ impl LanguageRegistryState {
         self.version += 1;
         *self.subscription.0.borrow_mut() = ();
     }
+
+    #[cfg(debug_assertions)]
+    fn reload(&mut self) {
+        self.languages.clear();
+        self.version += 1;
+        self.reload_count += 1;
+        for language in &mut self.available_languages {
+            language.loaded = false;
+        }
+        *self.subscription.0.borrow_mut() = ();
+    }
+
+    /// Mark the given language a having been loaded, so that the
+    /// language registry won't try to load it again.
+    fn mark_language_loaded(&mut self, id: AvailableLanguageId) {
+        for language in &mut self.available_languages {
+            if language.id == id {
+                language.loaded = true;
+                break;
+            }
+        }
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]
@@ -1021,34 +1061,22 @@ impl Language {
 
     pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
         if let Some(query) = queries.highlights {
-            self = self
-                .with_highlights_query(query.as_ref())
-                .expect("failed to evaluate highlights query");
+            self = self.with_highlights_query(query.as_ref())?;
         }
         if let Some(query) = queries.brackets {
-            self = self
-                .with_brackets_query(query.as_ref())
-                .expect("failed to load brackets query");
+            self = self.with_brackets_query(query.as_ref())?;
         }
         if let Some(query) = queries.indents {
-            self = self
-                .with_indents_query(query.as_ref())
-                .expect("failed to load indents query");
+            self = self.with_indents_query(query.as_ref())?;
         }
         if let Some(query) = queries.outline {
-            self = self
-                .with_outline_query(query.as_ref())
-                .expect("failed to load outline query");
+            self = self.with_outline_query(query.as_ref())?;
         }
         if let Some(query) = queries.injections {
-            self = self
-                .with_injection_query(query.as_ref())
-                .expect("failed to load injection query");
+            self = self.with_injection_query(query.as_ref())?;
         }
         if let Some(query) = queries.overrides {
-            self = self
-                .with_override_query(query.as_ref())
-                .expect("failed to load override query");
+            self = self.with_override_query(query.as_ref())?;
         }
         Ok(self)
     }
@@ -1064,12 +1092,14 @@ impl Language {
         let mut item_capture_ix = None;
         let mut name_capture_ix = None;
         let mut context_capture_ix = None;
+        let mut extra_context_capture_ix = None;
         get_capture_indices(
             &query,
             &mut [
                 ("item", &mut item_capture_ix),
                 ("name", &mut name_capture_ix),
                 ("context", &mut context_capture_ix),
+                ("context.extra", &mut extra_context_capture_ix),
             ],
         );
         if let Some((item_capture_ix, name_capture_ix)) = item_capture_ix.zip(name_capture_ix) {
@@ -1078,6 +1108,7 @@ impl Language {
                 item_capture_ix,
                 name_capture_ix,
                 context_capture_ix,
+                extra_context_capture_ix,
             });
         }
         Ok(self)

crates/project/src/project.rs 🔗

@@ -523,7 +523,7 @@ impl Project {
                 _subscriptions: vec![
                     cx.observe_global::<SettingsStore, _>(Self::on_settings_changed)
                 ],
-                _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
+                _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
                 _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
                 active_entry: None,
                 languages,
@@ -592,7 +592,7 @@ impl Project {
                 active_entry: None,
                 collaborators: Default::default(),
                 join_project_response_message_id: response.message_id,
-                _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx),
+                _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
                 _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx),
                 languages,
                 user_store: user_store.clone(),
@@ -2238,13 +2238,34 @@ impl Project {
     }
 
     fn maintain_buffer_languages(
-        languages: &LanguageRegistry,
+        languages: Arc<LanguageRegistry>,
         cx: &mut ModelContext<Project>,
     ) -> Task<()> {
         let mut subscription = languages.subscribe();
+        let mut prev_reload_count = languages.reload_count();
         cx.spawn_weak(|project, mut cx| async move {
             while let Some(()) = subscription.next().await {
                 if let Some(project) = project.upgrade(&cx) {
+                    // If the language registry has been reloaded, then remove and
+                    // re-assign the languages on all open buffers.
+                    let reload_count = languages.reload_count();
+                    if reload_count > prev_reload_count {
+                        prev_reload_count = reload_count;
+                        project.update(&mut cx, |this, cx| {
+                            let buffers = this
+                                .opened_buffers
+                                .values()
+                                .filter_map(|b| b.upgrade(cx))
+                                .collect::<Vec<_>>();
+                            for buffer in buffers {
+                                if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() {
+                                    this.unregister_buffer_from_language_servers(&buffer, &f, cx);
+                                    buffer.update(cx, |buffer, cx| buffer.set_language(None, cx));
+                                }
+                            }
+                        });
+                    }
+
                     project.update(&mut cx, |project, cx| {
                         let mut plain_text_buffers = Vec::new();
                         let mut buffers_with_unknown_injections = Vec::new();

crates/settings/Cargo.toml 🔗

@@ -12,7 +12,6 @@ doctest = false
 test-support = ["gpui/test-support", "fs/test-support"]
 
 [dependencies]
-assets = { path = "../assets" }
 collections = { path = "../collections" }
 gpui = { path = "../gpui" }
 sqlez = { path = "../sqlez" }
@@ -25,6 +24,7 @@ futures.workspace = true
 json_comments = "0.2"
 lazy_static.workspace = true
 postage.workspace = true
+rust-embed.workspace = true
 schemars.workspace = true
 serde.workspace = true
 serde_derive.workspace = true

crates/settings/src/keymap_file.rs 🔗

@@ -1,6 +1,5 @@
-use crate::settings_store::parse_json_with_comments;
+use crate::{settings_store::parse_json_with_comments, SettingsAssets};
 use anyhow::{Context, Result};
-use assets::Assets;
 use collections::BTreeMap;
 use gpui::{keymap_matcher::Binding, AppContext};
 use schemars::{
@@ -10,11 +9,11 @@ use schemars::{
 };
 use serde::Deserialize;
 use serde_json::{value::RawValue, Value};
-use util::ResultExt;
+use util::{asset_str, ResultExt};
 
 #[derive(Deserialize, Default, Clone, JsonSchema)]
 #[serde(transparent)]
-pub struct KeymapFileContent(Vec<KeymapBlock>);
+pub struct KeymapFile(Vec<KeymapBlock>);
 
 #[derive(Deserialize, Default, Clone, JsonSchema)]
 pub struct KeymapBlock {
@@ -40,11 +39,10 @@ impl JsonSchema for KeymapAction {
 #[derive(Deserialize)]
 struct ActionWithData(Box<str>, Box<RawValue>);
 
-impl KeymapFileContent {
+impl KeymapFile {
     pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
-        let content = Assets::get(asset_path).unwrap().data;
-        let content_str = std::str::from_utf8(content.as_ref()).unwrap();
-        Self::parse(content_str)?.add_to_cx(cx)
+        let content = asset_str::<SettingsAssets>(asset_path);
+        Self::parse(content.as_ref())?.add_to_cx(cx)
     }
 
     pub fn parse(content: &str) -> Result<Self> {
@@ -83,40 +81,40 @@ impl KeymapFileContent {
         }
         Ok(())
     }
-}
 
-pub fn keymap_file_json_schema(action_names: &[&'static str]) -> serde_json::Value {
-    let mut root_schema = SchemaSettings::draft07()
-        .with(|settings| settings.option_add_null_type = false)
-        .into_generator()
-        .into_root_schema_for::<KeymapFileContent>();
+    pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
+        let mut root_schema = SchemaSettings::draft07()
+            .with(|settings| settings.option_add_null_type = false)
+            .into_generator()
+            .into_root_schema_for::<KeymapFile>();
 
-    let action_schema = Schema::Object(SchemaObject {
-        subschemas: Some(Box::new(SubschemaValidation {
-            one_of: Some(vec![
-                Schema::Object(SchemaObject {
-                    instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
-                    enum_values: Some(
-                        action_names
-                            .iter()
-                            .map(|name| Value::String(name.to_string()))
-                            .collect(),
-                    ),
-                    ..Default::default()
-                }),
-                Schema::Object(SchemaObject {
-                    instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
-                    ..Default::default()
-                }),
-            ]),
+        let action_schema = Schema::Object(SchemaObject {
+            subschemas: Some(Box::new(SubschemaValidation {
+                one_of: Some(vec![
+                    Schema::Object(SchemaObject {
+                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
+                        enum_values: Some(
+                            action_names
+                                .iter()
+                                .map(|name| Value::String(name.to_string()))
+                                .collect(),
+                        ),
+                        ..Default::default()
+                    }),
+                    Schema::Object(SchemaObject {
+                        instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
+                        ..Default::default()
+                    }),
+                ]),
+                ..Default::default()
+            })),
             ..Default::default()
-        })),
-        ..Default::default()
-    });
+        });
 
-    root_schema
-        .definitions
-        .insert("KeymapAction".to_owned(), action_schema);
+        root_schema
+            .definitions
+            .insert("KeymapAction".to_owned(), action_schema);
 
-    serde_json::to_value(root_schema).unwrap()
+        serde_json::to_value(root_schema).unwrap()
+    }
 }

crates/settings/src/settings.rs 🔗

@@ -2,31 +2,37 @@ mod keymap_file;
 mod settings_file;
 mod settings_store;
 
-use gpui::AssetSource;
-pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
+use rust_embed::RustEmbed;
+use std::{borrow::Cow, str};
+use util::asset_str;
+
+pub use keymap_file::KeymapFile;
 pub use settings_file::*;
 pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
-use std::{borrow::Cow, str};
 
-pub const DEFAULT_SETTINGS_ASSET_PATH: &str = "settings/default.json";
-const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settings.json";
-const INITIAL_LOCAL_SETTINGS_ASSET_PATH: &str = "settings/initial_local_settings.json";
+#[derive(RustEmbed)]
+#[folder = "../../assets"]
+#[include = "settings/*"]
+#[include = "keymaps/*"]
+#[exclude = "*.DS_Store"]
+pub struct SettingsAssets;
 
 pub fn default_settings() -> Cow<'static, str> {
-    asset_str(&assets::Assets, DEFAULT_SETTINGS_ASSET_PATH)
+    asset_str::<SettingsAssets>("settings/default.json")
+}
+
+pub fn default_keymap() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("keymaps/default.json")
 }
 
-pub fn initial_user_settings_content(assets: &dyn AssetSource) -> Cow<'_, str> {
-    asset_str(assets, INITIAL_USER_SETTINGS_ASSET_PATH)
+pub fn vim_keymap() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("keymaps/vim.json")
 }
 
-pub fn initial_local_settings_content(assets: &dyn AssetSource) -> Cow<'_, str> {
-    asset_str(assets, INITIAL_LOCAL_SETTINGS_ASSET_PATH)
+pub fn initial_user_settings_content() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("settings/initial_user_settings.json")
 }
 
-fn asset_str<'a>(assets: &'a dyn AssetSource, path: &str) -> Cow<'a, str> {
-    match assets.load(path).unwrap() {
-        Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
-        Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
-    }
+pub fn initial_local_settings_content() -> Cow<'static, str> {
+    asset_str::<SettingsAssets>("settings/initial_local_settings.json")
 }

crates/settings/src/settings_file.rs 🔗

@@ -1,6 +1,5 @@
 use crate::{settings_store::SettingsStore, Setting};
 use anyhow::Result;
-use assets::Assets;
 use fs::Fs;
 use futures::{channel::mpsc, StreamExt};
 use gpui::{executor::Background, AppContext};
@@ -111,7 +110,7 @@ async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
         Err(err) => {
             if let Some(e) = err.downcast_ref::<std::io::Error>() {
                 if e.kind() == ErrorKind::NotFound {
-                    return Ok(crate::initial_user_settings_content(&Assets).to_string());
+                    return Ok(crate::initial_user_settings_content().to_string());
                 }
             }
             return Err(err);

crates/util/Cargo.toml 🔗

@@ -21,6 +21,7 @@ isahc.workspace = true
 smol.workspace = true
 url = "2.2"
 rand.workspace = true
+rust-embed.workspace = true
 tempdir = { workspace = true, optional = true }
 serde.workspace = true
 serde_json.workspace = true

crates/util/src/util.rs 🔗

@@ -7,6 +7,7 @@ pub mod paths;
 pub mod test;
 
 use std::{
+    borrow::Cow,
     cmp::{self, Ordering},
     ops::{AddAssign, Range, RangeInclusive},
     panic::Location,
@@ -284,6 +285,14 @@ impl<T: Rng> Iterator for RandomCharIter<T> {
     }
 }
 
+/// Get an embedded file as a string.
+pub fn asset_str<A: rust_embed::RustEmbed>(path: &str) -> Cow<'static, str> {
+    match A::get(path).unwrap().data {
+        Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()),
+        Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()),
+    }
+}
+
 // copy unstable standard feature option unzip
 // https://github.com/rust-lang/rust/issues/87800
 // Remove when this ship in Rust 1.66 or 1.67

crates/vim/Cargo.toml 🔗

@@ -24,7 +24,6 @@ nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", f
 tokio = { version = "1.15", "optional" = true }
 serde_json.workspace = true
 
-assets = { path = "../assets" }
 collections = { path = "../collections" }
 command_palette = { path = "../command_palette" }
 editor = { path = "../editor" }

crates/vim/src/test/vim_test_context.rs 🔗

@@ -27,7 +27,7 @@ impl<'a> VimTestContext<'a> {
             cx.update_global(|store: &mut SettingsStore, cx| {
                 store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
             });
-            settings::KeymapFileContent::load_asset("keymaps/vim.json", cx).unwrap();
+            settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
         });
 
         // Setup search toolbars and keypress hook

crates/workspace/Cargo.toml 🔗

@@ -19,7 +19,6 @@ test-support = [
 ]
 
 [dependencies]
-assets = { path = "../assets" }
 db = { path = "../db" }
 call = { path = "../call" }
 client = { path = "../client" }

crates/zed/Cargo.toml 🔗

@@ -17,7 +17,6 @@ path = "src/main.rs"
 
 [dependencies]
 activity_indicator = { path = "../activity_indicator" }
-assets = { path = "../assets" }
 auto_update = { path = "../auto_update" }
 breadcrumbs = { path = "../breadcrumbs" }
 call = { path = "../call" }
@@ -107,7 +106,7 @@ tree-sitter = "0.20"
 tree-sitter-c = "0.20.1"
 tree-sitter-cpp = "0.20.0"
 tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
-tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
+tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" }
 tree-sitter-embedded-template = "0.20.0"
 tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
 tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }

crates/zed/build.rs 🔗

@@ -1,3 +1,5 @@
+use std::process::Command;
+
 fn main() {
     println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
 
@@ -21,4 +23,32 @@ fn main() {
 
     // Register exported Objective-C selectors, protocols, etc
     println!("cargo:rustc-link-arg=-Wl,-ObjC");
+
+    // Install dependencies for theme-generation
+    let output = Command::new("npm")
+        .current_dir("../../styles")
+        .args(["install", "--no-save"])
+        .output()
+        .expect("failed to run npm");
+    if !output.status.success() {
+        panic!(
+            "failed to install theme dependencies {}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+    }
+
+    // Regenerate themes
+    let output = Command::new("npm")
+        .current_dir("../../styles")
+        .args(["run", "build"])
+        .output()
+        .expect("failed to run npm");
+    if !output.status.success() {
+        panic!(
+            "build script failed {}",
+            String::from_utf8_lossy(&output.stderr)
+        );
+    }
+
+    println!("cargo:rerun-if-changed=../../styles/src");
 }

crates/assets/src/assets.rs → crates/zed/src/assets.rs 🔗

@@ -4,6 +4,10 @@ use rust_embed::RustEmbed;
 
 #[derive(RustEmbed)]
 #[folder = "../../assets"]
+#[include = "fonts/**/*"]
+#[include = "icons/**/*"]
+#[include = "themes/**/*"]
+#[include = "*.md"]
 #[exclude = "*.DS_Store"]
 pub struct Assets;
 

crates/zed/src/languages.rs 🔗

@@ -3,6 +3,7 @@ pub use language::*;
 use node_runtime::NodeRuntime;
 use rust_embed::RustEmbed;
 use std::{borrow::Cow, str, sync::Arc};
+use util::asset_str;
 
 mod c;
 mod elixir;
@@ -179,10 +180,7 @@ fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {
     for path in LanguageDir::iter() {
         if let Some(remainder) = path.strip_prefix(name) {
             if remainder.starts_with(filename_prefix) {
-                let contents = match LanguageDir::get(path.as_ref()).unwrap().data {
-                    Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()),
-                    Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()),
-                };
+                let contents = asset_str::<LanguageDir>(path.as_ref());
                 match &mut result {
                     None => result = Some(contents),
                     Some(r) => r.to_mut().push_str(contents.as_ref()),

crates/zed/src/languages/elixir/highlights.scm 🔗

@@ -1,20 +1,5 @@
 ["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
 
-(unary_operator
-  operator: "@" @comment.doc
-  operand: (call
-    target: (identifier) @comment.doc.__attribute__
-    (arguments
-      [
-        (string) @comment.doc
-        (charlist) @comment.doc
-        (sigil
-          quoted_start: _ @comment.doc
-          quoted_end: _ @comment.doc) @comment.doc
-        (boolean) @comment.doc
-      ]))
-  (#match? @comment.doc.__attribute__ "^(moduledoc|typedoc|doc)$"))
-
 (unary_operator
   operator: "&"
   operand: (integer) @operator)
@@ -84,6 +69,11 @@
   quoted_start: _ @string.special
   quoted_end: _ @string.special) @string.special
 
+(
+  (identifier) @comment.unused
+  (#match? @comment.unused "^_")
+)
+
 (call
   target: [
     (identifier) @function
@@ -99,17 +89,12 @@
       (binary_operator
         left: (identifier) @function
         operator: "when")
+      (binary_operator
+        operator: "|>"
+        right: (identifier))
     ])
   (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
 
-(call
-  target: (identifier) @keyword
-  (arguments
-    (binary_operator
-      operator: "|>"
-      right: (identifier)))
-  (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
-
 (binary_operator
   operator: "|>"
   right: (identifier) @function)
@@ -127,10 +112,18 @@
   (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
 )
 
-(
-  (identifier) @comment.unused
-  (#match? @comment.unused "^_")
-)
+(unary_operator
+  operator: "@" @comment.doc
+  operand: (call
+    target: (identifier) @__attribute__ @comment.doc
+    (arguments
+      [
+        (string)
+        (charlist)
+        (sigil)
+        (boolean)
+      ] @comment.doc))
+  (#match? @__attribute__ "^(moduledoc|typedoc|doc)$"))
 
 (comment) @comment
 

crates/zed/src/languages/elixir/outline.scm 🔗

@@ -8,9 +8,19 @@
   (arguments
     [
       (identifier) @name
-      (call target: (identifier) @name)
+      (call
+          target: (identifier) @name
+          (arguments
+              "(" @context.extra
+              _* @context.extra
+              ")" @context.extra))
       (binary_operator
-        left: (call target: (identifier) @name)
+        left: (call
+            target: (identifier) @name
+            (arguments
+                "(" @context.extra
+                _* @context.extra
+                ")" @context.extra))
         operator: "when")
     ])
   (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item

crates/zed/src/languages/json.rs 🔗

@@ -6,7 +6,7 @@ use gpui::AppContext;
 use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter};
 use node_runtime::NodeRuntime;
 use serde_json::json;
-use settings::{keymap_file_json_schema, SettingsJsonSchemaParams, SettingsStore};
+use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
 use smol::fs;
 use staff_mode::StaffMode;
 use std::{
@@ -143,7 +143,7 @@ impl LspAdapter for JsonLspAdapter {
                         },
                         {
                             "fileMatch": [schema_file_match(&paths::KEYMAP)],
-                            "schema": keymap_file_json_schema(&action_names),
+                            "schema": KeymapFile::generate_json_schema(&action_names),
                         }
                     ]
                 }

crates/zed/src/languages/markdown/highlights.scm 🔗

@@ -14,11 +14,11 @@
   (list_marker_parenthesis)
 ] @punctuation.list_marker
 
-[
-  (indented_code_block)
-  (fenced_code_block)
-  (code_span)
-] @text.literal
+(code_span) @text.literal
+
+(fenced_code_block
+  (info_string
+    (language) @text.literal))
 
 (link_destination) @link_uri
 (link_text) @link_text

crates/zed/src/languages/typescript.rs 🔗

@@ -327,10 +327,10 @@ mod tests {
                 .map(|item| (item.text.as_str(), item.depth))
                 .collect::<Vec<_>>(),
             &[
-                ("function a ( )", 0),
-                ("async function a2 ( )", 1),
+                ("function a()", 0),
+                ("async function a2()", 1),
                 ("let b", 0),
-                ("function getB ( )", 0),
+                ("function getB()", 0),
                 ("const d", 0),
             ]
         );

crates/zed/src/main.rs 🔗

@@ -2,7 +2,6 @@
 #![allow(non_snake_case)]
 
 use anyhow::{anyhow, Context, Result};
-use assets::Assets;
 use backtrace::Backtrace;
 use cli::{
     ipc::{self, IpcSender},
@@ -58,7 +57,8 @@ use staff_mode::StaffMode;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
 use zed::{
-    self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
+    assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace,
+    languages, menus,
 };
 
 fn main() {
@@ -160,6 +160,8 @@ fn main() {
         ai::init(cx);
 
         cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
+        cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
+            .detach();
 
         languages.set_theme(theme::current(cx).clone());
         cx.observe_global::<SettingsStore, _>({
@@ -660,11 +662,30 @@ async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
     Some(())
 }
 
+#[cfg(debug_assertions)]
+async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
+    let mut events = fs
+        .watch(
+            "crates/zed/src/languages".as_ref(),
+            Duration::from_millis(100),
+        )
+        .await;
+    while (events.next().await).is_some() {
+        languages.reload();
+    }
+    Some(())
+}
+
 #[cfg(not(debug_assertions))]
 async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
     None
 }
 
+#[cfg(not(debug_assertions))]
+async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
+    None
+}
+
 fn connect_to_cli(
     server_name: &str,
 ) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {

crates/zed/src/zed.rs 🔗

@@ -1,7 +1,9 @@
+pub mod assets;
 pub mod languages;
 pub mod menus;
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
+
 use ai::AssistantPanel;
 use anyhow::Context;
 use assets::Assets;
@@ -31,12 +33,11 @@ use project_panel::ProjectPanel;
 use search::{BufferSearchBar, ProjectSearchBar};
 use serde::Deserialize;
 use serde_json::to_string_pretty;
-use settings::{
-    initial_local_settings_content, KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH,
-};
+use settings::{initial_local_settings_content, KeymapFile, SettingsStore};
 use std::{borrow::Cow, str, sync::Arc};
 use terminal_view::terminal_panel::{self, TerminalPanel};
 use util::{
+    asset_str,
     channel::ReleaseChannel,
     paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
     ResultExt,
@@ -150,7 +151,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
                 workspace,
-                "licenses.md",
+                asset_str::<Assets>("licenses.md"),
                 "Open Source License Attribution",
                 "Markdown",
                 cx,
@@ -170,9 +171,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
     cx.add_action(
         move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
             create_and_open_local_file(&paths::SETTINGS, cx, || {
-                settings::initial_user_settings_content(&Assets)
-                    .as_ref()
-                    .into()
+                settings::initial_user_settings_content().as_ref().into()
             })
             .detach_and_log_err(cx);
         },
@@ -182,7 +181,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
         move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
                 workspace,
-                "keymaps/default.json",
+                settings::default_keymap(),
                 "Default Key Bindings",
                 "JSON",
                 cx,
@@ -195,7 +194,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
               cx: &mut ViewContext<Workspace>| {
             open_bundled_file(
                 workspace,
-                DEFAULT_SETTINGS_ASSET_PATH,
+                settings::default_settings(),
                 "Default Settings",
                 "JSON",
                 cx,
@@ -532,11 +531,11 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 
 pub fn load_default_keymap(cx: &mut AppContext) {
     for path in ["keymaps/default.json", "keymaps/vim.json"] {
-        KeymapFileContent::load_asset(path, cx).unwrap();
+        KeymapFile::load_asset(path, cx).unwrap();
     }
 
     if let Some(asset_path) = settings::get::<BaseKeymap>(cx).asset_path() {
-        KeymapFileContent::load_asset(asset_path, cx).unwrap();
+        KeymapFile::load_asset(asset_path, cx).unwrap();
     }
 }
 
@@ -547,7 +546,7 @@ pub fn handle_keymap_file_changes(
     cx.spawn(move |mut cx| async move {
         let mut settings_subscription = None;
         while let Some(user_keymap_content) = user_keymap_file_rx.next().await {
-            if let Ok(keymap_content) = KeymapFileContent::parse(&user_keymap_content) {
+            if let Ok(keymap_content) = KeymapFile::parse(&user_keymap_content) {
                 cx.update(|cx| {
                     cx.clear_bindings();
                     load_default_keymap(cx);
@@ -624,11 +623,7 @@ fn open_local_settings_file(
                     if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
                         if buffer.read(cx).is_empty() {
                             buffer.update(cx, |buffer, cx| {
-                                buffer.edit(
-                                    [(0..0, initial_local_settings_content(&Assets))],
-                                    None,
-                                    cx,
-                                )
+                                buffer.edit([(0..0, initial_local_settings_content())], None, cx)
                             });
                         }
                     }
@@ -704,7 +699,7 @@ fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Works
 
 fn open_bundled_file(
     workspace: &mut Workspace,
-    asset_path: &'static str,
+    text: Cow<'static, str>,
     title: &'static str,
     language: &'static str,
     cx: &mut ViewContext<Workspace>,
@@ -716,13 +711,9 @@ fn open_bundled_file(
             .update(&mut cx, |workspace, cx| {
                 workspace.with_local_workspace(cx, |workspace, cx| {
                     let project = workspace.project();
-                    let buffer = project.update(cx, |project, cx| {
-                        let text = Assets::get(asset_path)
-                            .map(|f| f.data)
-                            .unwrap_or_else(|| Cow::Borrowed(b"File not found"));
-                        let text = str::from_utf8(text.as_ref()).unwrap();
+                    let buffer = project.update(cx, move |project, cx| {
                         project
-                            .create_buffer(text, language, cx)
+                            .create_buffer(text.as_ref(), language, cx)
                             .expect("creating buffers on a local workspace always succeeds")
                     });
                     let buffer = cx.add_model(|cx| {

rust-toolchain.toml 🔗

@@ -0,0 +1,4 @@
+[toolchain]
+channel = "1.70"
+components = [ "rustfmt" ]
+targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ]