diff --git a/Cargo.lock b/Cargo.lock index 21a08332c5e08e81c35c0d9d4db343a38983d0d6..4562f2708c0d46750cc3259e172f326c214a88e1 100644 --- a/Cargo.lock +++ b/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", ] diff --git a/zed/Cargo.toml b/zed/Cargo.toml index a1749f2474930dacb4ba2db0d52b8e7dc9ea4d13..91477e2831ad0b57aca08d372ba2817c658fcf12 100644 --- a/zed/Cargo.toml +++ b/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" diff --git a/zed/languages/rust/highlights.scm b/zed/languages/rust/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..32b3dcac607798a8e1fb47ba6a3bb7584a557671 --- /dev/null +++ b/zed/languages/rust/highlights.scm @@ -0,0 +1,6 @@ +[ + "else" + "fn" + "if" + "while" +] @keyword \ No newline at end of file diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 1b458388bc1cf26495fd97a22ce82bed935528aa..9fdf4e0c3620ee20993d0db1fb5f2ac562992043 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/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, + language: Option>, selections: HashMap>, pub selections_last_update: SelectionsVersion, deferred_ops: OperationQueue, @@ -357,22 +359,24 @@ impl Buffer { base_text: T, ctx: &mut ModelContext, ) -> 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, + language: Option>, ctx: &mut ModelContext, ) -> 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, + language: Option>, ctx: &mut ModelContext, ) -> 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. diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 7e57057e7215dee4a6ff67fc244aa2e45b99c34e..5c3cffff7b792f5dec627e918234d8a5339ac677 100644 --- a/zed/src/file_finder.rs +++ b/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 diff --git a/zed/src/language.rs b/zed/src/language.rs new file mode 100644 index 0000000000000000000000000000000000000000..666e55c892c2844aa4d9100181b189df117bee77 --- /dev/null +++ b/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, +} + +pub struct LanguageRegistry { + languages: Vec>, +} + +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) -> Option<&Arc> { + 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) -> &str { + language.name.as_str() + } + } +} diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 7dd383f56e612e8f0db08d6affc8d649d6acd702..936185bf884277f091792737125693d1d4fbe0a1 100644 --- a/zed/src/lib.rs +++ b/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, + pub language_registry: std::sync::Arc, +} diff --git a/zed/src/main.rs b/zed/src/main.rs index 773acf147e03d55052ca85c8c819989dc40cd27f..7d264e9c329405eb4881db2ad57a72871357340f 100644 --- a/zed/src/main.rs +++ b/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(), }, ); } diff --git a/zed/src/test.rs b/zed/src/test.rs index 1d155d4a5ab7a4687a8e3e8d15972edcdd5d3880..5efb0f3e3573f3165596b299b02c6dd844e10a4d 100644 --- a/zed/src/test.rs +++ b/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, + } +} diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 328ebf30e8413f637f7f486f52dca51e0ca5191a..bb77ca11817ad2d961586806eff11b14b85a8939 100644 --- a/zed/src/workspace.rs +++ b/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, - pub settings: watch::Receiver, + pub app_state: AppState, } -fn open(settings: &watch::Receiver, 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, 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(¶ms.paths, ctx); ctx.foreground().spawn(open_paths).detach(); view @@ -284,6 +291,7 @@ pub struct State { pub struct Workspace { pub settings: watch::Receiver, + language_registry: Arc, modal: Option, center: PaneGroup, panes: Vec>, @@ -301,6 +309,7 @@ impl Workspace { pub fn new( replica_id: ReplicaId, settings: watch::Receiver, + language_registry: Arc, ctx: &mut ViewContext, ) -> 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 { #[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 });