mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 02:17:35 +03:00
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
@ -532,6 +532,19 @@
|
|||||||
"enable": false
|
"enable": false
|
||||||
},
|
},
|
||||||
"code_actions_on_format": {},
|
"code_actions_on_format": {},
|
||||||
|
// An object whose keys are language names, and whose values
|
||||||
|
// are arrays of filenames or extensions of files that should
|
||||||
|
// use those languages.
|
||||||
|
//
|
||||||
|
// For example, to treat files like `foo.notjs` as JavaScript,
|
||||||
|
// and 'Embargo.lock' as TOML:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "JavaScript": ["notjs"],
|
||||||
|
// "TOML": ["Embargo.lock"]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
"file_types": {},
|
||||||
// Different settings for specific languages.
|
// Different settings for specific languages.
|
||||||
"languages": {
|
"languages": {
|
||||||
"Plain Text": {
|
"Plain Text": {
|
||||||
|
@ -3268,7 +3268,7 @@ mod tests {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
init(cx);
|
init(cx);
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
|
|
||||||
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
||||||
let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
|
let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
|
||||||
@ -3399,7 +3399,7 @@ mod tests {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
init(cx);
|
init(cx);
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
||||||
|
|
||||||
let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
|
let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
|
||||||
@ -3498,7 +3498,7 @@ mod tests {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
init(cx);
|
init(cx);
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
||||||
let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
|
let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
|
||||||
let buffer = conversation.read(cx).buffer.clone();
|
let buffer = conversation.read(cx).buffer.clone();
|
||||||
@ -3582,7 +3582,7 @@ mod tests {
|
|||||||
let settings_store = cx.update(SettingsStore::test);
|
let settings_store = cx.update(SettingsStore::test);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
cx.update(init);
|
cx.update(init);
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
let completion_provider = Arc::new(FakeCompletionProvider::new());
|
||||||
let conversation =
|
let conversation =
|
||||||
cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
|
cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
|
||||||
|
@ -257,13 +257,12 @@ impl TestServer {
|
|||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||||
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||||
let mut language_registry = LanguageRegistry::test();
|
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
language_registry.set_executor(cx.executor());
|
|
||||||
let app_state = Arc::new(workspace::AppState {
|
let app_state = Arc::new(workspace::AppState {
|
||||||
client: client.clone(),
|
client: client.clone(),
|
||||||
user_store: user_store.clone(),
|
user_store: user_store.clone(),
|
||||||
workspace_store,
|
workspace_store,
|
||||||
languages: Arc::new(language_registry),
|
languages: language_registry,
|
||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
build_window_options: |_, _| Default::default(),
|
build_window_options: |_, _| Default::default(),
|
||||||
node_runtime: FakeNodeRuntime::new(),
|
node_runtime: FakeNodeRuntime::new(),
|
||||||
|
@ -1042,8 +1042,8 @@ mod tests {
|
|||||||
use util::test::marked_text_ranges;
|
use util::test::marked_text_ranges;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_render_markdown_with_mentions() {
|
fn test_render_markdown_with_mentions(cx: &mut AppContext) {
|
||||||
let language_registry = Arc::new(LanguageRegistry::test());
|
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
|
let (body, ranges) = marked_text_ranges("*hi*, «@abc», let's **call** «@fgh»", false);
|
||||||
let message = channel::ChannelMessage {
|
let message = channel::ChannelMessage {
|
||||||
id: ChannelMessageId::Saved(0),
|
id: ChannelMessageId::Saved(0),
|
||||||
@ -1090,8 +1090,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_render_markdown_with_auto_detect_links() {
|
fn test_render_markdown_with_auto_detect_links(cx: &mut AppContext) {
|
||||||
let language_registry = Arc::new(LanguageRegistry::test());
|
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let message = channel::ChannelMessage {
|
let message = channel::ChannelMessage {
|
||||||
id: ChannelMessageId::Saved(0),
|
id: ChannelMessageId::Saved(0),
|
||||||
body: "Here is a link https://zed.dev to zeds website".to_string(),
|
body: "Here is a link https://zed.dev to zeds website".to_string(),
|
||||||
@ -1130,8 +1130,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_render_markdown_with_auto_detect_links_and_additional_formatting() {
|
fn test_render_markdown_with_auto_detect_links_and_additional_formatting(cx: &mut AppContext) {
|
||||||
let language_registry = Arc::new(LanguageRegistry::test());
|
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let message = channel::ChannelMessage {
|
let message = channel::ChannelMessage {
|
||||||
id: ChannelMessageId::Saved(0),
|
id: ChannelMessageId::Saved(0),
|
||||||
body: "**Here is a link https://zed.dev to zeds website**".to_string(),
|
body: "**Here is a link https://zed.dev to zeds website**".to_string(),
|
||||||
|
@ -624,7 +624,7 @@ mod tests {
|
|||||||
MessageEditorSettings::register(cx);
|
MessageEditorSettings::register(cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let language_registry = Arc::new(LanguageRegistry::test());
|
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
language_registry.add(Arc::new(Language::new(
|
language_registry.add(Arc::new(Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
name: "Markdown".into(),
|
name: "Markdown".into(),
|
||||||
|
@ -225,12 +225,14 @@ impl CopilotButton {
|
|||||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||||
let language = snapshot.language_at(suggestion_anchor);
|
let language = snapshot.language_at(suggestion_anchor);
|
||||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||||
|
self.editor_enabled = {
|
||||||
self.editor_enabled = Some(
|
let file = file.as_ref();
|
||||||
file.as_ref().map(|file| !file.is_private()).unwrap_or(true)
|
Some(
|
||||||
&& all_language_settings(self.file.as_ref(), cx)
|
file.map(|file| !file.is_private()).unwrap_or(true)
|
||||||
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
&& all_language_settings(file, cx)
|
||||||
);
|
.copilot_enabled(language, file.map(|file| file.path().as_ref())),
|
||||||
|
)
|
||||||
|
};
|
||||||
self.language = language.cloned();
|
self.language = language.cloned();
|
||||||
self.file = file;
|
self.file = file;
|
||||||
|
|
||||||
|
@ -9302,8 +9302,7 @@ impl Editor {
|
|||||||
.redacted_ranges(search_range, |file| {
|
.redacted_ranges(search_range, |file| {
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
file.is_private()
|
file.is_private()
|
||||||
&& EditorSettings::get(Some((file.worktree_id(), file.path())), cx)
|
&& EditorSettings::get(Some(file.as_ref().into()), cx).redact_private_values
|
||||||
.redact_private_values
|
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -9645,22 +9644,16 @@ impl Editor {
|
|||||||
telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension)
|
telemetry.report_copilot_event(suggestion_id, suggestion_accepted, file_extension)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
fn report_editor_event(
|
|
||||||
&self,
|
|
||||||
_operation: &'static str,
|
|
||||||
_file_extension: Option<String>,
|
|
||||||
_cx: &AppContext,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
fn report_editor_event(
|
fn report_editor_event(
|
||||||
&self,
|
&self,
|
||||||
operation: &'static str,
|
operation: &'static str,
|
||||||
file_extension: Option<String>,
|
file_extension: Option<String>,
|
||||||
cx: &AppContext,
|
cx: &AppContext,
|
||||||
) {
|
) {
|
||||||
|
if cfg!(any(test, feature = "test-support")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(project) = &self.project else { return };
|
let Some(project) = &self.project else { return };
|
||||||
|
|
||||||
// If None, we are in a file without an extension
|
// If None, we are in a file without an extension
|
||||||
|
@ -15,8 +15,7 @@ use language::{
|
|||||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
|
||||||
BracketPairConfig,
|
BracketPairConfig,
|
||||||
Capability::ReadWrite,
|
Capability::ReadWrite,
|
||||||
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageRegistry,
|
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
|
||||||
Override, Point,
|
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::project_settings::{LspSettings, ProjectSettings};
|
use project::project_settings::{LspSettings, ProjectSettings};
|
||||||
@ -4447,10 +4446,8 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
|
|||||||
Some(tree_sitter_rust::language()),
|
Some(tree_sitter_rust::language()),
|
||||||
));
|
));
|
||||||
|
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
cx.language_registry().add(language.clone());
|
||||||
registry.add(language.clone());
|
|
||||||
cx.update_buffer(|buffer, cx| {
|
cx.update_buffer(|buffer, cx| {
|
||||||
buffer.set_language_registry(registry);
|
|
||||||
buffer.set_language(Some(language), cx);
|
buffer.set_language(Some(language), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4649,12 +4646,10 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
|
|||||||
Some(tree_sitter_typescript::language_tsx()),
|
Some(tree_sitter_typescript::language_tsx()),
|
||||||
));
|
));
|
||||||
|
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
cx.language_registry().add(html_language.clone());
|
||||||
registry.add(html_language.clone());
|
cx.language_registry().add(javascript_language.clone());
|
||||||
registry.add(javascript_language.clone());
|
|
||||||
|
|
||||||
cx.update_buffer(|buffer, cx| {
|
cx.update_buffer(|buffer, cx| {
|
||||||
buffer.set_language_registry(registry);
|
|
||||||
buffer.set_language(Some(html_language), cx);
|
buffer.set_language(Some(html_language), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -4829,11 +4824,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
|
|||||||
.unwrap(),
|
.unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
cx.language_registry().add(rust_language.clone());
|
||||||
registry.add(rust_language.clone());
|
|
||||||
|
|
||||||
cx.update_buffer(|buffer, cx| {
|
cx.update_buffer(|buffer, cx| {
|
||||||
buffer.set_language_registry(registry);
|
|
||||||
buffer.set_language(Some(rust_language), cx);
|
buffer.set_language(Some(rust_language), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -6139,12 +6131,10 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
|
|||||||
Some(tree_sitter_rust::language()),
|
Some(tree_sitter_rust::language()),
|
||||||
));
|
));
|
||||||
|
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
|
||||||
registry.add(language.clone());
|
|
||||||
|
|
||||||
let mut cx = EditorTestContext::new(cx).await;
|
let mut cx = EditorTestContext::new(cx).await;
|
||||||
|
|
||||||
|
cx.language_registry().add(language.clone());
|
||||||
cx.update_buffer(|buffer, cx| {
|
cx.update_buffer(|buffer, cx| {
|
||||||
buffer.set_language_registry(registry);
|
|
||||||
buffer.set_language(Some(language), cx);
|
buffer.set_language(Some(language), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -6294,12 +6284,9 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
|
|||||||
Some(tree_sitter_typescript::language_tsx()),
|
Some(tree_sitter_typescript::language_tsx()),
|
||||||
));
|
));
|
||||||
|
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
cx.language_registry().add(html_language.clone());
|
||||||
registry.add(html_language.clone());
|
cx.language_registry().add(javascript_language.clone());
|
||||||
registry.add(javascript_language.clone());
|
|
||||||
|
|
||||||
cx.update_buffer(|buffer, cx| {
|
cx.update_buffer(|buffer, cx| {
|
||||||
buffer.set_language_registry(registry);
|
|
||||||
buffer.set_language(Some(html_language), cx);
|
buffer.set_language(Some(html_language), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -824,6 +824,8 @@ mod tests {
|
|||||||
.next()
|
.next()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
let languages = cx.language_registry().clone();
|
||||||
|
|
||||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||||
cx.editor(|editor, _| {
|
cx.editor(|editor, _| {
|
||||||
let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
|
let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
|
||||||
@ -835,7 +837,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
|
|
||||||
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
|
let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
rendered.text,
|
rendered.text,
|
||||||
code_str.trim(),
|
code_str.trim(),
|
||||||
@ -916,6 +918,7 @@ mod tests {
|
|||||||
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
|
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
||||||
|
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
let editor = cx.add_window(|cx| Editor::single_line(cx));
|
let editor = cx.add_window(|cx| Editor::single_line(cx));
|
||||||
editor
|
editor
|
||||||
.update(cx, |editor, _cx| {
|
.update(cx, |editor, _cx| {
|
||||||
@ -1028,7 +1031,7 @@ mod tests {
|
|||||||
expected_styles,
|
expected_styles,
|
||||||
} in &rows[0..]
|
} in &rows[0..]
|
||||||
{
|
{
|
||||||
let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
|
let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
|
||||||
|
|
||||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||||
let expected_highlights = ranges
|
let expected_highlights = ranges
|
||||||
|
@ -3387,17 +3387,7 @@ pub mod tests {
|
|||||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||||
|
|
||||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||||
language_registry.add(Arc::new(Language::new(
|
language_registry.add(crate::editor_tests::rust_lang());
|
||||||
LanguageConfig {
|
|
||||||
name: "Rust".into(),
|
|
||||||
matcher: LanguageMatcher {
|
|
||||||
path_suffixes: vec!["rs".to_string()],
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
Some(tree_sitter_rust::language()),
|
|
||||||
)));
|
|
||||||
let mut fake_servers = language_registry.register_fake_lsp_adapter(
|
let mut fake_servers = language_registry.register_fake_lsp_adapter(
|
||||||
"Rust",
|
"Rust",
|
||||||
FakeLspAdapter {
|
FakeLspAdapter {
|
||||||
|
@ -1247,65 +1247,15 @@ fn path_for_file<'a>(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use std::{
|
use language::TestFile;
|
||||||
path::{Path, PathBuf},
|
use std::path::Path;
|
||||||
sync::Arc,
|
|
||||||
time::SystemTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_path_for_file(cx: &mut AppContext) {
|
fn test_path_for_file(cx: &mut AppContext) {
|
||||||
let file = TestFile {
|
let file = TestFile {
|
||||||
path: Path::new("").into(),
|
path: Path::new("").into(),
|
||||||
full_path: PathBuf::from(""),
|
root_name: String::new(),
|
||||||
};
|
};
|
||||||
assert_eq!(path_for_file(&file, 0, false, cx), None);
|
assert_eq!(path_for_file(&file, 0, false, cx), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TestFile {
|
|
||||||
path: Arc<Path>,
|
|
||||||
full_path: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl language::File for TestFile {
|
|
||||||
fn path(&self) -> &Arc<Path> {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
|
|
||||||
fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
|
|
||||||
self.full_path.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mtime(&self) -> Option<SystemTime> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{Buffer, BufferSnapshot};
|
use language::{Buffer, BufferSnapshot, LanguageRegistry};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use project::{FakeFs, Project};
|
use project::{FakeFs, Project};
|
||||||
use std::{
|
use std::{
|
||||||
@ -120,6 +120,18 @@ impl EditorTestContext {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn language_registry(&mut self) -> Arc<LanguageRegistry> {
|
||||||
|
self.editor(|editor, cx| {
|
||||||
|
editor
|
||||||
|
.project
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.languages()
|
||||||
|
.clone()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
pub fn update_buffer<F, T>(&mut self, update: F) -> T
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
|
||||||
|
@ -249,7 +249,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
|||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let language_registry = Arc::new(LanguageRegistry::test());
|
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||||
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
|
||||||
let node_runtime = FakeNodeRuntime::new();
|
let node_runtime = FakeNodeRuntime::new();
|
||||||
|
|
||||||
|
@ -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(
|
pub(crate) fn contiguous_ranges(
|
||||||
values: impl Iterator<Item = u32>,
|
values: impl Iterator<Item = u32>,
|
||||||
max_len: usize,
|
max_len: usize,
|
||||||
|
@ -69,8 +69,10 @@ fn test_line_endings(cx: &mut gpui::AppContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_select_language() {
|
fn test_select_language(cx: &mut AppContext) {
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
init_settings(cx, |_| {});
|
||||||
|
|
||||||
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
registry.add(Arc::new(Language::new(
|
registry.add(Arc::new(Language::new(
|
||||||
LanguageConfig {
|
LanguageConfig {
|
||||||
name: "Rust".into(),
|
name: "Rust".into(),
|
||||||
@ -97,14 +99,14 @@ fn test_select_language() {
|
|||||||
// matching file extension
|
// matching file extension
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_file("zed/lib.rs".as_ref(), None)
|
.language_for_file(&file("src/lib.rs"), None, cx)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Rust".into())
|
Some("Rust".into())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_file("zed/lib.mk".as_ref(), None)
|
.language_for_file(&file("src/lib.mk"), None, cx)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Make".into())
|
Some("Make".into())
|
||||||
@ -113,7 +115,7 @@ fn test_select_language() {
|
|||||||
// matching filename
|
// matching filename
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_file("zed/Makefile".as_ref(), None)
|
.language_for_file(&file("src/Makefile"), None, cx)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
Some("Make".into())
|
Some("Make".into())
|
||||||
@ -122,27 +124,132 @@ fn test_select_language() {
|
|||||||
// matching suffix that is not the full file extension or filename
|
// matching suffix that is not the full file extension or filename
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_file("zed/cars".as_ref(), None)
|
.language_for_file(&file("zed/cars"), None, cx)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_file("zed/a.cars".as_ref(), None)
|
.language_for_file(&file("zed/a.cars"), None, cx)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
None
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
registry
|
registry
|
||||||
.language_for_file("zed/sumk".as_ref(), None)
|
.language_for_file(&file("zed/sumk"), None, cx)
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(|l| Some(l.ok()?.name())),
|
.and_then(|l| Some(l.ok()?.name())),
|
||||||
None
|
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]
|
#[gpui::test]
|
||||||
fn test_edit_events(cx: &mut gpui::AppContext) {
|
fn test_edit_events(cx: &mut gpui::AppContext) {
|
||||||
let mut now = Instant::now();
|
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 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(html_language.clone());
|
||||||
language_registry.add(javascript_language.clone());
|
language_registry.add(javascript_language.clone());
|
||||||
|
|
||||||
@ -1895,7 +2002,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
|
|||||||
"#
|
"#
|
||||||
.unindent();
|
.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(ruby_lang()));
|
||||||
language_registry.add(Arc::new(html_lang()));
|
language_registry.add(Arc::new(html_lang()));
|
||||||
language_registry.add(Arc::new(erb_lang()));
|
language_registry.add(Arc::new(erb_lang()));
|
||||||
|
@ -852,11 +852,7 @@ struct BracketConfig {
|
|||||||
|
|
||||||
impl Language {
|
impl Language {
|
||||||
pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
|
pub fn new(config: LanguageConfig, ts_language: Option<tree_sitter::Language>) -> Self {
|
||||||
Self::new_with_id(
|
Self::new_with_id(LanguageId::new(), config, ts_language)
|
||||||
LanguageId(NEXT_LANGUAGE_ID.fetch_add(1, SeqCst)),
|
|
||||||
config,
|
|
||||||
ts_language,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_with_id(
|
fn new_with_id(
|
||||||
@ -1569,44 +1565,9 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use gpui::TestAppContext;
|
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)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_language_loading(cx: &mut TestAppContext) {
|
async fn test_language_loading(cx: &mut TestAppContext) {
|
||||||
let mut languages = LanguageRegistry::test();
|
let languages = LanguageRegistry::test(cx.executor());
|
||||||
languages.set_executor(cx.executor());
|
|
||||||
let languages = Arc::new(languages);
|
let languages = Arc::new(languages);
|
||||||
languages.register_native_grammars([
|
languages.register_native_grammars([
|
||||||
("json", tree_sitter_json::language()),
|
("json", tree_sitter_json::language()),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
CachedLspAdapter, Language, LanguageConfig, LanguageContextProvider, LanguageId,
|
language_settings::all_language_settings, CachedLspAdapter, File, Language, LanguageConfig,
|
||||||
LanguageMatcher, LanguageServerName, LspAdapter, LspAdapterDelegate, PARSER, PLAIN_TEXT,
|
LanguageContextProvider, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter,
|
||||||
|
LspAdapterDelegate, PARSER, PLAIN_TEXT,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use collections::{hash_map, HashMap};
|
use collections::{hash_map, HashMap};
|
||||||
@ -10,7 +11,7 @@ use futures::{
|
|||||||
Future, FutureExt as _,
|
Future, FutureExt as _,
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, BackgroundExecutor, Task};
|
use gpui::{AppContext, BackgroundExecutor, Task};
|
||||||
use lsp::{LanguageServerBinary, LanguageServerId};
|
use lsp::LanguageServerId;
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use std::{
|
use std::{
|
||||||
@ -30,11 +31,7 @@ pub struct LanguageRegistry {
|
|||||||
state: RwLock<LanguageRegistryState>,
|
state: RwLock<LanguageRegistryState>,
|
||||||
language_server_download_dir: Option<Arc<Path>>,
|
language_server_download_dir: Option<Arc<Path>>,
|
||||||
login_shell_env_loaded: Shared<Task<()>>,
|
login_shell_env_loaded: Shared<Task<()>>,
|
||||||
#[allow(clippy::type_complexity)]
|
executor: BackgroundExecutor,
|
||||||
lsp_binary_paths: Mutex<
|
|
||||||
HashMap<LanguageServerName, Shared<Task<Result<LanguageServerBinary, Arc<anyhow::Error>>>>>,
|
|
||||||
>,
|
|
||||||
executor: Option<BackgroundExecutor>,
|
|
||||||
lsp_binary_status_tx: LspBinaryStatusSender,
|
lsp_binary_status_tx: LspBinaryStatusSender,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,12 +118,12 @@ struct LspBinaryStatusSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LanguageRegistry {
|
impl LanguageRegistry {
|
||||||
pub fn new(login_shell_env_loaded: Task<()>) -> Self {
|
pub fn new(login_shell_env_loaded: Task<()>, executor: BackgroundExecutor) -> Self {
|
||||||
Self {
|
let this = Self {
|
||||||
state: RwLock::new(LanguageRegistryState {
|
state: RwLock::new(LanguageRegistryState {
|
||||||
next_language_server_id: 0,
|
next_language_server_id: 0,
|
||||||
languages: vec![PLAIN_TEXT.clone()],
|
languages: Vec::new(),
|
||||||
available_languages: Default::default(),
|
available_languages: Vec::new(),
|
||||||
grammars: Default::default(),
|
grammars: Default::default(),
|
||||||
loading_languages: Default::default(),
|
loading_languages: Default::default(),
|
||||||
lsp_adapters: Default::default(),
|
lsp_adapters: Default::default(),
|
||||||
@ -140,21 +137,18 @@ impl LanguageRegistry {
|
|||||||
}),
|
}),
|
||||||
language_server_download_dir: None,
|
language_server_download_dir: None,
|
||||||
login_shell_env_loaded: login_shell_env_loaded.shared(),
|
login_shell_env_loaded: login_shell_env_loaded.shared(),
|
||||||
lsp_binary_paths: Default::default(),
|
|
||||||
executor: None,
|
|
||||||
lsp_binary_status_tx: Default::default(),
|
lsp_binary_status_tx: Default::default(),
|
||||||
}
|
executor,
|
||||||
}
|
};
|
||||||
|
this.add(PLAIN_TEXT.clone());
|
||||||
#[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());
|
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_executor(&mut self, executor: BackgroundExecutor) {
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
self.executor = Some(executor);
|
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.
|
/// Clears out all of the loaded languages and reload them from scratch.
|
||||||
@ -317,8 +311,19 @@ impl LanguageRegistry {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Add a pre-loaded language to the registry.
|
||||||
pub fn add(&self, language: Arc<Language>) {
|
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<()> {
|
pub fn subscribe(&self) -> watch::Receiver<()> {
|
||||||
@ -353,7 +358,13 @@ impl LanguageRegistry {
|
|||||||
name: &str,
|
name: &str,
|
||||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||||
let name = UniCase::new(name);
|
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? }
|
async move { rx.await? }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -363,28 +374,62 @@ impl LanguageRegistry {
|
|||||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||||
let string = UniCase::new(string);
|
let string = UniCase::new(string);
|
||||||
let rx = self.get_or_load_language(|name, config| {
|
let rx = self.get_or_load_language(|name, config| {
|
||||||
UniCase::new(name) == string
|
if UniCase::new(name) == string
|
||||||
|| config
|
|| config
|
||||||
.path_suffixes
|
.path_suffixes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|suffix| UniCase::new(suffix) == string)
|
.any(|suffix| UniCase::new(suffix) == string)
|
||||||
|
{
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
});
|
});
|
||||||
async move { rx.await? }
|
async move { rx.await? }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn language_for_file(
|
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>,
|
self: &Arc<Self>,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
content: Option<&Rope>,
|
content: Option<&Rope>,
|
||||||
|
user_file_types: Option<&HashMap<Arc<str>, Vec<String>>>,
|
||||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||||
let filename = path.file_name().and_then(|name| name.to_str());
|
let filename = path.file_name().and_then(|name| name.to_str());
|
||||||
let extension = path.extension_or_hidden_file_name();
|
let extension = path.extension_or_hidden_file_name();
|
||||||
let path_suffixes = [extension, filename];
|
let path_suffixes = [extension, filename];
|
||||||
let rx = self.get_or_load_language(move |_, config| {
|
let empty = Vec::new();
|
||||||
let path_matches = config
|
|
||||||
|
let rx = self.get_or_load_language(move |language_name, config| {
|
||||||
|
let path_matches_default_suffix = config
|
||||||
.path_suffixes
|
.path_suffixes
|
||||||
.iter()
|
.iter()
|
||||||
.any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
|
.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(
|
let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or(
|
||||||
false,
|
false,
|
||||||
|(content, pattern)| {
|
|(content, pattern)| {
|
||||||
@ -394,93 +439,110 @@ impl LanguageRegistry {
|
|||||||
pattern.is_match(&text)
|
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? }
|
async move { rx.await? }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_or_load_language(
|
fn get_or_load_language(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
callback: impl Fn(&str, &LanguageMatcher) -> bool,
|
callback: impl Fn(&str, &LanguageMatcher) -> usize,
|
||||||
) -> oneshot::Receiver<Result<Arc<Language>>> {
|
) -> oneshot::Receiver<Result<Arc<Language>>> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
let mut state = self.state.write();
|
let mut state = self.state.write();
|
||||||
if let Some(language) = state
|
let Some((language, _)) = state
|
||||||
.languages
|
.available_languages
|
||||||
.iter()
|
.iter()
|
||||||
.find(|language| callback(language.config.name.as_ref(), &language.config.matcher))
|
.filter_map(|language| {
|
||||||
{
|
let score = callback(&language.name, &language.matcher);
|
||||||
let _ = tx.send(Ok(language.clone()));
|
if score > 0 {
|
||||||
} else if let Some(executor) = self.executor.clone() {
|
Some((language.clone(), score))
|
||||||
if let Some(language) = state
|
} else {
|
||||||
.available_languages
|
None
|
||||||
.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]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} 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
|
rx
|
||||||
@ -502,43 +564,40 @@ impl LanguageRegistry {
|
|||||||
txs.push(tx);
|
txs.push(tx);
|
||||||
}
|
}
|
||||||
AvailableGrammar::Unloaded(wasm_path) => {
|
AvailableGrammar::Unloaded(wasm_path) => {
|
||||||
if let Some(executor) = &self.executor {
|
let this = self.clone();
|
||||||
let this = self.clone();
|
self.executor
|
||||||
executor
|
.spawn({
|
||||||
.spawn({
|
let wasm_path = wasm_path.clone();
|
||||||
let wasm_path = wasm_path.clone();
|
async move {
|
||||||
async move {
|
let wasm_bytes = std::fs::read(&wasm_path)?;
|
||||||
let wasm_bytes = std::fs::read(&wasm_path)?;
|
let grammar_name = wasm_path
|
||||||
let grammar_name = wasm_path
|
.file_stem()
|
||||||
.file_stem()
|
.and_then(OsStr::to_str)
|
||||||
.and_then(OsStr::to_str)
|
.ok_or_else(|| anyhow!("invalid grammar filename"))?;
|
||||||
.ok_or_else(|| anyhow!("invalid grammar filename"))?;
|
let grammar = PARSER.with(|parser| {
|
||||||
let grammar = PARSER.with(|parser| {
|
let mut parser = parser.borrow_mut();
|
||||||
let mut parser = parser.borrow_mut();
|
let mut store = parser.take_wasm_store().unwrap();
|
||||||
let mut store = parser.take_wasm_store().unwrap();
|
let grammar = store.load_language(&grammar_name, &wasm_bytes);
|
||||||
let grammar =
|
parser.set_wasm_store(store).unwrap();
|
||||||
store.load_language(&grammar_name, &wasm_bytes);
|
grammar
|
||||||
parser.set_wasm_store(store).unwrap();
|
})?;
|
||||||
grammar
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if let Some(AvailableGrammar::Loading(_, txs)) =
|
if let Some(AvailableGrammar::Loading(_, txs)) =
|
||||||
this.state.write().grammars.insert(
|
this.state.write().grammars.insert(
|
||||||
name,
|
name,
|
||||||
AvailableGrammar::Loaded(wasm_path, grammar.clone()),
|
AvailableGrammar::Loaded(wasm_path, grammar.clone()),
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
for tx in txs {
|
for tx in txs {
|
||||||
tx.send(Ok(grammar.clone())).ok();
|
tx.send(Ok(grammar.clone())).ok();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
.detach();
|
anyhow::Ok(())
|
||||||
*grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
|
}
|
||||||
}
|
})
|
||||||
|
.detach();
|
||||||
|
*grammar = AvailableGrammar::Loading(wasm_path.clone(), vec![tx]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -694,9 +753,6 @@ impl LanguageRegistry {
|
|||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
log::info!("deleting server container");
|
log::info!("deleting server container");
|
||||||
|
|
||||||
let mut lock = self.lsp_binary_paths.lock();
|
|
||||||
lock.remove(&adapter.name);
|
|
||||||
|
|
||||||
let download_dir = self
|
let download_dir = self
|
||||||
.language_server_download_dir
|
.language_server_download_dir
|
||||||
.clone()
|
.clone()
|
||||||
@ -716,13 +772,6 @@ impl LanguageRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
impl Default for LanguageRegistry {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::test()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LanguageRegistryState {
|
impl LanguageRegistryState {
|
||||||
fn next_language_server_id(&mut self) -> LanguageServerId {
|
fn next_language_server_id(&mut self) -> LanguageServerId {
|
||||||
LanguageServerId(post_inc(&mut self.next_language_server_id))
|
LanguageServerId(post_inc(&mut self.next_language_server_id))
|
||||||
|
@ -10,9 +10,18 @@ use schemars::{
|
|||||||
JsonSchema,
|
JsonSchema,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
use settings::{Settings, SettingsLocation};
|
||||||
use std::{num::NonZeroU32, path::Path, sync::Arc};
|
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.
|
/// Initializes the language settings.
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
AllLanguageSettings::register(cx);
|
AllLanguageSettings::register(cx);
|
||||||
@ -33,7 +42,7 @@ pub fn all_language_settings<'a>(
|
|||||||
file: Option<&Arc<dyn File>>,
|
file: Option<&Arc<dyn File>>,
|
||||||
cx: &'a AppContext,
|
cx: &'a AppContext,
|
||||||
) -> &'a AllLanguageSettings {
|
) -> &'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)
|
AllLanguageSettings::get(location, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +53,7 @@ pub struct AllLanguageSettings {
|
|||||||
pub copilot: CopilotSettings,
|
pub copilot: CopilotSettings,
|
||||||
defaults: LanguageSettings,
|
defaults: LanguageSettings,
|
||||||
languages: HashMap<Arc<str>, LanguageSettings>,
|
languages: HashMap<Arc<str>, LanguageSettings>,
|
||||||
|
pub(crate) file_types: HashMap<Arc<str>, Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The settings for a particular language.
|
/// The settings for a particular language.
|
||||||
@ -121,6 +131,10 @@ pub struct AllLanguageSettingsContent {
|
|||||||
/// The settings for individual languages.
|
/// The settings for individual languages.
|
||||||
#[serde(default, alias = "language_overrides")]
|
#[serde(default, alias = "language_overrides")]
|
||||||
pub languages: HashMap<Arc<str>, LanguageSettingsContent>,
|
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.
|
/// 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 {
|
Ok(Self {
|
||||||
copilot: CopilotSettings {
|
copilot: CopilotSettings {
|
||||||
feature_enabled: copilot_enabled,
|
feature_enabled: copilot_enabled,
|
||||||
@ -512,6 +536,7 @@ impl settings::Settings for AllLanguageSettings {
|
|||||||
},
|
},
|
||||||
defaults,
|
defaults,
|
||||||
languages,
|
languages,
|
||||||
|
file_types,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{LanguageConfig, LanguageMatcher};
|
use crate::{LanguageConfig, LanguageMatcher};
|
||||||
|
use gpui::AppContext;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use std::{env, ops::Range, sync::Arc};
|
use std::{env, ops::Range, sync::Arc};
|
||||||
use text::{Buffer, BufferId};
|
use text::{Buffer, BufferId};
|
||||||
@ -79,8 +80,8 @@ fn test_splice_included_ranges() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_syntax_map_layers_for_range() {
|
fn test_syntax_map_layers_for_range(cx: &mut AppContext) {
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let language = Arc::new(rust_lang());
|
let language = Arc::new(rust_lang());
|
||||||
registry.add(language.clone());
|
registry.add(language.clone());
|
||||||
|
|
||||||
@ -176,8 +177,8 @@ fn test_syntax_map_layers_for_range() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_dynamic_language_injection() {
|
fn test_dynamic_language_injection(cx: &mut AppContext) {
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let markdown = Arc::new(markdown_lang());
|
let markdown = Arc::new(markdown_lang());
|
||||||
registry.add(markdown.clone());
|
registry.add(markdown.clone());
|
||||||
registry.add(Arc::new(rust_lang()));
|
registry.add(Arc::new(rust_lang()));
|
||||||
@ -254,7 +255,7 @@ fn test_dynamic_language_injection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_typing_multiple_new_injections() {
|
fn test_typing_multiple_new_injections(cx: &mut AppContext) {
|
||||||
let (buffer, syntax_map) = test_edit_sequence(
|
let (buffer, syntax_map) = test_edit_sequence(
|
||||||
"Rust",
|
"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«.»])) }",
|
||||||
"fn a() { test_macro!(b.c(vec![d.«e»])) }",
|
"fn a() { test_macro!(b.c(vec![d.«e»])) }",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_capture_ranges(
|
assert_capture_ranges(
|
||||||
@ -283,7 +285,7 @@ fn test_typing_multiple_new_injections() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[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(
|
let (buffer, syntax_map) = test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
@ -309,6 +311,7 @@ fn test_pasting_new_injection_line_between_others() {
|
|||||||
}
|
}
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_capture_ranges(
|
assert_capture_ranges(
|
||||||
@ -330,7 +333,7 @@ fn test_pasting_new_injection_line_between_others() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[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(
|
let (buffer, syntax_map) = test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
@ -355,6 +358,7 @@ fn test_joining_injections_with_child_injections() {
|
|||||||
}
|
}
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_capture_ranges(
|
assert_capture_ranges(
|
||||||
@ -374,7 +378,7 @@ fn test_joining_injections_with_child_injections() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_editing_edges_of_injection() {
|
fn test_editing_edges_of_injection(cx: &mut AppContext) {
|
||||||
test_edit_sequence(
|
test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
@ -399,11 +403,12 @@ fn test_editing_edges_of_injection() {
|
|||||||
}
|
}
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_edits_preceding_and_intersecting_injection() {
|
fn test_edits_preceding_and_intersecting_injection(cx: &mut AppContext) {
|
||||||
test_edit_sequence(
|
test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
@ -411,11 +416,12 @@ fn test_edits_preceding_and_intersecting_injection() {
|
|||||||
"const aaaaaaaaaaaa: B = c!(d(e.f));",
|
"const aaaaaaaaaaaa: B = c!(d(e.f));",
|
||||||
"const aˇa: B = c!(d(eˇ));",
|
"const aˇa: B = c!(d(eˇ));",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_non_local_changes_create_injections() {
|
fn test_non_local_changes_create_injections(cx: &mut AppContext) {
|
||||||
test_edit_sequence(
|
test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
@ -430,11 +436,12 @@ fn test_non_local_changes_create_injections() {
|
|||||||
ˇ}
|
ˇ}
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_creating_many_injections_in_one_edit() {
|
fn test_creating_many_injections_in_one_edit(cx: &mut AppContext) {
|
||||||
test_edit_sequence(
|
test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
@ -460,11 +467,12 @@ fn test_creating_many_injections_in_one_edit() {
|
|||||||
}
|
}
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_editing_across_injection_boundary() {
|
fn test_editing_across_injection_boundary(cx: &mut AppContext) {
|
||||||
test_edit_sequence(
|
test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
@ -488,11 +496,12 @@ fn test_editing_across_injection_boundary() {
|
|||||||
}
|
}
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_removing_injection_by_replacing_across_boundary() {
|
fn test_removing_injection_by_replacing_across_boundary(cx: &mut AppContext) {
|
||||||
test_edit_sequence(
|
test_edit_sequence(
|
||||||
"Rust",
|
"Rust",
|
||||||
&[
|
&[
|
||||||
@ -514,11 +523,12 @@ fn test_removing_injection_by_replacing_across_boundary() {
|
|||||||
}
|
}
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_combined_injections_simple() {
|
fn test_combined_injections_simple(cx: &mut AppContext) {
|
||||||
let (buffer, syntax_map) = test_edit_sequence(
|
let (buffer, syntax_map) = test_edit_sequence(
|
||||||
"ERB",
|
"ERB",
|
||||||
&[
|
&[
|
||||||
@ -549,6 +559,7 @@ fn test_combined_injections_simple() {
|
|||||||
</body>
|
</body>
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_capture_ranges(
|
assert_capture_ranges(
|
||||||
@ -565,7 +576,7 @@ fn test_combined_injections_simple() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_combined_injections_empty_ranges() {
|
fn test_combined_injections_empty_ranges(cx: &mut AppContext) {
|
||||||
test_edit_sequence(
|
test_edit_sequence(
|
||||||
"ERB",
|
"ERB",
|
||||||
&[
|
&[
|
||||||
@ -579,11 +590,12 @@ fn test_combined_injections_empty_ranges() {
|
|||||||
ˇ<% end %>
|
ˇ<% end %>
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[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(
|
let (buffer, syntax_map) = test_edit_sequence(
|
||||||
"ERB",
|
"ERB",
|
||||||
&[
|
&[
|
||||||
@ -600,6 +612,7 @@ fn test_combined_injections_edit_edges_of_ranges() {
|
|||||||
<%= three @four %>
|
<%= three @four %>
|
||||||
",
|
",
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_capture_ranges(
|
assert_capture_ranges(
|
||||||
@ -614,7 +627,7 @@ fn test_combined_injections_edit_edges_of_ranges() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[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(
|
let (_buffer, _syntax_map) = test_edit_sequence(
|
||||||
"ERB",
|
"ERB",
|
||||||
&[
|
&[
|
||||||
@ -635,11 +648,12 @@ fn test_combined_injections_splitting_some_injections() {
|
|||||||
<% f %>
|
<% f %>
|
||||||
"#,
|
"#,
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_combined_injections_editing_after_last_injection() {
|
fn test_combined_injections_editing_after_last_injection(cx: &mut AppContext) {
|
||||||
test_edit_sequence(
|
test_edit_sequence(
|
||||||
"ERB",
|
"ERB",
|
||||||
&[
|
&[
|
||||||
@ -655,11 +669,12 @@ fn test_combined_injections_editing_after_last_injection() {
|
|||||||
more text»
|
more text»
|
||||||
"#,
|
"#,
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_combined_injections_inside_injections() {
|
fn test_combined_injections_inside_injections(cx: &mut AppContext) {
|
||||||
let (buffer, syntax_map) = test_edit_sequence(
|
let (buffer, syntax_map) = test_edit_sequence(
|
||||||
"Markdown",
|
"Markdown",
|
||||||
&[
|
&[
|
||||||
@ -709,6 +724,7 @@ fn test_combined_injections_inside_injections() {
|
|||||||
```
|
```
|
||||||
"#,
|
"#,
|
||||||
],
|
],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that the code directive below the ruby comment is
|
// Check that the code directive below the ruby comment is
|
||||||
@ -735,7 +751,7 @@ fn test_combined_injections_inside_injections() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[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(
|
let (buffer, syntax_map) = test_edit_sequence(
|
||||||
"Markdown",
|
"Markdown",
|
||||||
&[r#"
|
&[r#"
|
||||||
@ -745,6 +761,7 @@ fn test_empty_combined_injections_inside_injections() {
|
|||||||
|
|
||||||
goodbye
|
goodbye
|
||||||
"#],
|
"#],
|
||||||
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_layers_for_range(
|
assert_layers_for_range(
|
||||||
@ -763,7 +780,7 @@ fn test_empty_combined_injections_inside_injections() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 50)]
|
#[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#"
|
let text = r#"
|
||||||
fn test_something() {
|
fn test_something() {
|
||||||
let vec = vec![5, 1, 3, 8];
|
let vec = vec![5, 1, 3, 8];
|
||||||
@ -781,7 +798,7 @@ fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
|
|||||||
.unindent()
|
.unindent()
|
||||||
.repeat(2);
|
.repeat(2);
|
||||||
|
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let language = Arc::new(rust_lang());
|
let language = Arc::new(rust_lang());
|
||||||
registry.add(language.clone());
|
registry.add(language.clone());
|
||||||
|
|
||||||
@ -789,7 +806,7 @@ fn test_random_syntax_map_edits_rust_macros(rng: StdRng) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 50)]
|
#[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#"
|
let text = r#"
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<% if one?(:two) %>
|
<% if one?(:two) %>
|
||||||
@ -808,7 +825,7 @@ fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
|
|||||||
.unindent()
|
.unindent()
|
||||||
.repeat(5);
|
.repeat(5);
|
||||||
|
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let language = Arc::new(erb_lang());
|
let language = Arc::new(erb_lang());
|
||||||
registry.add(language.clone());
|
registry.add(language.clone());
|
||||||
registry.add(Arc::new(ruby_lang()));
|
registry.add(Arc::new(ruby_lang()));
|
||||||
@ -818,7 +835,7 @@ fn test_random_syntax_map_edits_with_erb(rng: StdRng) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 50)]
|
#[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#"
|
let text = r#"
|
||||||
defmodule TheModule do
|
defmodule TheModule do
|
||||||
def the_method(assigns) do
|
def the_method(assigns) do
|
||||||
@ -841,7 +858,7 @@ fn test_random_syntax_map_edits_with_heex(rng: StdRng) {
|
|||||||
.unindent()
|
.unindent()
|
||||||
.repeat(3);
|
.repeat(3);
|
||||||
|
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let language = Arc::new(elixir_lang());
|
let language = Arc::new(elixir_lang());
|
||||||
registry.add(language.clone());
|
registry.add(language.clone());
|
||||||
registry.add(Arc::new(heex_lang()));
|
registry.add(Arc::new(heex_lang()));
|
||||||
@ -1025,8 +1042,12 @@ fn check_interpolation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap) {
|
fn test_edit_sequence(
|
||||||
let registry = Arc::new(LanguageRegistry::test());
|
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(elixir_lang()));
|
||||||
registry.add(Arc::new(heex_lang()));
|
registry.add(Arc::new(heex_lang()));
|
||||||
registry.add(Arc::new(rust_lang()));
|
registry.add(Arc::new(rust_lang()));
|
||||||
|
@ -1079,9 +1079,7 @@ fn main() {
|
|||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_code_block_with_language(executor: BackgroundExecutor) {
|
async fn test_code_block_with_language(executor: BackgroundExecutor) {
|
||||||
let mut language_registry = LanguageRegistry::test();
|
let language_registry = Arc::new(LanguageRegistry::test(executor.clone()));
|
||||||
language_registry.set_executor(executor);
|
|
||||||
let language_registry = Arc::new(language_registry);
|
|
||||||
language_registry.add(rust_lang());
|
language_registry.add(rust_lang());
|
||||||
|
|
||||||
let parsed = parse_markdown(
|
let parsed = parse_markdown(
|
||||||
|
@ -64,7 +64,7 @@ use worktree::LocalSnapshot;
|
|||||||
use rpc::{ErrorCode, ErrorExt as _};
|
use rpc::{ErrorCode, ErrorExt as _};
|
||||||
use search::SearchQuery;
|
use search::SearchQuery;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use settings::{watch_config_file, Settings, SettingsStore};
|
use settings::{watch_config_file, Settings, SettingsLocation, SettingsStore};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use similar::{ChangeTag, TextDiff};
|
use similar::{ChangeTag, TextDiff};
|
||||||
use smol::channel::{Receiver, Sender};
|
use smol::channel::{Receiver, Sender};
|
||||||
@ -861,8 +861,7 @@ impl Project {
|
|||||||
) -> Model<Project> {
|
) -> Model<Project> {
|
||||||
use clock::FakeSystemClock;
|
use clock::FakeSystemClock;
|
||||||
|
|
||||||
let mut languages = LanguageRegistry::test();
|
let languages = LanguageRegistry::test(cx.executor());
|
||||||
languages.set_executor(cx.executor());
|
|
||||||
let clock = Arc::new(FakeSystemClock::default());
|
let clock = Arc::new(FakeSystemClock::default());
|
||||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||||
let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
|
let client = cx.update(|cx| client::Client::new(clock, http_client.clone(), cx));
|
||||||
@ -2776,11 +2775,11 @@ impl Project {
|
|||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
// If the buffer has a language, set it and start the language server if we haven't already.
|
// If the buffer has a language, set it and start the language server if we haven't already.
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
let full_path = buffer.file()?.full_path(cx);
|
let file = buffer.file()?;
|
||||||
let content = buffer.as_rope();
|
let content = buffer.as_rope();
|
||||||
let new_language = self
|
let new_language = self
|
||||||
.languages
|
.languages
|
||||||
.language_for_file(&full_path, Some(content))
|
.language_for_file(file, Some(content), cx)
|
||||||
.now_or_never()?
|
.now_or_never()?
|
||||||
.ok()?;
|
.ok()?;
|
||||||
self.set_language_for_buffer(buffer_handle, new_language, cx);
|
self.set_language_for_buffer(buffer_handle, new_language, cx);
|
||||||
@ -2869,8 +2868,13 @@ impl Project {
|
|||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
let project_settings =
|
let project_settings = ProjectSettings::get(
|
||||||
ProjectSettings::get(Some((worktree_id.to_proto() as usize, Path::new(""))), cx);
|
Some(SettingsLocation {
|
||||||
|
worktree_id: worktree_id.to_proto() as usize,
|
||||||
|
path: Path::new(""),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
let lsp = project_settings.lsp.get(&adapter.name.0);
|
let lsp = project_settings.lsp.get(&adapter.name.0);
|
||||||
let override_options = lsp.and_then(|s| s.initialization_options.clone());
|
let override_options = lsp.and_then(|s| s.initialization_options.clone());
|
||||||
|
|
||||||
@ -3553,14 +3557,14 @@ impl Project {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|buffer| {
|
.filter_map(|buffer| {
|
||||||
let buffer = buffer.read(cx);
|
let buffer = buffer.read(cx);
|
||||||
let file = File::from_dyn(buffer.file())?;
|
let file = buffer.file()?;
|
||||||
let full_path = file.full_path(cx);
|
let worktree = File::from_dyn(Some(file))?.worktree.clone();
|
||||||
let language = self
|
let language = self
|
||||||
.languages
|
.languages
|
||||||
.language_for_file(&full_path, Some(buffer.as_rope()))
|
.language_for_file(file, Some(buffer.as_rope()), cx)
|
||||||
.now_or_never()?
|
.now_or_never()?
|
||||||
.ok()?;
|
.ok()?;
|
||||||
Some((file.worktree.clone(), language))
|
Some((worktree, language))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
for (worktree, language) in language_server_lookup_info {
|
for (worktree, language) in language_server_lookup_info {
|
||||||
@ -4900,11 +4904,15 @@ impl Project {
|
|||||||
if self.is_local() {
|
if self.is_local() {
|
||||||
let mut requests = Vec::new();
|
let mut requests = Vec::new();
|
||||||
for ((worktree_id, _), server_id) in self.language_server_ids.iter() {
|
for ((worktree_id, _), server_id) in self.language_server_ids.iter() {
|
||||||
let worktree_id = *worktree_id;
|
let Some(worktree_handle) = self.worktree_for_id(*worktree_id, cx) else {
|
||||||
let worktree_handle = self.worktree_for_id(worktree_id, cx);
|
continue;
|
||||||
let worktree = match worktree_handle.and_then(|tree| tree.read(cx).as_local()) {
|
};
|
||||||
Some(worktree) => worktree,
|
let worktree = worktree_handle.read(cx);
|
||||||
None => continue,
|
if !worktree.is_visible() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(worktree) = worktree.as_local() else {
|
||||||
|
continue;
|
||||||
};
|
};
|
||||||
let worktree_abs_path = worktree.abs_path().clone();
|
let worktree_abs_path = worktree.abs_path().clone();
|
||||||
|
|
||||||
@ -4952,7 +4960,7 @@ impl Project {
|
|||||||
(
|
(
|
||||||
adapter,
|
adapter,
|
||||||
language,
|
language,
|
||||||
worktree_id,
|
worktree_handle.downgrade(),
|
||||||
worktree_abs_path,
|
worktree_abs_path,
|
||||||
lsp_symbols,
|
lsp_symbols,
|
||||||
)
|
)
|
||||||
@ -4972,7 +4980,7 @@ impl Project {
|
|||||||
for (
|
for (
|
||||||
adapter,
|
adapter,
|
||||||
adapter_language,
|
adapter_language,
|
||||||
source_worktree_id,
|
source_worktree,
|
||||||
worktree_abs_path,
|
worktree_abs_path,
|
||||||
lsp_symbols,
|
lsp_symbols,
|
||||||
) in responses
|
) in responses
|
||||||
@ -4980,17 +4988,22 @@ impl Project {
|
|||||||
symbols.extend(lsp_symbols.into_iter().filter_map(
|
symbols.extend(lsp_symbols.into_iter().filter_map(
|
||||||
|(symbol_name, symbol_kind, symbol_location)| {
|
|(symbol_name, symbol_kind, symbol_location)| {
|
||||||
let abs_path = symbol_location.uri.to_file_path().ok()?;
|
let abs_path = symbol_location.uri.to_file_path().ok()?;
|
||||||
let mut worktree_id = source_worktree_id;
|
let source_worktree = source_worktree.upgrade()?;
|
||||||
|
let source_worktree_id = source_worktree.read(cx).id();
|
||||||
|
|
||||||
let path;
|
let path;
|
||||||
if let Some((worktree, rel_path)) =
|
let worktree;
|
||||||
|
if let Some((tree, rel_path)) =
|
||||||
this.find_local_worktree(&abs_path, cx)
|
this.find_local_worktree(&abs_path, cx)
|
||||||
{
|
{
|
||||||
worktree_id = worktree.read(cx).id();
|
worktree = tree;
|
||||||
path = rel_path;
|
path = rel_path;
|
||||||
} else {
|
} else {
|
||||||
|
worktree = source_worktree.clone();
|
||||||
path = relativize_path(&worktree_abs_path, &abs_path);
|
path = relativize_path(&worktree_abs_path, &abs_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let worktree_id = worktree.read(cx).id();
|
||||||
let project_path = ProjectPath {
|
let project_path = ProjectPath {
|
||||||
worktree_id,
|
worktree_id,
|
||||||
path: path.into(),
|
path: path.into(),
|
||||||
@ -4999,7 +5012,7 @@ impl Project {
|
|||||||
let adapter_language = adapter_language.clone();
|
let adapter_language = adapter_language.clone();
|
||||||
let language = this
|
let language = this
|
||||||
.languages
|
.languages
|
||||||
.language_for_file(&project_path.path, None)
|
.language_for_file_path(&project_path.path)
|
||||||
.unwrap_or_else(move |_| adapter_language);
|
.unwrap_or_else(move |_| adapter_language);
|
||||||
let adapter = adapter.clone();
|
let adapter = adapter.clone();
|
||||||
Some(async move {
|
Some(async move {
|
||||||
@ -8538,7 +8551,7 @@ impl Project {
|
|||||||
.symbol
|
.symbol
|
||||||
.ok_or_else(|| anyhow!("invalid symbol"))?;
|
.ok_or_else(|| anyhow!("invalid symbol"))?;
|
||||||
let symbol = this
|
let symbol = this
|
||||||
.update(&mut cx, |this, _| this.deserialize_symbol(symbol))?
|
.update(&mut cx, |this, _cx| this.deserialize_symbol(symbol))?
|
||||||
.await?;
|
.await?;
|
||||||
let symbol = this.update(&mut cx, |this, _| {
|
let symbol = this.update(&mut cx, |this, _| {
|
||||||
let signature = this.symbol_signature(&symbol.path);
|
let signature = this.symbol_signature(&symbol.path);
|
||||||
@ -8928,27 +8941,26 @@ impl Project {
|
|||||||
serialized_symbol: proto::Symbol,
|
serialized_symbol: proto::Symbol,
|
||||||
) -> impl Future<Output = Result<Symbol>> {
|
) -> impl Future<Output = Result<Symbol>> {
|
||||||
let languages = self.languages.clone();
|
let languages = self.languages.clone();
|
||||||
|
let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
|
||||||
|
let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
|
||||||
|
let kind = unsafe { mem::transmute(serialized_symbol.kind) };
|
||||||
|
let path = ProjectPath {
|
||||||
|
worktree_id,
|
||||||
|
path: PathBuf::from(serialized_symbol.path).into(),
|
||||||
|
};
|
||||||
|
let language = languages.language_for_file_path(&path.path);
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
|
let language = language.await.log_err();
|
||||||
let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
|
let adapter = language
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|language| languages.lsp_adapters(language).first().cloned());
|
||||||
let start = serialized_symbol
|
let start = serialized_symbol
|
||||||
.start
|
.start
|
||||||
.ok_or_else(|| anyhow!("invalid start"))?;
|
.ok_or_else(|| anyhow!("invalid start"))?;
|
||||||
let end = serialized_symbol
|
let end = serialized_symbol
|
||||||
.end
|
.end
|
||||||
.ok_or_else(|| anyhow!("invalid end"))?;
|
.ok_or_else(|| anyhow!("invalid end"))?;
|
||||||
let kind = unsafe { mem::transmute(serialized_symbol.kind) };
|
|
||||||
let path = ProjectPath {
|
|
||||||
worktree_id,
|
|
||||||
path: PathBuf::from(serialized_symbol.path).into(),
|
|
||||||
};
|
|
||||||
let language = languages
|
|
||||||
.language_for_file(&path.path, None)
|
|
||||||
.await
|
|
||||||
.log_err();
|
|
||||||
let adapter = language
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|language| languages.lsp_adapters(language).first().cloned());
|
|
||||||
Ok(Symbol {
|
Ok(Symbol {
|
||||||
language_server_name: LanguageServerName(
|
language_server_name: LanguageServerName(
|
||||||
serialized_symbol.language_server_name.into(),
|
serialized_symbol.language_server_name.into(),
|
||||||
@ -9419,6 +9431,15 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
|
|||||||
|
|
||||||
impl EventEmitter<Event> for Project {}
|
impl EventEmitter<Event> for Project {}
|
||||||
|
|
||||||
|
impl<'a> Into<SettingsLocation<'a>> for &'a ProjectPath {
|
||||||
|
fn into(self) -> SettingsLocation<'a> {
|
||||||
|
SettingsLocation {
|
||||||
|
worktree_id: self.worktree_id.to_usize(),
|
||||||
|
path: self.path.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
|
impl<P: AsRef<Path>> From<(WorktreeId, P)> for ProjectPath {
|
||||||
fn from((worktree_id, path): (WorktreeId, P)) -> Self {
|
fn from((worktree_id, path): (WorktreeId, P)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -583,7 +583,7 @@ impl SemanticIndex {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(language) = language_registry
|
if let Ok(language) = language_registry
|
||||||
.language_for_file(&absolute_path, None)
|
.language_for_file_path(&absolute_path)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// Test if file is valid parseable file
|
// Test if file is valid parseable file
|
||||||
@ -1144,7 +1144,7 @@ impl SemanticIndex {
|
|||||||
|
|
||||||
for mut pending_file in pending_files {
|
for mut pending_file in pending_files {
|
||||||
if let Ok(language) = language_registry
|
if let Ok(language) = language_registry
|
||||||
.language_for_file(&pending_file.relative_path, None)
|
.language_for_file_path(&pending_file.relative_path)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
|
if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref())
|
||||||
|
@ -5,8 +5,7 @@ use crate::{
|
|||||||
FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
|
FileToEmbed, JobHandle, SearchResult, SemanticIndex, EMBEDDING_QUEUE_FLUSH_TIMEOUT,
|
||||||
};
|
};
|
||||||
use ai::test::FakeEmbeddingProvider;
|
use ai::test::FakeEmbeddingProvider;
|
||||||
|
use gpui::TestAppContext;
|
||||||
use gpui::{Task, TestAppContext};
|
|
||||||
use language::{Language, LanguageConfig, LanguageMatcher, LanguageRegistry, ToOffset};
|
use language::{Language, LanguageConfig, LanguageMatcher, LanguageRegistry, ToOffset};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
@ -57,7 +56,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let languages = Arc::new(LanguageRegistry::new(Task::ready(())));
|
let languages = Arc::new(LanguageRegistry::test(cx.executor().clone()));
|
||||||
let rust_language = rust_lang();
|
let rust_language = rust_lang();
|
||||||
let toml_language = toml_lang();
|
let toml_language = toml_lang();
|
||||||
languages.add(rust_language);
|
languages.add(rust_language);
|
||||||
@ -1720,6 +1719,7 @@ fn init_test(cx: &mut TestAppContext) {
|
|||||||
let settings_store = SettingsStore::test(cx);
|
let settings_store = SettingsStore::test(cx);
|
||||||
cx.set_global(settings_store);
|
cx.set_global(settings_store);
|
||||||
SemanticIndexSettings::register(cx);
|
SemanticIndexSettings::register(cx);
|
||||||
|
language::init(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ use util::asset_str;
|
|||||||
|
|
||||||
pub use keymap_file::KeymapFile;
|
pub use keymap_file::KeymapFile;
|
||||||
pub use settings_file::*;
|
pub use settings_file::*;
|
||||||
pub use settings_store::{Settings, SettingsJsonSchemaParams, SettingsStore};
|
pub use settings_store::{Settings, SettingsJsonSchemaParams, SettingsLocation, SettingsStore};
|
||||||
|
|
||||||
#[derive(RustEmbed)]
|
#[derive(RustEmbed)]
|
||||||
#[folder = "../../assets"]
|
#[folder = "../../assets"]
|
||||||
|
@ -86,9 +86,8 @@ pub trait Settings: 'static + Send + Sync {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// path is a (worktree ID, Path)
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn get<'a>(path: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a Self
|
fn get<'a>(path: Option<SettingsLocation>, cx: &'a AppContext) -> &'a Self
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
@ -120,6 +119,12 @@ pub trait Settings: 'static + Send + Sync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct SettingsLocation<'a> {
|
||||||
|
pub worktree_id: usize,
|
||||||
|
pub path: &'a Path,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SettingsJsonSchemaParams<'a> {
|
pub struct SettingsJsonSchemaParams<'a> {
|
||||||
pub staff_mode: bool,
|
pub staff_mode: bool,
|
||||||
pub language_names: &'a [String],
|
pub language_names: &'a [String],
|
||||||
@ -168,7 +173,7 @@ trait AnySettingValue: 'static + Send + Sync {
|
|||||||
custom: &[DeserializedSetting],
|
custom: &[DeserializedSetting],
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
) -> Result<Box<dyn Any>>;
|
) -> Result<Box<dyn Any>>;
|
||||||
fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any;
|
fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
|
||||||
fn set_global_value(&mut self, value: Box<dyn Any>);
|
fn set_global_value(&mut self, value: Box<dyn Any>);
|
||||||
fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
|
fn set_local_value(&mut self, root_id: usize, path: Arc<Path>, value: Box<dyn Any>);
|
||||||
fn json_schema(
|
fn json_schema(
|
||||||
@ -234,7 +239,7 @@ impl SettingsStore {
|
|||||||
///
|
///
|
||||||
/// Panics if the given setting type has not been registered, or if there is no
|
/// Panics if the given setting type has not been registered, or if there is no
|
||||||
/// value for this setting.
|
/// value for this setting.
|
||||||
pub fn get<T: Settings>(&self, path: Option<(usize, &Path)>) -> &T {
|
pub fn get<T: Settings>(&self, path: Option<SettingsLocation>) -> &T {
|
||||||
self.setting_values
|
self.setting_values
|
||||||
.get(&TypeId::of::<T>())
|
.get(&TypeId::of::<T>())
|
||||||
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
|
.unwrap_or_else(|| panic!("unregistered setting type {}", type_name::<T>()))
|
||||||
@ -659,10 +664,10 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
|
|||||||
Ok(DeserializedSetting(Box::new(value)))
|
Ok(DeserializedSetting(Box::new(value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_for_path(&self, path: Option<(usize, &Path)>) -> &dyn Any {
|
fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any {
|
||||||
if let Some((root_id, path)) = path {
|
if let Some(SettingsLocation { worktree_id, path }) = path {
|
||||||
for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
|
for (settings_root_id, settings_path, value) in self.local_values.iter().rev() {
|
||||||
if root_id == *settings_root_id && path.starts_with(settings_path) {
|
if worktree_id == *settings_root_id && path.starts_with(settings_path) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1010,7 +1015,10 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store.get::<UserSettings>(Some((1, Path::new("/root1/something")))),
|
store.get::<UserSettings>(Some(SettingsLocation {
|
||||||
|
worktree_id: 1,
|
||||||
|
path: Path::new("/root1/something"),
|
||||||
|
})),
|
||||||
&UserSettings {
|
&UserSettings {
|
||||||
name: "John Doe".to_string(),
|
name: "John Doe".to_string(),
|
||||||
age: 31,
|
age: 31,
|
||||||
@ -1018,7 +1026,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store.get::<UserSettings>(Some((1, Path::new("/root1/subdir/something")))),
|
store.get::<UserSettings>(Some(SettingsLocation {
|
||||||
|
worktree_id: 1,
|
||||||
|
path: Path::new("/root1/subdir/something")
|
||||||
|
})),
|
||||||
&UserSettings {
|
&UserSettings {
|
||||||
name: "Jane Doe".to_string(),
|
name: "Jane Doe".to_string(),
|
||||||
age: 31,
|
age: 31,
|
||||||
@ -1026,7 +1037,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store.get::<UserSettings>(Some((1, Path::new("/root2/something")))),
|
store.get::<UserSettings>(Some(SettingsLocation {
|
||||||
|
worktree_id: 1,
|
||||||
|
path: Path::new("/root2/something")
|
||||||
|
})),
|
||||||
&UserSettings {
|
&UserSettings {
|
||||||
name: "John Doe".to_string(),
|
name: "John Doe".to_string(),
|
||||||
age: 42,
|
age: 42,
|
||||||
@ -1034,7 +1048,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
store.get::<MultiKeySettings>(Some((1, Path::new("/root2/something")))),
|
store.get::<MultiKeySettings>(Some(SettingsLocation {
|
||||||
|
worktree_id: 1,
|
||||||
|
path: Path::new("/root2/something")
|
||||||
|
})),
|
||||||
&MultiKeySettings {
|
&MultiKeySettings {
|
||||||
key1: "a".to_string(),
|
key1: "a".to_string(),
|
||||||
key2: "b".to_string(),
|
key2: "b".to_string(),
|
||||||
|
@ -405,7 +405,7 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let fs = fs::FakeFs::new(cx.background_executor().clone());
|
let fs = fs::FakeFs::new(cx.background_executor().clone());
|
||||||
let languages = Arc::new(LanguageRegistry::test());
|
let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||||
let clock = Arc::new(clock::FakeSystemClock::default());
|
let clock = Arc::new(clock::FakeSystemClock::default());
|
||||||
let http_client = util::http::FakeHttpClient::with_404_response();
|
let http_client = util::http::FakeHttpClient::with_404_response();
|
||||||
let client = Client::new(clock, http_client.clone(), cx);
|
let client = Client::new(clock, http_client.clone(), cx);
|
||||||
|
@ -46,7 +46,7 @@ use postage::{
|
|||||||
watch,
|
watch,
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsLocation, SettingsStore};
|
||||||
use smol::channel::{self, Sender};
|
use smol::channel::{self, Sender};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
@ -352,7 +352,10 @@ impl Worktree {
|
|||||||
"file_scan_exclusions",
|
"file_scan_exclusions",
|
||||||
);
|
);
|
||||||
let new_private_files = path_matchers(
|
let new_private_files = path_matchers(
|
||||||
WorktreeSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
|
WorktreeSettings::get(Some(settings::SettingsLocation {
|
||||||
|
worktree_id: cx.handle().entity_id().as_u64() as usize,
|
||||||
|
path: Path::new("")
|
||||||
|
}), cx).private_files.as_deref(),
|
||||||
"private_files",
|
"private_files",
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -408,7 +411,10 @@ impl Worktree {
|
|||||||
"file_scan_exclusions",
|
"file_scan_exclusions",
|
||||||
),
|
),
|
||||||
private_files: path_matchers(
|
private_files: path_matchers(
|
||||||
WorktreeSettings::get(Some((cx.handle().entity_id().as_u64() as usize, &Path::new(""))), cx).private_files.as_deref(),
|
WorktreeSettings::get(Some(SettingsLocation {
|
||||||
|
worktree_id: cx.handle().entity_id().as_u64() as usize,
|
||||||
|
path: Path::new(""),
|
||||||
|
}), cx).private_files.as_deref(),
|
||||||
"private_files",
|
"private_files",
|
||||||
),
|
),
|
||||||
ignores_by_parent_abs_path: Default::default(),
|
ignores_by_parent_abs_path: Default::default(),
|
||||||
|
@ -142,9 +142,9 @@ fn main() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
let client = client::Client::new(clock, http.clone(), cx);
|
let client = client::Client::new(clock, http.clone(), cx);
|
||||||
let mut languages = LanguageRegistry::new(login_shell_env_loaded);
|
let mut languages =
|
||||||
|
LanguageRegistry::new(login_shell_env_loaded, cx.background_executor().clone());
|
||||||
let copilot_language_server_id = languages.next_language_server_id();
|
let copilot_language_server_id = languages.next_language_server_id();
|
||||||
languages.set_executor(cx.background_executor().clone());
|
|
||||||
languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
|
languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
|
||||||
let languages = Arc::new(languages);
|
let languages = Arc::new(languages);
|
||||||
let node_runtime = RealNodeRuntime::new(http.clone());
|
let node_runtime = RealNodeRuntime::new(http.clone());
|
||||||
|
@ -3024,15 +3024,18 @@ mod tests {
|
|||||||
async fn test_bundled_languages(cx: &mut TestAppContext) {
|
async fn test_bundled_languages(cx: &mut TestAppContext) {
|
||||||
let settings = cx.update(|cx| SettingsStore::test(cx));
|
let settings = cx.update(|cx| SettingsStore::test(cx));
|
||||||
cx.set_global(settings);
|
cx.set_global(settings);
|
||||||
let mut languages = LanguageRegistry::test();
|
let languages = LanguageRegistry::test(cx.executor());
|
||||||
languages.set_executor(cx.executor().clone());
|
|
||||||
let languages = Arc::new(languages);
|
let languages = Arc::new(languages);
|
||||||
let node_runtime = node_runtime::FakeNodeRuntime::new();
|
let node_runtime = node_runtime::FakeNodeRuntime::new();
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
languages::init(languages.clone(), node_runtime, cx);
|
languages::init(languages.clone(), node_runtime, cx);
|
||||||
});
|
});
|
||||||
for name in languages.language_names() {
|
for name in languages.language_names() {
|
||||||
languages.language_for_name(&name).await.unwrap();
|
languages
|
||||||
|
.language_for_name(&name)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("language name {name}"))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
}
|
}
|
||||||
|
@ -380,6 +380,25 @@ To override settings for a language, add an entry for that language server's nam
|
|||||||
|
|
||||||
`boolean` values
|
`boolean` values
|
||||||
|
|
||||||
|
## File Types
|
||||||
|
|
||||||
|
- Setting: `file_types`
|
||||||
|
- Description: Configure how Zed selects a language for a file based on its filename or extension.
|
||||||
|
- Default: `{}`
|
||||||
|
|
||||||
|
**Examples**
|
||||||
|
|
||||||
|
To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"file_types": {
|
||||||
|
"C++": ["c"],
|
||||||
|
"TOML": ["MyLockFile"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Git
|
## Git
|
||||||
|
|
||||||
- Description: Configuration for git-related features.
|
- Description: Configuration for git-related features.
|
||||||
|
Loading…
Reference in New Issue
Block a user