Introduce LanguageRegistry object

Max Brunsfeld , Antonio Scandurra , and Nathan Sobo created

* Include it, along with settings in `OpenParams` grouped under a new struct called `AppState`

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                        |  24 +++++++
zed/Cargo.toml                    |   2 
zed/languages/rust/highlights.scm |   6 +
zed/src/editor/buffer/mod.rs      |  24 +++++-
zed/src/file_finder.rs            |  50 +++++++++++---
zed/src/language.rs               | 105 +++++++++++++++++++++++++++++++++
zed/src/lib.rs                    |   7 ++
zed/src/main.rs                   |  18 ++++-
zed/src/test.rs                   |  13 +++
zed/src/workspace.rs              |  68 ++++++++++++++------
10 files changed, 271 insertions(+), 46 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1202,7 +1202,7 @@ dependencies = [
  "smallvec",
  "smol",
  "tiny-skia",
- "tree-sitter",
+ "tree-sitter 0.17.1",
  "usvg",
 ]
 
@@ -2712,6 +2712,26 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "tree-sitter"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f41201fed3db3b520405a9c01c61773a250d4c3f43e9861c14b2bb232c981ab"
+dependencies = [
+ "cc",
+ "regex",
+]
+
+[[package]]
+name = "tree-sitter-rust"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784f7ef9cdbd4c895dc2d4bb785e95b4a5364a602eec803681db83d1927ddf15"
+dependencies = [
+ "cc",
+ "tree-sitter 0.19.3",
+]
+
 [[package]]
 name = "ttf-parser"
 version = "0.9.0"
@@ -2987,5 +3007,7 @@ dependencies = [
  "smallvec",
  "smol",
  "tempdir",
+ "tree-sitter 0.19.3",
+ "tree-sitter-rust",
  "unindent",
 ]

zed/Cargo.toml 🔗

@@ -38,6 +38,8 @@ similar = "1.3"
 simplelog = "0.9"
 smallvec = {version = "1.6", features = ["union"]}
 smol = "1.2.5"
+tree-sitter = "0.19.3"
+tree-sitter-rust = "0.19.0"
 
 [dev-dependencies]
 cargo-bundle = "0.5.0"

zed/src/editor/buffer/mod.rs 🔗

@@ -12,6 +12,7 @@ use similar::{ChangeTag, TextDiff};
 
 use crate::{
     editor::Bias,
+    language::Language,
     operation_queue::{self, OperationQueue},
     sum_tree::{self, FilterCursor, SeekBias, SumTree},
     time::{self, ReplicaId},
@@ -68,6 +69,7 @@ pub struct Buffer {
     undo_map: UndoMap,
     history: History,
     file: Option<FileHandle>,
+    language: Option<Arc<Language>>,
     selections: HashMap<SelectionSetId, Arc<[Selection]>>,
     pub selections_last_update: SelectionsVersion,
     deferred_ops: OperationQueue<Operation>,
@@ -357,22 +359,24 @@ impl Buffer {
         base_text: T,
         ctx: &mut ModelContext<Self>,
     ) -> Self {
-        Self::build(replica_id, History::new(base_text.into()), None, ctx)
+        Self::build(replica_id, History::new(base_text.into()), None, None, ctx)
     }
 
     pub fn from_history(
         replica_id: ReplicaId,
         history: History,
         file: Option<FileHandle>,
+        language: Option<Arc<Language>>,
         ctx: &mut ModelContext<Self>,
     ) -> Self {
-        Self::build(replica_id, history, file, ctx)
+        Self::build(replica_id, history, file, language, ctx)
     }
 
     fn build(
         replica_id: ReplicaId,
         history: History,
         file: Option<FileHandle>,
+        language: Option<Arc<Language>>,
         ctx: &mut ModelContext<Self>,
     ) -> Self {
         let saved_mtime;
@@ -472,6 +476,7 @@ impl Buffer {
             undo_map: Default::default(),
             history,
             file,
+            language,
             saved_mtime,
             selections: HashMap::default(),
             selections_last_update: 0,
@@ -1884,6 +1889,7 @@ impl Clone for Buffer {
             selections_last_update: self.selections_last_update.clone(),
             deferred_ops: self.deferred_ops.clone(),
             file: self.file.clone(),
+            language: self.language.clone(),
             deferred_replicas: self.deferred_replicas.clone(),
             replica_id: self.replica_id,
             local_clock: self.local_clock.clone(),
@@ -2812,7 +2818,7 @@ mod tests {
 
             let file1 = app.update(|ctx| tree.file("file1", ctx)).await;
             let buffer1 = app.add_model(|ctx| {
-                Buffer::from_history(0, History::new("abc".into()), Some(file1), ctx)
+                Buffer::from_history(0, History::new("abc".into()), Some(file1), None, ctx)
             });
             let events = Rc::new(RefCell::new(Vec::new()));
 
@@ -2877,7 +2883,7 @@ mod tests {
                     move |_, event, _| events.borrow_mut().push(event.clone())
                 });
 
-                Buffer::from_history(0, History::new("abc".into()), Some(file2), ctx)
+                Buffer::from_history(0, History::new("abc".into()), Some(file2), None, ctx)
             });
 
             fs::remove_file(dir.path().join("file2")).unwrap();
@@ -2896,7 +2902,7 @@ mod tests {
                     move |_, event, _| events.borrow_mut().push(event.clone())
                 });
 
-                Buffer::from_history(0, History::new("abc".into()), Some(file3), ctx)
+                Buffer::from_history(0, History::new("abc".into()), Some(file3), None, ctx)
             });
 
             tree.flush_fs_events(&app).await;
@@ -2923,7 +2929,13 @@ mod tests {
         let abs_path = dir.path().join("the-file");
         let file = app.update(|ctx| tree.file("the-file", ctx)).await;
         let buffer = app.add_model(|ctx| {
-            Buffer::from_history(0, History::new(initial_contents.into()), Some(file), ctx)
+            Buffer::from_history(
+                0,
+                History::new(initial_contents.into()),
+                Some(file),
+                None,
+                ctx,
+            )
         });
 
         // Add a cursor at the start of each row.

zed/src/file_finder.rs 🔗

@@ -458,7 +458,11 @@ impl FileFinder {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor, settings, test::temp_tree, workspace::Workspace};
+    use crate::{
+        editor,
+        test::{build_app_state, temp_tree},
+        workspace::Workspace,
+    };
     use serde_json::json;
     use std::fs;
     use tempdir::TempDir;
@@ -474,9 +478,10 @@ mod tests {
             editor::init(ctx);
         });
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (window_id, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(tmp_dir.path(), ctx);
             workspace
         });
@@ -541,15 +546,21 @@ mod tests {
             "hi": "",
             "hiccup": "",
         }));
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings.clone(), ctx);
+            let mut workspace = Workspace::new(
+                0,
+                app_state.settings.clone(),
+                app_state.language_registry.clone(),
+                ctx,
+            );
             workspace.add_worktree(tmp_dir.path(), ctx);
             workspace
         });
         app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
             .await;
-        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+        let (_, finder) =
+            app.add_window(|ctx| FileFinder::new(app_state.settings, workspace.clone(), ctx));
 
         let query = "hi".to_string();
         finder
@@ -598,15 +609,21 @@ mod tests {
         fs::create_dir(&dir_path).unwrap();
         fs::write(&file_path, "").unwrap();
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings.clone(), ctx);
+            let mut workspace = Workspace::new(
+                0,
+                app_state.settings.clone(),
+                app_state.language_registry.clone(),
+                ctx,
+            );
             workspace.add_worktree(&file_path, ctx);
             workspace
         });
         app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
             .await;
-        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+        let (_, finder) =
+            app.add_window(|ctx| FileFinder::new(app_state.settings, workspace.clone(), ctx));
 
         // Even though there is only one worktree, that worktree's filename
         // is included in the matching, because the worktree is a single file.
@@ -641,9 +658,17 @@ mod tests {
             "dir1": { "a.txt": "" },
             "dir2": { "a.txt": "" }
         }));
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
 
-        let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx));
+        let app_state = app.read(build_app_state);
+
+        let (_, workspace) = app.add_window(|ctx| {
+            Workspace::new(
+                0,
+                app_state.settings.clone(),
+                app_state.language_registry.clone(),
+                ctx,
+            )
+        });
 
         workspace
             .update(&mut app, |workspace, ctx| {
@@ -656,7 +681,8 @@ mod tests {
         app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
             .await;
 
-        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+        let (_, finder) =
+            app.add_window(|ctx| FileFinder::new(app_state.settings, workspace.clone(), ctx));
 
         // Run a search that matches two files with the same relative path.
         finder

zed/src/language.rs 🔗

@@ -0,0 +1,105 @@
+use rust_embed::RustEmbed;
+use std::{path::Path, sync::Arc};
+use tree_sitter::{Language as Grammar, Query};
+
+pub use tree_sitter::{Parser, Tree};
+
+#[derive(RustEmbed)]
+#[folder = "languages"]
+pub struct LanguageDir;
+
+pub struct Language {
+    name: String,
+    grammar: Grammar,
+    highlight_query: Query,
+    path_suffixes: Vec<String>,
+}
+
+pub struct LanguageRegistry {
+    languages: Vec<Arc<Language>>,
+}
+
+impl LanguageRegistry {
+    pub fn new() -> Self {
+        let grammar = tree_sitter_rust::language();
+        let rust_language = Language {
+            name: "Rust".to_string(),
+            grammar,
+            highlight_query: Query::new(
+                grammar,
+                std::str::from_utf8(LanguageDir::get("rust/highlights.scm").unwrap().as_ref())
+                    .unwrap(),
+            )
+            .unwrap(),
+            path_suffixes: vec!["rs".to_string()],
+        };
+
+        Self {
+            languages: vec![Arc::new(rust_language)],
+        }
+    }
+
+    pub fn select_language(&self, path: impl AsRef<Path>) -> Option<&Arc<Language>> {
+        let path = path.as_ref();
+        let filename = path.file_name().and_then(|name| name.to_str());
+        let extension = path.extension().and_then(|name| name.to_str());
+        let path_suffixes = [extension, filename];
+        self.languages.iter().find(|language| {
+            language
+                .path_suffixes
+                .iter()
+                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_select_language() {
+        let grammar = tree_sitter_rust::language();
+        let registry = LanguageRegistry {
+            languages: vec![
+                Arc::new(Language {
+                    name: "Rust".to_string(),
+                    grammar,
+                    highlight_query: Query::new(grammar, "").unwrap(),
+                    path_suffixes: vec!["rs".to_string()],
+                }),
+                Arc::new(Language {
+                    name: "Make".to_string(),
+                    grammar,
+                    highlight_query: Query::new(grammar, "").unwrap(),
+                    path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
+                }),
+            ],
+        };
+
+        // matching file extension
+        assert_eq!(
+            registry.select_language("zed/lib.rs").map(get_name),
+            Some("Rust")
+        );
+        assert_eq!(
+            registry.select_language("zed/lib.mk").map(get_name),
+            Some("Make")
+        );
+
+        // matching filename
+        assert_eq!(
+            registry.select_language("zed/Makefile").map(get_name),
+            Some("Make")
+        );
+
+        // matching suffix that is not the full file extension or filename
+        assert_eq!(registry.select_language("zed/cars").map(get_name), None);
+        assert_eq!(registry.select_language("zed/a.cars").map(get_name), None);
+        assert_eq!(registry.select_language("zed/sumk").map(get_name), None);
+
+        fn get_name(language: &Arc<Language>) -> &str {
+            language.name.as_str()
+        }
+    }
+}

zed/src/lib.rs 🔗

@@ -1,6 +1,7 @@
 pub mod assets;
 pub mod editor;
 pub mod file_finder;
+pub mod language;
 pub mod menus;
 mod operation_queue;
 pub mod settings;
@@ -11,3 +12,9 @@ mod time;
 mod util;
 pub mod workspace;
 mod worktree;
+
+#[derive(Clone)]
+pub struct AppState {
+    pub settings: postage::watch::Receiver<settings::Settings>,
+    pub language_registry: std::sync::Arc<language::LanguageRegistry>,
+}

zed/src/main.rs 🔗

@@ -4,19 +4,27 @@
 use fs::OpenOptions;
 use log::LevelFilter;
 use simplelog::SimpleLogger;
-use std::{fs, path::PathBuf};
+use std::{fs, path::PathBuf, sync::Arc};
 use zed::{
-    assets, editor, file_finder, menus, settings,
+    assets, editor, file_finder, language, menus, settings,
     workspace::{self, OpenParams},
+    AppState,
 };
 
 fn main() {
     init_logger();
 
     let app = gpui::App::new(assets::Assets).unwrap();
-    let (_, settings_rx) = settings::channel(&app.font_cache()).unwrap();
+
+    let (_, settings) = settings::channel(&app.font_cache()).unwrap();
+    let language_registry = Arc::new(language::LanguageRegistry::new());
+    let app_state = AppState {
+        language_registry,
+        settings,
+    };
+
     app.run(move |ctx| {
-        ctx.set_menus(menus::menus(settings_rx.clone()));
+        ctx.set_menus(menus::menus(app_state.settings.clone()));
         workspace::init(ctx);
         editor::init(ctx);
         file_finder::init(ctx);
@@ -31,7 +39,7 @@ fn main() {
                 "workspace:open_paths",
                 OpenParams {
                     paths,
-                    settings: settings_rx,
+                    app_state: app_state.clone(),
                 },
             );
         }

zed/src/test.rs 🔗

@@ -1,9 +1,11 @@
-use crate::time::ReplicaId;
+use crate::{language::LanguageRegistry, settings, time::ReplicaId, AppState};
 use ctor::ctor;
+use gpui::AppContext;
 use rand::Rng;
 use std::{
     collections::BTreeMap,
     path::{Path, PathBuf},
+    sync::Arc,
 };
 use tempdir::TempDir;
 
@@ -141,3 +143,12 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
         panic!("You must pass a JSON object to this helper")
     }
 }
+
+pub fn build_app_state(ctx: &AppContext) -> AppState {
+    let settings = settings::channel(&ctx.font_cache()).unwrap().1;
+    let language_registry = Arc::new(LanguageRegistry::new());
+    AppState {
+        settings,
+        language_registry,
+    }
+}

zed/src/workspace.rs 🔗

@@ -2,9 +2,11 @@ pub mod pane;
 pub mod pane_group;
 use crate::{
     editor::{Buffer, BufferView},
+    language::LanguageRegistry,
     settings::Settings,
     time::ReplicaId,
     worktree::{FileHandle, Worktree, WorktreeHandle},
+    AppState,
 };
 use futures_core::Future;
 use gpui::{
@@ -40,11 +42,11 @@ pub fn init(app: &mut MutableAppContext) {
 
 pub struct OpenParams {
     pub paths: Vec<PathBuf>,
-    pub settings: watch::Receiver<Settings>,
+    pub app_state: AppState,
 }
 
-fn open(settings: &watch::Receiver<Settings>, ctx: &mut MutableAppContext) {
-    let settings = settings.clone();
+fn open(app_state: &AppState, ctx: &mut MutableAppContext) {
+    let app_state = app_state.clone();
     ctx.prompt_for_paths(
         PathPromptOptions {
             files: true,
@@ -53,7 +55,7 @@ fn open(settings: &watch::Receiver<Settings>, ctx: &mut MutableAppContext) {
         },
         move |paths, ctx| {
             if let Some(paths) = paths {
-                ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, settings });
+                ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, app_state });
             }
         },
     );
@@ -84,7 +86,12 @@ fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
 
     // Add a new workspace if necessary
     app.add_window(|ctx| {
-        let mut view = Workspace::new(0, params.settings.clone(), ctx);
+        let mut view = Workspace::new(
+            0,
+            params.app_state.settings.clone(),
+            params.app_state.language_registry.clone(),
+            ctx,
+        );
         let open_paths = view.open_paths(&params.paths, ctx);
         ctx.foreground().spawn(open_paths).detach();
         view
@@ -284,6 +291,7 @@ pub struct State {
 
 pub struct Workspace {
     pub settings: watch::Receiver<Settings>,
+    language_registry: Arc<LanguageRegistry>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     panes: Vec<ViewHandle<Pane>>,
@@ -301,6 +309,7 @@ impl Workspace {
     pub fn new(
         replica_id: ReplicaId,
         settings: watch::Receiver<Settings>,
+        language_registry: Arc<LanguageRegistry>,
         ctx: &mut ViewContext<Self>,
     ) -> Self {
         let pane = ctx.add_view(|_| Pane::new(settings.clone()));
@@ -316,6 +325,7 @@ impl Workspace {
             panes: vec![pane.clone()],
             active_pane: pane.clone(),
             settings,
+            language_registry,
             replica_id,
             worktrees: Default::default(),
             items: Default::default(),
@@ -503,6 +513,7 @@ impl Workspace {
             let (mut tx, rx) = postage::watch::channel();
             entry.insert(rx);
             let replica_id = self.replica_id;
+            let language_registry = self.language_registry.clone();
 
             ctx.as_mut()
                 .spawn(|mut ctx| async move {
@@ -512,7 +523,14 @@ impl Workspace {
 
                     *tx.borrow_mut() = Some(match history {
                         Ok(history) => Ok(Box::new(ctx.add_model(|ctx| {
-                            Buffer::from_history(replica_id, history, Some(file), ctx)
+                            let language = language_registry.select_language(path);
+                            Buffer::from_history(
+                                replica_id,
+                                history,
+                                Some(file),
+                                language.cloned(),
+                                ctx,
+                            )
                         }))),
                         Err(error) => Err(Arc::new(error)),
                     })
@@ -757,14 +775,17 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor::BufferView, settings, test::temp_tree};
+    use crate::{
+        editor::BufferView,
+        test::{build_app_state, temp_tree},
+    };
     use serde_json::json;
     use std::{collections::HashSet, fs};
     use tempdir::TempDir;
 
     #[gpui::test]
     fn test_open_paths_action(app: &mut gpui::MutableAppContext) {
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = build_app_state(app.as_ref());
 
         init(app);
 
@@ -790,7 +811,7 @@ mod tests {
                     dir.path().join("a").to_path_buf(),
                     dir.path().join("b").to_path_buf(),
                 ],
-                settings: settings.clone(),
+                app_state: app_state.clone(),
             },
         );
         assert_eq!(app.window_ids().count(), 1);
@@ -799,7 +820,7 @@ mod tests {
             "workspace:open_paths",
             OpenParams {
                 paths: vec![dir.path().join("a").to_path_buf()],
-                settings: settings.clone(),
+                app_state: app_state.clone(),
             },
         );
         assert_eq!(app.window_ids().count(), 1);
@@ -815,7 +836,7 @@ mod tests {
                     dir.path().join("b").to_path_buf(),
                     dir.path().join("c").to_path_buf(),
                 ],
-                settings: settings.clone(),
+                app_state: app_state.clone(),
             },
         );
         assert_eq!(app.window_ids().count(), 2);
@@ -831,10 +852,11 @@ mod tests {
             },
         }));
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
 
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir.path(), ctx);
             workspace
         });
@@ -935,9 +957,10 @@ mod tests {
             "b.txt": "",
         }));
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir1.path(), ctx);
             workspace
         });
@@ -1003,9 +1026,10 @@ mod tests {
             "a.txt": "",
         }));
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (window_id, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir.path(), ctx);
             workspace
         });
@@ -1046,9 +1070,10 @@ mod tests {
     #[gpui::test]
     async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) {
         let dir = TempDir::new("test-new-file").unwrap();
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir.path(), ctx);
             workspace
         });
@@ -1150,9 +1175,10 @@ mod tests {
             },
         }));
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (window_id, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir.path(), ctx);
             workspace
         });