diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 8d99b9bad7..4675e4e9dc 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -81,14 +81,14 @@ fn test_select_language() { // matching file extension assert_eq!( registry - .language_for_path("zed/lib.rs") + .language_for_file("zed/lib.rs", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Rust".into()) ); assert_eq!( registry - .language_for_path("zed/lib.mk") + .language_for_file("zed/lib.mk", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) @@ -97,7 +97,7 @@ fn test_select_language() { // matching filename assert_eq!( registry - .language_for_path("zed/Makefile") + .language_for_file("zed/Makefile", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), Some("Make".into()) @@ -106,21 +106,21 @@ fn test_select_language() { // matching suffix that is not the full file extension or filename assert_eq!( registry - .language_for_path("zed/cars") + .language_for_file("zed/cars", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry - .language_for_path("zed/a.cars") + .language_for_file("zed/a.cars", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None ); assert_eq!( registry - .language_for_path("zed/sumk") + .language_for_file("zed/sumk", None) .now_or_never() .and_then(|l| Some(l.ok()?.name())), None diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 60bb2cfddf..81aa1de7bd 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -262,6 +262,8 @@ pub struct LanguageConfig { pub name: Arc, pub path_suffixes: Vec, pub brackets: BracketPairConfig, + #[serde(default, deserialize_with = "deserialize_regex")] + pub first_line_pattern: Option, #[serde(default = "auto_indent_using_last_non_empty_line_default")] pub auto_indent_using_last_non_empty_line: bool, #[serde(default, deserialize_with = "deserialize_regex")] @@ -334,6 +336,7 @@ impl Default for LanguageConfig { path_suffixes: Default::default(), brackets: Default::default(), auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(), + first_line_pattern: Default::default(), increase_indent_pattern: Default::default(), decrease_indent_pattern: Default::default(), autoclose_before: Default::default(), @@ -660,19 +663,30 @@ impl LanguageRegistry { }) } - pub fn language_for_path( + pub fn language_for_file( self: &Arc, path: impl AsRef, + content: Option<&Rope>, ) -> UnwrapFuture>>> { 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.get_or_load_language(|config| { - config + let path_matches = config .path_suffixes .iter() - .any(|suffix| path_suffixes.contains(&Some(suffix.as_str()))) + .any(|suffix| path_suffixes.contains(&Some(suffix.as_str()))); + let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or( + false, + |(content, pattern)| { + let end = content.clip_point(Point::new(0, 256), Bias::Left); + let end = content.point_to_offset(end); + let text = content.chunks_in_range(0..end).collect::(); + pattern.is_match(&text) + }, + ); + path_matches || content_matches }) } @@ -1528,9 +1542,45 @@ pub fn range_from_lsp(range: lsp::Range) -> Range> { #[cfg(test)] mod tests { + use super::*; use gpui::TestAppContext; - use super::*; + #[gpui::test(iterations = 10)] + async fn test_first_line_pattern(cx: &mut TestAppContext) { + let mut languages = LanguageRegistry::test(); + languages.set_executor(cx.background()); + let languages = Arc::new(languages); + languages.register( + "/javascript", + LanguageConfig { + name: "JavaScript".into(), + path_suffixes: vec!["js".into()], + first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), + ..Default::default() + }, + tree_sitter_javascript::language(), + None, + |_| Default::default(), + ); + + languages + .language_for_file("the/script", None) + .await + .unwrap_err(); + languages + .language_for_file("the/script", Some(&"nothing".into())) + .await + .unwrap_err(); + assert_eq!( + languages + .language_for_file("the/script", Some(&"#!/bin/env node".into())) + .await + .unwrap() + .name() + .as_ref(), + "JavaScript" + ); + } #[gpui::test(iterations = 10)] async fn test_language_loading(cx: &mut TestAppContext) { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9192c7a411..e080cca964 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1997,17 +1997,19 @@ impl Project { fn detect_language_for_buffer( &mut self, - buffer: &ModelHandle, + buffer_handle: &ModelHandle, cx: &mut ModelContext, ) -> Option<()> { // If the buffer has a language, set it and start the language server if we haven't already. - let full_path = buffer.read(cx).file()?.full_path(cx); + let buffer = buffer_handle.read(cx); + let full_path = buffer.file()?.full_path(cx); + let content = buffer.as_rope(); let new_language = self .languages - .language_for_path(&full_path) + .language_for_file(&full_path, Some(content)) .now_or_never()? .ok()?; - self.set_language_for_buffer(buffer, new_language, cx); + self.set_language_for_buffer(buffer_handle, new_language, cx); None } @@ -2418,26 +2420,23 @@ impl Project { buffers: impl IntoIterator>, cx: &mut ModelContext, ) -> Option<()> { - let language_server_lookup_info: HashSet<(WorktreeId, Arc, PathBuf)> = buffers + let language_server_lookup_info: HashSet<(WorktreeId, Arc, Arc)> = buffers .into_iter() .filter_map(|buffer| { - let file = File::from_dyn(buffer.read(cx).file())?; + let buffer = buffer.read(cx); + let file = File::from_dyn(buffer.file())?; let worktree = file.worktree.read(cx).as_local()?; - let worktree_id = worktree.id(); - let worktree_abs_path = worktree.abs_path().clone(); let full_path = file.full_path(cx); - Some((worktree_id, worktree_abs_path, full_path)) + let language = self + .languages + .language_for_file(&full_path, Some(buffer.as_rope())) + .now_or_never()? + .ok()?; + Some((worktree.id(), worktree.abs_path().clone(), language)) }) .collect(); - for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info { - if let Some(language) = self - .languages - .language_for_path(&full_path) - .now_or_never() - .and_then(|language| language.ok()) - { - self.restart_language_server(worktree_id, worktree_abs_path, language, cx); - } + for (worktree_id, worktree_abs_path, language) in language_server_lookup_info { + self.restart_language_server(worktree_id, worktree_abs_path, language, cx); } None @@ -3471,7 +3470,7 @@ impl Project { let adapter_language = adapter_language.clone(); let language = this .languages - .language_for_path(&project_path.path) + .language_for_file(&project_path.path, None) .unwrap_or_else(move |_| adapter_language); let language_server_name = adapter.name.clone(); Some(async move { @@ -5900,7 +5899,10 @@ impl Project { worktree_id, path: PathBuf::from(serialized_symbol.path).into(), }; - let language = languages.language_for_path(&path.path).await.log_err(); + let language = languages + .language_for_file(&path.path, None) + .await + .log_err(); Ok(Symbol { language_server_name: LanguageServerName( serialized_symbol.language_server_name.into(), diff --git a/crates/zed/src/languages/javascript/config.toml b/crates/zed/src/languages/javascript/config.toml index 7c49ac9513..c23ddcd6e7 100644 --- a/crates/zed/src/languages/javascript/config.toml +++ b/crates/zed/src/languages/javascript/config.toml @@ -1,5 +1,6 @@ name = "JavaScript" path_suffixes = ["js", "jsx", "mjs"] +first_line_pattern = '^#!.*\bnode\b' line_comment = "// " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/python/config.toml b/crates/zed/src/languages/python/config.toml index e733676d89..80609de0ba 100644 --- a/crates/zed/src/languages/python/config.toml +++ b/crates/zed/src/languages/python/config.toml @@ -1,5 +1,6 @@ name = "Python" path_suffixes = ["py", "pyi"] +first_line_pattern = '^#!.*\bpython[0-9.]*\b' line_comment = "# " autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/zed/src/languages/ruby/config.toml b/crates/zed/src/languages/ruby/config.toml index 329e080740..a0b26bff92 100644 --- a/crates/zed/src/languages/ruby/config.toml +++ b/crates/zed/src/languages/ruby/config.toml @@ -1,5 +1,6 @@ name = "Ruby" path_suffixes = ["rb", "Gemfile"] +first_line_pattern = '^#!.*\bruby\b' line_comment = "# " autoclose_before = ";:.,=}])>" brackets = [