Add a setting for custom associations between languages and files (#9290)
Closes #5178 Release Notes: - Added a `file_types` setting that can be used to associate languages with file names and file extensions. For example, to interpret all `.c` files as C++, and files called `MyLockFile` as TOML, add the following to `settings.json`: ```json { "file_types": { "C++": ["c"], "TOML": ["MyLockFile"] } } ``` As with most zed settings, this can be configured on a per-directory basis by including a local `.zed/settings.json` file in that directory. --------- Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
parent
77de5689a3
commit
724c19a223
30 changed files with 640 additions and 415 deletions
|
@ -3527,6 +3527,55 @@ impl Completion {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct TestFile {
|
||||
pub path: Arc<Path>,
|
||||
pub root_name: String,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl File for TestFile {
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
|
||||
PathBuf::from(&self.root_name).join(self.path.as_ref())
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<&dyn LocalFile> {
|
||||
None
|
||||
}
|
||||
|
||||
fn mtime(&self) -> Option<SystemTime> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
|
||||
self.path().file_name().unwrap_or(self.root_name.as_ref())
|
||||
}
|
||||
|
||||
fn worktree_id(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_private(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn contiguous_ranges(
|
||||
values: impl Iterator<Item = u32>,
|
||||
max_len: usize,
|
||||
|
|
|
@ -69,8 +69,10 @@ fn test_line_endings(cx: &mut gpui::AppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_select_language() {
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
fn test_select_language(cx: &mut AppContext) {
|
||||
init_settings(cx, |_| {});
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
|
@ -97,14 +99,14 @@ fn test_select_language() {
|
|||
// matching file extension
|
||||
assert_eq!(
|
||||
registry
|
||||
.language_for_file("zed/lib.rs".as_ref(), None)
|
||||
.language_for_file(&file("src/lib.rs"), None, cx)
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
Some("Rust".into())
|
||||
);
|
||||
assert_eq!(
|
||||
registry
|
||||
.language_for_file("zed/lib.mk".as_ref(), None)
|
||||
.language_for_file(&file("src/lib.mk"), None, cx)
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
Some("Make".into())
|
||||
|
@ -113,7 +115,7 @@ fn test_select_language() {
|
|||
// matching filename
|
||||
assert_eq!(
|
||||
registry
|
||||
.language_for_file("zed/Makefile".as_ref(), None)
|
||||
.language_for_file(&file("src/Makefile"), None, cx)
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
Some("Make".into())
|
||||
|
@ -122,27 +124,132 @@ fn test_select_language() {
|
|||
// matching suffix that is not the full file extension or filename
|
||||
assert_eq!(
|
||||
registry
|
||||
.language_for_file("zed/cars".as_ref(), None)
|
||||
.language_for_file(&file("zed/cars"), None, cx)
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
registry
|
||||
.language_for_file("zed/a.cars".as_ref(), None)
|
||||
.language_for_file(&file("zed/a.cars"), None, cx)
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
registry
|
||||
.language_for_file("zed/sumk".as_ref(), None)
|
||||
.language_for_file(&file("zed/sumk"), None, cx)
|
||||
.now_or_never()
|
||||
.and_then(|l| Some(l.ok()?.name())),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_first_line_pattern(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| init_settings(cx, |_| {}));
|
||||
|
||||
let languages = LanguageRegistry::test(cx.executor());
|
||||
let languages = Arc::new(languages);
|
||||
|
||||
languages.register_test_language(LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["js".into()],
|
||||
first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
cx.read(|cx| languages.language_for_file(&file("the/script"), None, cx))
|
||||
.await
|
||||
.unwrap_err();
|
||||
cx.read(|cx| languages.language_for_file(&file("the/script"), Some(&"nothing".into()), cx))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
cx.read(|cx| languages.language_for_file(
|
||||
&file("the/script"),
|
||||
Some(&"#!/bin/env node".into()),
|
||||
cx
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
.name()
|
||||
.as_ref(),
|
||||
"JavaScript"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
init_settings(cx, |settings| {
|
||||
settings.file_types.extend([
|
||||
("TypeScript".into(), vec!["js".into()]),
|
||||
("C++".into(), vec!["c".into()]),
|
||||
]);
|
||||
})
|
||||
});
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
|
||||
for config in [
|
||||
LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["js".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
LanguageConfig {
|
||||
name: "TypeScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["js".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
LanguageConfig {
|
||||
name: "C++".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["cpp".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
LanguageConfig {
|
||||
name: "C".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["c".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
] {
|
||||
languages.add(Arc::new(Language::new(config, None)));
|
||||
}
|
||||
|
||||
let language = cx
|
||||
.read(|cx| languages.language_for_file(&file("foo.js"), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(language.name().as_ref(), "TypeScript");
|
||||
let language = cx
|
||||
.read(|cx| languages.language_for_file(&file("foo.c"), None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(language.name().as_ref(), "C++");
|
||||
}
|
||||
|
||||
fn file(path: &str) -> Arc<dyn File> {
|
||||
Arc::new(TestFile {
|
||||
path: Path::new(path).into(),
|
||||
root_name: "zed".into(),
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_edit_events(cx: &mut gpui::AppContext) {
|
||||
let mut now = Instant::now();
|
||||
|
@ -1575,7 +1682,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
|
|||
|
||||
let javascript_language = Arc::new(javascript_lang());
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
language_registry.add(html_language.clone());
|
||||
language_registry.add(javascript_language.clone());
|
||||
|
||||
|
@ -1895,7 +2002,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
|
|||
"#
|
||||
.unindent();
|
||||
|
||||
let language_registry = Arc::new(LanguageRegistry::test());
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
language_registry.add(Arc::new(ruby_lang()));
|
||||
language_registry.add(Arc::new(html_lang()));
|
||||
language_registry.add(Arc::new(erb_lang()));
|
||||
|
|
|
@ -852,11 +852,7 @@ struct BracketConfig {
|
|||
|
||||
impl Language {
|
||||
pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
|
||||
Self::new_with_id(
|
||||
LanguageId(NEXT_LANGUAGE_ID.fetch_add(1, SeqCst)),
|
||||
config,
|
||||
ts_language,
|
||||
)
|
||||
Self::new_with_id(LanguageId::new(), config, ts_language)
|
||||
}
|
||||
|
||||
fn new_with_id(
|
||||
|
@ -1569,44 +1565,9 @@ mod tests {
|
|||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_first_line_pattern(cx: &mut TestAppContext) {
|
||||
let mut languages = LanguageRegistry::test();
|
||||
|
||||
languages.set_executor(cx.executor());
|
||||
let languages = Arc::new(languages);
|
||||
languages.register_test_language(LanguageConfig {
|
||||
name: "JavaScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["js".into()],
|
||||
first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
languages
|
||||
.language_for_file("the/script".as_ref(), None)
|
||||
.await
|
||||
.unwrap_err();
|
||||
languages
|
||||
.language_for_file("the/script".as_ref(), Some(&"nothing".into()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
languages
|
||||
.language_for_file("the/script".as_ref(), Some(&"#!/bin/env node".into()))
|
||||
.await
|
||||
.unwrap()
|
||||
.name()
|
||||
.as_ref(),
|
||||
"JavaScript"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_language_loading(cx: &mut TestAppContext) {
|
||||
let mut languages = LanguageRegistry::test();
|
||||
languages.set_executor(cx.executor());
|
||||
let languages = LanguageRegistry::test(cx.executor());
|
||||
let languages = Arc::new(languages);
|
||||
languages.register_native_grammars([
|
||||
("json", tree_sitter_json::language()),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
CachedLspAdapter, Language, LanguageConfig, LanguageContextProvider, LanguageId,
|
||||
LanguageMatcher, LanguageServerName, LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT,
|
||||
language_settings::all_language_settings, CachedLspAdapter, File, Language, LanguageConfig,
|
||||
LanguageContextProvider, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter,
|
||||
LspAdapterDelegate, PARSER, PLAIN_TEXT,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{hash_map, HashMap};
|
||||
|
@ -10,7 +11,7 @@ use futures::{
|
|||
Future, FutureExt as _,
|
||||
};
|
||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||
use lsp::{LanguageServerBinary, LanguageServerId};
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use postage::watch;
|
||||
use std::{
|
||||
|
@ -30,11 +31,7 @@ pub struct LanguageRegistry {
|
|||
state: RwLock<LanguageRegistryState>,
|
||||
language_server_download_dir: Option<Arc<Path>>,
|
||||
login_shell_env_loaded: Shared<Task<()>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
lsp_binary_paths: Mutex<
|
||||
HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
|
||||
>,
|
||||
executor: Option<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
lsp_binary_status_tx: LspBinaryStatusSender,
|
||||
}
|
||||
|
||||
|
@ -121,12 +118,12 @@ struct LspBinaryStatusSender {
|
|||
}
|
||||
|
||||
impl LanguageRegistry {
|
||||
pub fn new(login_shell_env_loaded: Task<()>) -> Self {
|
||||
Self {
|
||||
pub fn new(login_shell_env_loaded: Task<()>, executor: BackgroundExecutor) -> Self {
|
||||
let this = Self {
|
||||
state: RwLock::new(LanguageRegistryState {
|
||||
next_language_server_id: 0,
|
||||
languages: vec![PLAIN_TEXT.clone()],
|
||||
available_languages: Default::default(),
|
||||
languages: Vec::new(),
|
||||
available_languages: Vec::new(),
|
||||
grammars: Default::default(),
|
||||
loading_languages: Default::default(),
|
||||
lsp_adapters: Default::default(),
|
||||
|
@ -140,21 +137,18 @@ impl LanguageRegistry {
|
|||
}),
|
||||
language_server_download_dir: None,
|
||||
login_shell_env_loaded: login_shell_env_loaded.shared(),
|
||||
lsp_binary_paths: Default::default(),
|
||||
executor: None,
|
||||
lsp_binary_status_tx: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test() -> Self {
|
||||
let mut this = Self::new(Task::ready(()));
|
||||
this.language_server_download_dir = Some(Path::new("/the-download-dir").into());
|
||||
executor,
|
||||
};
|
||||
this.add(PLAIN_TEXT.clone());
|
||||
this
|
||||
}
|
||||
|
||||
pub fn set_executor(&mut self, executor: BackgroundExecutor) {
|
||||
self.executor = Some(executor);
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(executor: BackgroundExecutor) -> Self {
|
||||
let mut this = Self::new(Task::ready(()), executor);
|
||||
this.language_server_download_dir = Some(Path::new("/the-download-dir").into());
|
||||
this
|
||||
}
|
||||
|
||||
/// Clears out all of the loaded languages and reload them from scratch.
|
||||
|
@ -317,8 +311,19 @@ impl LanguageRegistry {
|
|||
result
|
||||
}
|
||||
|
||||
/// Add a pre-loaded language to the registry.
|
||||
pub fn add(&self, language: Arc<Language>) {
|
||||
self.state.write().add(language);
|
||||
let mut state = self.state.write();
|
||||
state.available_languages.push(AvailableLanguage {
|
||||
id: language.id,
|
||||
name: language.name(),
|
||||
grammar: language.config.grammar.clone(),
|
||||
matcher: language.config.matcher.clone(),
|
||||
load: Arc::new(|| Err(anyhow!("already loaded"))),
|
||||
loaded: true,
|
||||
context_provider: language.context_provider.clone(),
|
||||
});
|
||||
state.add(language);
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> watch::Receiver<()> {
|
||||
|
@ -353,7 +358,13 @@ impl LanguageRegistry {
|
|||
name: &str,
|
||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||
let name = UniCase::new(name);
|
||||
let rx = self.get_or_load_language(|language_name, _| UniCase::new(language_name) == name);
|
||||
let rx = self.get_or_load_language(|language_name, _| {
|
||||
if UniCase::new(language_name) == name {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
async move { rx.await? }
|
||||
}
|
||||
|
||||
|
@ -363,28 +374,62 @@ impl LanguageRegistry {
|
|||
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||
let string = UniCase::new(string);
|
||||
let rx = self.get_or_load_language(|name, config| {
|
||||
UniCase::new(name) == string
|
||||
if UniCase::new(name) == string
|
||||
|| config
|
||||
.path_suffixes
|
||||
.iter()
|
||||
.any(|suffix| UniCase::new(suffix) == string)
|
||||
{
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
async move { rx.await? }
|
||||
}
|
||||
|
||||
pub fn language_for_file(
|
||||
self: &Arc<Self>,
|
||||
file: &Arc<dyn File>,
|
||||
content: Option<&Rope>,
|
||||
cx: &AppContext,
|
||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||
let user_file_types = all_language_settings(Some(file), cx);
|
||||
self.language_for_file_internal(
|
||||
&file.full_path(cx),
|
||||
content,
|
||||
Some(&user_file_types.file_types),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn language_for_file_path(
|
||||
self: &Arc<Self>,
|
||||
path: &Path,
|
||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||
self.language_for_file_internal(path, None, None)
|
||||
}
|
||||
|
||||
fn language_for_file_internal(
|
||||
self: &Arc<Self>,
|
||||
path: &Path,
|
||||
content: Option<&Rope>,
|
||||
user_file_types: Option<&HashMap<Arc<str>, Vec<String>>>,
|
||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||
let filename = path.file_name().and_then(|name| name.to_str());
|
||||
let extension = path.extension_or_hidden_file_name();
|
||||
let path_suffixes = [extension, filename];
|
||||
let rx = self.get_or_load_language(move |_, config| {
|
||||
let path_matches = config
|
||||
let empty = Vec::new();
|
||||
|
||||
let rx = self.get_or_load_language(move |language_name, config| {
|
||||
let path_matches_default_suffix = config
|
||||
.path_suffixes
|
||||
.iter()
|
||||
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
|
||||
let path_matches_custom_suffix = user_file_types
|
||||
.and_then(|types| types.get(language_name))
|
||||
.unwrap_or(&empty)
|
||||
.iter()
|
||||
.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)| {
|
||||
|
@ -394,93 +439,110 @@ impl LanguageRegistry {
|
|||
pattern.is_match(&text)
|
||||
},
|
||||
);
|
||||
path_matches || content_matches
|
||||
if path_matches_custom_suffix {
|
||||
2
|
||||
} else if path_matches_default_suffix || content_matches {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
});
|
||||
async move { rx.await? }
|
||||
}
|
||||
|
||||
fn get_or_load_language(
|
||||
self: &Arc<Self>,
|
||||
callback: impl Fn(&str, &LanguageMatcher) -> bool,
|
||||
callback: impl Fn(&str, &LanguageMatcher) -> usize,
|
||||
) -> oneshot::Receiver<Result<Arc<Language>>> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let mut state = self.state.write();
|
||||
if let Some(language) = state
|
||||
.languages
|
||||
let Some((language, _)) = state
|
||||
.available_languages
|
||||
.iter()
|
||||
.find(|language| callback(language.config.name.as_ref(), &language.config.matcher))
|
||||
{
|
||||
let _ = tx.send(Ok(language.clone()));
|
||||
} else if let Some(executor) = self.executor.clone() {
|
||||
if let Some(language) = state
|
||||
.available_languages
|
||||
.iter()
|
||||
.rfind(|l| !l.loaded && callback(&l.name, &l.matcher))
|
||||
.cloned()
|
||||
{
|
||||
match state.loading_languages.entry(language.id) {
|
||||
hash_map::Entry::Occupied(mut entry) => entry.get_mut().push(tx),
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let this = self.clone();
|
||||
executor
|
||||
.spawn(async move {
|
||||
let id = language.id;
|
||||
let name = language.name.clone();
|
||||
let provider = language.context_provider.clone();
|
||||
let language = async {
|
||||
let (config, queries) = (language.load)()?;
|
||||
|
||||
let grammar = if let Some(grammar) = config.grammar.clone() {
|
||||
Some(this.get_or_load_grammar(grammar).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Language::new_with_id(id, config, grammar)
|
||||
.with_context_provider(provider)
|
||||
.with_queries(queries)
|
||||
}
|
||||
.await;
|
||||
|
||||
match language {
|
||||
Ok(language) => {
|
||||
let language = Arc::new(language);
|
||||
let mut state = this.state.write();
|
||||
|
||||
state.add(language.clone());
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to load language {name}:\n{:?}", e);
|
||||
let mut state = this.state.write();
|
||||
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!(
|
||||
"failed to load language {}: {}",
|
||||
name,
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
entry.insert(vec![tx]);
|
||||
}
|
||||
.filter_map(|language| {
|
||||
let score = callback(&language.name, &language.matcher);
|
||||
if score > 0 {
|
||||
Some((language.clone(), score))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let _ = tx.send(Err(anyhow!("language not found")));
|
||||
})
|
||||
.max_by_key(|e| e.1)
|
||||
.clone()
|
||||
else {
|
||||
let _ = tx.send(Err(anyhow!("language not found")));
|
||||
return rx;
|
||||
};
|
||||
|
||||
// If the language is already loaded, resolve with it immediately.
|
||||
for loaded_language in state.languages.iter() {
|
||||
if loaded_language.id == language.id {
|
||||
let _ = tx.send(Ok(loaded_language.clone()));
|
||||
return rx;
|
||||
}
|
||||
}
|
||||
|
||||
match state.loading_languages.entry(language.id) {
|
||||
// If the language is already being loaded, then add this
|
||||
// channel to a list that will be sent to when the load completes.
|
||||
hash_map::Entry::Occupied(mut entry) => entry.get_mut().push(tx),
|
||||
|
||||
// Otherwise, start loading the language.
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let id = language.id;
|
||||
let name = language.name.clone();
|
||||
let provider = language.context_provider.clone();
|
||||
let language = async {
|
||||
let (config, queries) = (language.load)()?;
|
||||
|
||||
let grammar = if let Some(grammar) = config.grammar.clone() {
|
||||
Some(this.get_or_load_grammar(grammar).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Language::new_with_id(id, config, grammar)
|
||||
.with_context_provider(provider)
|
||||
.with_queries(queries)
|
||||
}
|
||||
.await;
|
||||
|
||||
match language {
|
||||
Ok(language) => {
|
||||
let language = Arc::new(language);
|
||||
let mut state = this.state.write();
|
||||
|
||||
state.add(language.clone());
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("failed to load language {name}:\n{:?}", e);
|
||||
let mut state = this.state.write();
|
||||
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!(
|
||||
"failed to load language {}: {}",
|
||||
name,
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
entry.insert(vec![tx]);
|
||||
}
|
||||
} else {
|
||||
let _ = tx.send(Err(anyhow!("executor does not exist")));
|
||||
}
|
||||
|
||||
rx
|
||||
|
@ -502,43 +564,40 @@ impl LanguageRegistry {
|
|||
txs.push(tx);
|
||||
}
|
||||
AvailableGrammar::Unloaded(wasm_path) => {
|
||||
if let Some(executor) = &self.executor {
|
||||
let this = self.clone();
|
||||
executor
|
||||
.spawn({
|
||||
let wasm_path = wasm_path.clone();
|
||||
async move {
|
||||
let wasm_bytes = std::fs::read(&wasm_path)?;
|
||||
let grammar_name = wasm_path
|
||||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.ok_or_else(|| anyhow!("invalid grammar filename"))?;
|
||||
let grammar = PARSER.with(|parser| {
|
||||
let mut parser = parser.borrow_mut();
|
||||
let mut store = parser.take_wasm_store().unwrap();
|
||||
let grammar =
|
||||
store.load_language(&grammar_name, &wasm_bytes);
|
||||
parser.set_wasm_store(store).unwrap();
|
||||
grammar
|
||||
})?;
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn({
|
||||
let wasm_path = wasm_path.clone();
|
||||
async move {
|
||||
let wasm_bytes = std::fs::read(&wasm_path)?;
|
||||
let grammar_name = wasm_path
|
||||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.ok_or_else(|| anyhow!("invalid grammar filename"))?;
|
||||
let grammar = PARSER.with(|parser| {
|
||||
let mut parser = parser.borrow_mut();
|
||||
let mut store = parser.take_wasm_store().unwrap();
|
||||
let grammar = store.load_language(&grammar_name, &wasm_bytes);
|
||||
parser.set_wasm_store(store).unwrap();
|
||||
grammar
|
||||
})?;
|
||||
|
||||
if let Some(AvailableGrammar::Loading(_, txs)) =
|
||||
this.state.write().grammars.insert(
|
||||
name,
|
||||
AvailableGrammar::Loaded(wasm_path, grammar.clone()),
|
||||
)
|
||||
{
|
||||
for tx in txs {
|
||||
tx.send(Ok(grammar.clone())).ok();
|
||||
}
|
||||
if let Some(AvailableGrammar::Loading(_, txs)) =
|
||||
this.state.write().grammars.insert(
|
||||
name,
|
||||
AvailableGrammar::Loaded(wasm_path, grammar.clone()),
|
||||
)
|
||||
{
|
||||
for tx in txs {
|
||||
tx.send(Ok(grammar.clone())).ok();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
*grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
*grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -694,9 +753,6 @@ impl LanguageRegistry {
|
|||
) -> Task<()> {
|
||||
log::info!("deleting server container");
|
||||
|
||||
let mut lock = self.lsp_binary_paths.lock();
|
||||
lock.remove(&adapter.name);
|
||||
|
||||
let download_dir = self
|
||||
.language_server_download_dir
|
||||
.clone()
|
||||
|
@ -716,13 +772,6 @@ impl LanguageRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Default for LanguageRegistry {
|
||||
fn default() -> Self {
|
||||
Self::test()
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageRegistryState {
|
||||
fn next_language_server_id(&mut self) -> LanguageServerId {
|
||||
LanguageServerId(post_inc(&mut self.next_language_server_id))
|
||||
|
|
|
@ -10,9 +10,18 @@ use schemars::{
|
|||
JsonSchema,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use settings::{Settings, SettingsLocation};
|
||||
use std::{num::NonZeroU32, path::Path, sync::Arc};
|
||||
|
||||
impl<'a> Into<SettingsLocation<'a>> for &'a dyn File {
|
||||
fn into(self) -> SettingsLocation<'a> {
|
||||
SettingsLocation {
|
||||
worktree_id: self.worktree_id(),
|
||||
path: self.path().as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes the language settings.
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
AllLanguageSettings::register(cx);
|
||||
|
@ -33,7 +42,7 @@ pub fn all_language_settings<'a>(
|
|||
file: Option<&Arc<dyn File>>,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a AllLanguageSettings {
|
||||
let location = file.map(|f| (f.worktree_id(), f.path().as_ref()));
|
||||
let location = file.map(|f| f.as_ref().into());
|
||||
AllLanguageSettings::get(location, cx)
|
||||
}
|
||||
|
||||
|
@ -44,6 +53,7 @@ pub struct AllLanguageSettings {
|
|||
pub copilot: CopilotSettings,
|
||||
defaults: LanguageSettings,
|
||||
languages: HashMap<Arc<str>, LanguageSettings>,
|
||||
pub(crate) file_types: HashMap<Arc<str>, Vec<String>>,
|
||||
}
|
||||
|
||||
/// The settings for a particular language.
|
||||
|
@ -121,6 +131,10 @@ pub struct AllLanguageSettingsContent {
|
|||
/// The settings for individual languages.
|
||||
#[serde(default, alias = "language_overrides")]
|
||||
pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
|
||||
/// Settings for associating file extensions and filenames
|
||||
/// with languages.
|
||||
#[serde(default)]
|
||||
pub file_types: HashMap<Arc<str>, Vec<String>>,
|
||||
}
|
||||
|
||||
/// The settings for a particular language.
|
||||
|
@ -502,6 +516,16 @@ impl settings::Settings for AllLanguageSettings {
|
|||
}
|
||||
}
|
||||
|
||||
let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
|
||||
for user_file_types in user_settings.iter().map(|s| &s.file_types) {
|
||||
for (language, suffixes) in user_file_types {
|
||||
file_types
|
||||
.entry(language.clone())
|
||||
.or_default()
|
||||
.extend_from_slice(suffixes);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
copilot: CopilotSettings {
|
||||
feature_enabled: copilot_enabled,
|
||||
|
@ -512,6 +536,7 @@ impl settings::Settings for AllLanguageSettings {
|
|||
},
|
||||
defaults,
|
||||
languages,
|
||||
file_types,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use super::*;
|
||||
use crate::{LanguageConfig, LanguageMatcher};
|
||||
use gpui::AppContext;
|
||||
use rand::rngs::StdRng;
|
||||
use std::{env, ops::Range, sync::Arc};
|
||||
use text::{Buffer, BufferId};
|
||||
|
@ -79,8 +80,8 @@ fn test_splice_included_ranges() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_syntax_map_layers_for_range() {
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
fn test_syntax_map_layers_for_range(cx: &mut AppContext) {
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let language = Arc::new(rust_lang());
|
||||
registry.add(language.clone());
|
||||
|
||||
|
@ -176,8 +177,8 @@ fn test_syntax_map_layers_for_range() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_dynamic_language_injection() {
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
fn test_dynamic_language_injection(cx: &mut AppContext) {
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let markdown = Arc::new(markdown_lang());
|
||||
registry.add(markdown.clone());
|
||||
registry.add(Arc::new(rust_lang()));
|
||||
|
@ -254,7 +255,7 @@ fn test_dynamic_language_injection() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_typing_multiple_new_injections() {
|
||||
fn test_typing_multiple_new_injections(cx: &mut AppContext) {
|
||||
let (buffer, syntax_map) = test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -272,6 +273,7 @@ fn test_typing_multiple_new_injections() {
|
|||
"fn a() { test_macro!(b.c(vec![d«.»])) }",
|
||||
"fn a() { test_macro!(b.c(vec![d.«e»])) }",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_capture_ranges(
|
||||
|
@ -283,7 +285,7 @@ fn test_typing_multiple_new_injections() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_pasting_new_injection_line_between_others() {
|
||||
fn test_pasting_new_injection_line_between_others(cx: &mut AppContext) {
|
||||
let (buffer, syntax_map) = test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -309,6 +311,7 @@ fn test_pasting_new_injection_line_between_others() {
|
|||
}
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_capture_ranges(
|
||||
|
@ -330,7 +333,7 @@ fn test_pasting_new_injection_line_between_others() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_joining_injections_with_child_injections() {
|
||||
fn test_joining_injections_with_child_injections(cx: &mut AppContext) {
|
||||
let (buffer, syntax_map) = test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -355,6 +358,7 @@ fn test_joining_injections_with_child_injections() {
|
|||
}
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_capture_ranges(
|
||||
|
@ -374,7 +378,7 @@ fn test_joining_injections_with_child_injections() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_editing_edges_of_injection() {
|
||||
fn test_editing_edges_of_injection(cx: &mut AppContext) {
|
||||
test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -399,11 +403,12 @@ fn test_editing_edges_of_injection() {
|
|||
}
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_edits_preceding_and_intersecting_injection() {
|
||||
fn test_edits_preceding_and_intersecting_injection(cx: &mut AppContext) {
|
||||
test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -411,11 +416,12 @@ fn test_edits_preceding_and_intersecting_injection() {
|
|||
"const aaaaaaaaaaaa: B = c!(d(e.f));",
|
||||
"const aˇa: B = c!(d(eˇ));",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_non_local_changes_create_injections() {
|
||||
fn test_non_local_changes_create_injections(cx: &mut AppContext) {
|
||||
test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -430,11 +436,12 @@ fn test_non_local_changes_create_injections() {
|
|||
ˇ}
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_creating_many_injections_in_one_edit() {
|
||||
fn test_creating_many_injections_in_one_edit(cx: &mut AppContext) {
|
||||
test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -460,11 +467,12 @@ fn test_creating_many_injections_in_one_edit() {
|
|||
}
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_editing_across_injection_boundary() {
|
||||
fn test_editing_across_injection_boundary(cx: &mut AppContext) {
|
||||
test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -488,11 +496,12 @@ fn test_editing_across_injection_boundary() {
|
|||
}
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_removing_injection_by_replacing_across_boundary() {
|
||||
fn test_removing_injection_by_replacing_across_boundary(cx: &mut AppContext) {
|
||||
test_edit_sequence(
|
||||
"Rust",
|
||||
&[
|
||||
|
@ -514,11 +523,12 @@ fn test_removing_injection_by_replacing_across_boundary() {
|
|||
}
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_simple() {
|
||||
fn test_combined_injections_simple(cx: &mut AppContext) {
|
||||
let (buffer, syntax_map) = test_edit_sequence(
|
||||
"ERB",
|
||||
&[
|
||||
|
@ -549,6 +559,7 @@ fn test_combined_injections_simple() {
|
|||
</body>
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_capture_ranges(
|
||||
|
@ -565,7 +576,7 @@ fn test_combined_injections_simple() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_empty_ranges() {
|
||||
fn test_combined_injections_empty_ranges(cx: &mut AppContext) {
|
||||
test_edit_sequence(
|
||||
"ERB",
|
||||
&[
|
||||
|
@ -579,11 +590,12 @@ fn test_combined_injections_empty_ranges() {
|
|||
ˇ<% end %>
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_edit_edges_of_ranges() {
|
||||
fn test_combined_injections_edit_edges_of_ranges(cx: &mut AppContext) {
|
||||
let (buffer, syntax_map) = test_edit_sequence(
|
||||
"ERB",
|
||||
&[
|
||||
|
@ -600,6 +612,7 @@ fn test_combined_injections_edit_edges_of_ranges() {
|
|||
<%= three @four %>
|
||||
",
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_capture_ranges(
|
||||
|
@ -614,7 +627,7 @@ fn test_combined_injections_edit_edges_of_ranges() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_splitting_some_injections() {
|
||||
fn test_combined_injections_splitting_some_injections(cx: &mut AppContext) {
|
||||
let (_buffer, _syntax_map) = test_edit_sequence(
|
||||
"ERB",
|
||||
&[
|
||||
|
@ -635,11 +648,12 @@ fn test_combined_injections_splitting_some_injections() {
|
|||
<% f %>
|
||||
"#,
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_editing_after_last_injection() {
|
||||
fn test_combined_injections_editing_after_last_injection(cx: &mut AppContext) {
|
||||
test_edit_sequence(
|
||||
"ERB",
|
||||
&[
|
||||
|
@ -655,11 +669,12 @@ fn test_combined_injections_editing_after_last_injection() {
|
|||
more text»
|
||||
"#,
|
||||
],
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_combined_injections_inside_injections() {
|
||||
fn test_combined_injections_inside_injections(cx: &mut AppContext) {
|
||||
let (buffer, syntax_map) = test_edit_sequence(
|
||||
"Markdown",
|
||||
&[
|
||||
|
@ -709,6 +724,7 @@ fn test_combined_injections_inside_injections() {
|
|||
```
|
||||
"#,
|
||||
],
|
||||
cx,
|
||||
);
|
||||
|
||||
// Check that the code directive below the ruby comment is
|
||||
|
@ -735,7 +751,7 @@ fn test_combined_injections_inside_injections() {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_empty_combined_injections_inside_injections() {
|
||||
fn test_empty_combined_injections_inside_injections(cx: &mut AppContext) {
|
||||
let (buffer, syntax_map) = test_edit_sequence(
|
||||
"Markdown",
|
||||
&[r#"
|
||||
|
@ -745,6 +761,7 @@ fn test_empty_combined_injections_inside_injections() {
|
|||
|
||||
goodbye
|
||||
"#],
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_layers_for_range(
|
||||
|
@ -763,7 +780,7 @@ fn test_empty_combined_injections_inside_injections() {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 50)]
|
||||
fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
|
||||
fn test_random_syntax_map_edits_rust_macros(rng: StdRng, cx: &mut AppContext) {
|
||||
let text = r#"
|
||||
fn test_something() {
|
||||
let vec = vec![5, 1, 3, 8];
|
||||
|
@ -781,7 +798,7 @@ fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
|
|||
.unindent()
|
||||
.repeat(2);
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let language = Arc::new(rust_lang());
|
||||
registry.add(language.clone());
|
||||
|
||||
|
@ -789,7 +806,7 @@ fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 50)]
|
||||
fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
|
||||
fn test_random_syntax_map_edits_with_erb(rng: StdRng, cx: &mut AppContext) {
|
||||
let text = r#"
|
||||
<div id="main">
|
||||
<% if one?(:two) %>
|
||||
|
@ -808,7 +825,7 @@ fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
|
|||
.unindent()
|
||||
.repeat(5);
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let language = Arc::new(erb_lang());
|
||||
registry.add(language.clone());
|
||||
registry.add(Arc::new(ruby_lang()));
|
||||
|
@ -818,7 +835,7 @@ fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
|
|||
}
|
||||
|
||||
#[gpui::test(iterations = 50)]
|
||||
fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
|
||||
fn test_random_syntax_map_edits_with_heex(rng: StdRng, cx: &mut AppContext) {
|
||||
let text = r#"
|
||||
defmodule TheModule do
|
||||
def the_method(assigns) do
|
||||
|
@ -841,7 +858,7 @@ fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
|
|||
.unindent()
|
||||
.repeat(3);
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let language = Arc::new(elixir_lang());
|
||||
registry.add(language.clone());
|
||||
registry.add(Arc::new(heex_lang()));
|
||||
|
@ -1025,8 +1042,12 @@ fn check_interpolation(
|
|||
}
|
||||
}
|
||||
|
||||
fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) {
|
||||
let registry = Arc::new(LanguageRegistry::test());
|
||||
fn test_edit_sequence(
|
||||
language_name: &str,
|
||||
steps: &[&str],
|
||||
cx: &mut AppContext,
|
||||
) -> (Buffer, SyntaxMap) {
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
registry.add(Arc::new(elixir_lang()));
|
||||
registry.add(Arc::new(heex_lang()));
|
||||
registry.add(Arc::new(rust_lang()));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue