Detailed changes
@@ -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",
]
@@ -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"
@@ -0,0 +1,6 @@
+[
+ "else"
+ "fn"
+ "if"
+ "while"
+] @keyword
@@ -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.
@@ -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
@@ -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()
+ }
+ }
+}
@@ -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>,
+}
@@ -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(),
},
);
}
@@ -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,
+ }
+}
@@ -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(¶ms.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
});