#![allow(clippy::format_collect)] mod agent_registry_store;mod bookmark_store;mod color_extractor;mod context_server_store;mod debugger;mod dynamic_registration;mod git_store;mod image_store;mod lsp_command;mod lsp_store;mod manifest_tree;mod project_search;mod search;mod search_history;mod signature_help;mod task_inventory;mod trusted_worktrees;mod yarn; use anyhow::Result;use async_trait::async_trait;use buffer_diff::{ BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks,};use collections::{BTreeSet, HashMap, HashSet};use encoding_rs;use fs::{FakeFs, PathEventKind, RealFs};use futures::{FutureExt as _, StreamExt, future};use git::{ GitHostingProviderRegistry, repository::{RepoPath, repo_path}, status::{DiffStat, FileStatus, StatusCode, TrackedStatus},};use gpui::{ App, AppContext, BackgroundExecutor, BorrowAppContext, Entity, FutureExt, SharedString, Task, TestAppContext, UpdateGlobal,};use itertools::Itertools;use language::{ Buffer, BufferEvent, Diagnostic, DiagnosticEntry, DiagnosticEntryRef, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, Language, LanguageAwareStyling, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point, ToPoint, Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata, language_settings::{ Formatter, FormatterList, LanguageSettings, LanguageSettingsContent, LineEndingSetting, }, markdown_lang, rust_lang, tree_sitter_typescript,};use lsp::{ CodeActionKind, DEFAULT_LSP_REQUEST_TIMEOUT, DiagnosticSeverity, DocumentChanges, FileOperationFilter, LanguageServerId, LanguageServerName, NumberOrString, TextDocumentEdit, Uri, WillRenameFiles, notification::DidRenameFiles,};use parking_lot::Mutex;use paths::{config_dir, global_gitignore_path, tasks_file};use postage::stream::Stream as _;use pretty_assertions::{assert_eq, assert_matches};use project::{ Event, TaskContexts, git_store::{GitStoreEvent, Repository, RepositoryEvent, StatusEntry, pending_op}, search::{SearchQuery, SearchResult}, task_store::{TaskSettingsLocation, TaskStore}, *,};use rand::{Rng as _, rngs::StdRng};use serde_json::json;use settings::{GlobalLspSettingsContent, SettingsStore};#[cfg(target_os = "linux")]use settings::{LocalSettingsKind, LocalSettingsPath};#[cfg(not(windows))]use std::os;use std::{ cell::RefCell, env, mem, num::NonZeroU32, ops::Range, path::{Path, PathBuf}, process::Command, rc::Rc, str::FromStr, sync::{Arc, OnceLock, atomic}, task::Poll, time::Duration,};use sum_tree::SumTree;use task::{ResolvedTask, ShellKind, TaskContext};use text::{Anchor, PointUtf16, ReplicaId, ToOffset, Unclipped};use unindent::Unindent as _;use util::{ RandomCharIter, TryFutureExt as _, assert_set_eq, maybe, path, paths::{PathMatcher, PathStyle}, rel_path::{RelPath, rel_path}, test::{TempTree, marked_text_offsets}, uri,};use worktree::WorktreeModelHandle as _; #[gpui::test]async fn test_block_via_channel(cx: &mut gpui::TestAppContext) { cx.executor().allow_parking(); let (tx, mut rx) = futures::channel::mpsc::unbounded(); let _thread = std::thread::spawn(move || { #[cfg(not(target_os = "windows"))] std::fs::metadata("/tmp").unwrap(); #[cfg(target_os = "windows")] std::fs::metadata("C:/Windows").unwrap(); std::thread::sleep(Duration::from_millis(1000)); tx.unbounded_send(1).unwrap(); }); rx.next().await.unwrap();} #[gpui::test]async fn test_block_via_smol(cx: &mut gpui::TestAppContext) { cx.executor().allow_parking(); let io_task = smol::unblock(move || { println!("sleeping on thread {:?}", std::thread::current().id()); std::thread::sleep(Duration::from_millis(10)); 1 }); let task = cx.foreground_executor().spawn(async move { io_task.await; }); task.await;} #[gpui::test]async fn test_default_session_work_dirs_prefers_directory_worktrees_over_single_file_parents( cx: &mut gpui::TestAppContext,) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ "dir-project": { "src": { "main.rs": "fn main() {}" } }, "single-file.rs": "fn helper() {}" }), ) .await; let project = Project::test( fs, [ Path::new(path!("/root/single-file.rs")), Path::new(path!("/root/dir-project")), ], cx, ) .await; let work_dirs = project.read_with(cx, |project, cx| project.default_path_list(cx)); let ordered_paths = work_dirs.ordered_paths().cloned().collect::<Vec<_>>(); assert_eq!( ordered_paths, vec![ PathBuf::from(path!("/root/dir-project")), PathBuf::from(path!("/root")), ] );} #[gpui::test]async fn test_default_session_work_dirs_falls_back_to_home_for_empty_project( cx: &mut gpui::TestAppContext,) { init_test(cx); let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; let work_dirs = project.read_with(cx, |project, cx| project.default_path_list(cx)); let ordered_paths = work_dirs.ordered_paths().cloned().collect::<Vec<_>>(); assert_eq!(ordered_paths, vec![paths::home_dir().to_path_buf()]);} // NOTE:// While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus// we assume that they are not supported out of the box.#[cfg(not(windows))]#[gpui::test]async fn test_symlinks(cx: &mut gpui::TestAppContext) { init_test(cx); cx.executor().allow_parking(); let dir = TempTree::new(json!({ "root": { "apple": "", "banana": { "carrot": { "date": "", "endive": "", } }, "fennel": { "grape": "", } } })); let root_link_path = dir.path().join("root_link"); os::unix::fs::symlink(dir.path().join("root"), &root_link_path).unwrap(); os::unix::fs::symlink( dir.path().join("root/fennel"), dir.path().join("root/finnochio"), ) .unwrap(); let project = Project::test( Arc::new(RealFs::new(None, cx.executor())), [root_link_path.as_ref()], cx, ) .await; project.update(cx, |project, cx| { let tree = project.worktrees(cx).next().unwrap().read(cx); assert_eq!(tree.file_count(), 5); assert_eq!( tree.entry_for_path(rel_path("fennel/grape")).unwrap().inode, tree.entry_for_path(rel_path("finnochio/grape")) .unwrap() .inode ); });} #[gpui::test]async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { init_test(cx); let dir = TempTree::new(json!({ ".editorconfig": r#" root = true [*.rs] indent_style = tab indent_size = 3 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true max_line_length = 120 [*.js] tab_width = 10 trim_trailing_whitespace = false max_line_length = off [*.json] trim_trailing_whitespace = true "#, ".zed": { "settings.json": r#"{ "tab_size": 8, "hard_tabs": false, "ensure_final_newline_on_save": false, "remove_trailing_whitespace_on_save": false, "preferred_line_length": 64, "soft_wrap": "editor_width", "languages": { "JavaScript": { "remove_trailing_whitespace_on_save": true, }, "JSON": { "remove_trailing_whitespace_on_save": true, }, "Markdown": { "remove_trailing_whitespace_on_save": true, }, }, }"#, }, "a.rs": "fn a() {\n A\n}", "b": { ".editorconfig": r#" [*.rs] indent_size = 2 max_line_length = off, "#, "b.rs": "fn b() {\n B\n}", }, "c.js": "def c\n C\nend", "d": { ".editorconfig": r#" [*.rs] indent_size = 1 "#, "d.rs": "fn d() {\n D\n}", }, "e": { ".editorconfig": r#" [*.rs] indent_size = 5 indent_style = space max_line_length = "#, "e.rs": "fn e() {\n E\n}", }, "README.json": "tabs are better\n", "README.md": "spaces are meaningful \n", })); let path = dir.path(); let fs = FakeFs::new(cx.executor()); fs.insert_tree_from_real_fs(path, path).await; let project = Project::test(fs, [path], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(js_lang()); language_registry.add(json_lang()); language_registry.add(markdown_lang()); language_registry.add(rust_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked(); let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings { let buffer = project .update(cx, |project, cx| { project.open_buffer((worktree.read(cx).id(), rel_path(path)), cx) }) .await .unwrap(); cx.update(|cx| LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned()) }; let settings_a = settings_for("a.rs", cx).await; let settings_b = settings_for("b/b.rs", cx).await; let settings_c = settings_for("c.js", cx).await; let settings_d = settings_for("d/d.rs", cx).await; let settings_e = settings_for("e/e.rs", cx).await; let settings_readme = settings_for("README.json", cx).await; let settings_markdown = settings_for("README.md", cx).await; // .editorconfig overrides .zed/settings assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3)); assert_eq!(settings_a.hard_tabs, true); assert_eq!(settings_a.ensure_final_newline_on_save, true); // .editorconfig can disable trailing whitespace removal, but should not // re-enable it when Zed settings have disabled it. assert_eq!(settings_a.remove_trailing_whitespace_on_save, false); assert_eq!(settings_a.line_ending, LineEndingSetting::EnforceLf); assert_eq!(settings_a.preferred_line_length, 120); // .editorconfig in b/ overrides .editorconfig in root assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); // .editorconfig in subdirectory overrides .editorconfig in root assert_eq!(Some(settings_d.tab_size), NonZeroU32::new(1)); // Non-empty values in e/ are parsed and applied as usual. assert_eq!(Some(settings_e.tab_size), NonZeroU32::new(5)); assert_eq!(settings_e.hard_tabs, false); // An empty value opts out of the inherited `max_line_length = 120`, // falling back to .zed/settings.json instead of rejecting the whole file. assert_eq!(settings_e.preferred_line_length, 64); // "indent_size" is not set, so "tab_width" is used assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); assert_eq!(settings_c.remove_trailing_whitespace_on_save, false); assert_eq!(settings_readme.remove_trailing_whitespace_on_save, true); assert_eq!(settings_markdown.remove_trailing_whitespace_on_save, true); // When max_line_length is "off", default to .zed/settings.json assert_eq!(settings_b.preferred_line_length, 64); assert_eq!(settings_c.preferred_line_length, 64); // README.md should not be affected by .editorconfig's globe "*.rs" assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));} #[gpui::test]async fn test_external_editorconfig_support(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/grandparent"), json!({ ".editorconfig": "[*]\nindent_size = 4\n", "parent": { ".editorconfig": "[*.rs]\nindent_size = 2\n", "worktree": { ".editorconfig": "[*.md]\nindent_size = 3\n", "main.rs": "fn main() {}", "README.md": "# README", "other.txt": "other content", } } }), ) .await; let project = Project::test(fs, [path!("/grandparent/parent/worktree").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); language_registry.add(markdown_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked(); let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings { let buffer = project .update(cx, |project, cx| { project.open_buffer((worktree.read(cx).id(), rel_path(path)), cx) }) .await .unwrap(); cx.update(|cx| LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned()) }; let settings_rs = settings_for("main.rs", cx).await; let settings_md = settings_for("README.md", cx).await; let settings_txt = settings_for("other.txt", cx).await; // main.rs gets indent_size = 2 from parent's external .editorconfig assert_eq!(Some(settings_rs.tab_size), NonZeroU32::new(2)); // README.md gets indent_size = 3 from internal worktree .editorconfig assert_eq!(Some(settings_md.tab_size), NonZeroU32::new(3)); // other.txt gets indent_size = 4 from grandparent's external .editorconfig assert_eq!(Some(settings_txt.tab_size), NonZeroU32::new(4));} #[gpui::test]async fn test_internal_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/worktree"), json!({ ".editorconfig": "[*]\nindent_size = 99\n", "src": { ".editorconfig": "root = true\n[*]\nindent_size = 2\n", "file.rs": "fn main() {}", } }), ) .await; let project = Project::test(fs, [path!("/worktree").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked(); let buffer = project .update(cx, |project, cx| { project.open_buffer((worktree.read(cx).id(), rel_path("src/file.rs")), cx) }) .await .unwrap(); cx.update(|cx| { let settings = LanguageSettings::for_buffer(buffer.read(cx), cx).into_owned(); assert_eq!(Some(settings.tab_size), NonZeroU32::new(2)); });} #[gpui::test]async fn test_external_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/parent"), json!({ ".editorconfig": "[*]\nindent_size = 99\n", "worktree": { ".editorconfig": "root = true\n[*]\nindent_size = 2\n", "file.rs": "fn main() {}", } }), ) .await; let project = Project::test(fs, [path!("/parent/worktree").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked();Lines 1–500 of 16449. Load more
#![allow(clippy::format_collect)] mod agent_registry_store; mod bookmark_store; mod color_extractor; mod context_server_store; mod debugger; mod dynamic_registration; mod git_store; mod image_store; mod lsp_command; mod lsp_store; mod manifest_tree; mod project_search; mod search; mod search_history; mod signature_help; mod task_inventory; mod trusted_worktrees; mod yarn; use anyhow::Result; use async_trait::async_trait; use buffer_diff::{ BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks, }; use collections::{BTreeSet, HashMap, HashSet}; use encoding_rs; use fs::{FakeFs, PathEventKind, RealFs}; use futures::{FutureExt as _, StreamExt, future}; use git::{ GitHostingProviderRegistry, repository::{RepoPath, repo_path}, status::{DiffStat, FileStatus, StatusCode, TrackedStatus}, }; use gpui::{ App, AppContext, BackgroundExecutor, BorrowAppContext, Entity, FutureExt, SharedString, Task, TestAppContext, UpdateGlobal, }; use itertools::Itertools; use language::{ Buffer, BufferEvent, Diagnostic, DiagnosticEntry, DiagnosticEntryRef, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, Language, LanguageAwareStyling, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point, ToPoint, Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata, language_settings::{ Formatter, FormatterList, LanguageSettings, LanguageSettingsContent, LineEndingSetting, }, markdown_lang, rust_lang, tree_sitter_typescript, }; use lsp::{ CodeActionKind, DEFAULT_LSP_REQUEST_TIMEOUT, DiagnosticSeverity, DocumentChanges, FileOperationFilter, LanguageServerId, LanguageServerName, NumberOrString, TextDocumentEdit, Uri, WillRenameFiles, notification::DidRenameFiles, }; use parking_lot::Mutex; use paths::{config_dir, global_gitignore_path, tasks_file}; use postage::stream::Stream as _; use pretty_assertions::{assert_eq, assert_matches}; use project::{ Event, TaskContexts, git_store::{GitStoreEvent, Repository, RepositoryEvent, StatusEntry, pending_op}, search::{SearchQuery, SearchResult}, task_store::{TaskSettingsLocation, TaskStore}, *, }; use rand::{Rng as _, rngs::StdRng}; use serde_json::json; use settings::{GlobalLspSettingsContent, SettingsStore}; #[cfg(target_os = "linux")] use settings::{LocalSettingsKind, LocalSettingsPath}; #[cfg(not(windows))] use std::os; use std::{ cell::RefCell, env, mem, num::NonZeroU32, ops::Range, path::{Path, PathBuf}, process::Command, rc::Rc, str::FromStr, sync::{Arc, OnceLock, atomic}, task::Poll, time::Duration, }; use sum_tree::SumTree; use task::{ResolvedTask, ShellKind, TaskContext}; use text::{Anchor, PointUtf16, ReplicaId, ToOffset, Unclipped}; use unindent::Unindent as _; use util::{ RandomCharIter, TryFutureExt as _, assert_set_eq, maybe, path, paths::{PathMatcher, PathStyle}, rel_path::{RelPath, rel_path}, test::{TempTree, marked_text_offsets}, uri, }; use worktree::WorktreeModelHandle as _; #[gpui::test] async fn test_block_via_channel(cx: &mut gpui::TestAppContext) { cx.executor().allow_parking(); let (tx, mut rx) = futures::channel::mpsc::unbounded(); let _thread = std::thread::spawn(move || { #[cfg(not(target_os = "windows"))] std::fs::metadata("/tmp").unwrap(); #[cfg(target_os = "windows")] std::fs::metadata("C:/Windows").unwrap(); std::thread::sleep(Duration::from_millis(1000)); tx.unbounded_send(1).unwrap(); }); rx.next().await.unwrap(); } #[gpui::test] async fn test_block_via_smol(cx: &mut gpui::TestAppContext) { cx.executor().allow_parking(); let io_task = smol::unblock(move || { println!("sleeping on thread {:?}", std::thread::current().id()); std::thread::sleep(Duration::from_millis(10)); 1 }); let task = cx.foreground_executor().spawn(async move { io_task.await; }); task.await; } #[gpui::test] async fn test_default_session_work_dirs_prefers_directory_worktrees_over_single_file_parents( cx: &mut gpui::TestAppContext, ) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/root"), json!({ "dir-project": { "src": { "main.rs": "fn main() {}" } }, "single-file.rs": "fn helper() {}" }), ) .await; let project = Project::test( fs, [ Path::new(path!("/root/single-file.rs")), Path::new(path!("/root/dir-project")), ], cx, ) .await; let work_dirs = project.read_with(cx, |project, cx| project.default_path_list(cx)); let ordered_paths = work_dirs.ordered_paths().cloned().collect::<Vec<_>>(); assert_eq!( ordered_paths, vec![ PathBuf::from(path!("/root/dir-project")), PathBuf::from(path!("/root")), ] ); } #[gpui::test] async fn test_default_session_work_dirs_falls_back_to_home_for_empty_project( cx: &mut gpui::TestAppContext, ) { init_test(cx); let fs = FakeFs::new(cx.executor()); let project = Project::test(fs, [], cx).await; let work_dirs = project.read_with(cx, |project, cx| project.default_path_list(cx)); let ordered_paths = work_dirs.ordered_paths().cloned().collect::<Vec<_>>(); assert_eq!(ordered_paths, vec![paths::home_dir().to_path_buf()]); } // NOTE: // While POSIX symbolic links are somewhat supported on Windows, they are an opt in by the user, and thus // we assume that they are not supported out of the box. #[cfg(not(windows))] #[gpui::test] async fn test_symlinks(cx: &mut gpui::TestAppContext) { init_test(cx); cx.executor().allow_parking(); let dir = TempTree::new(json!({ "root": { "apple": "", "banana": { "carrot": { "date": "", "endive": "", } }, "fennel": { "grape": "", } } })); let root_link_path = dir.path().join("root_link"); os::unix::fs::symlink(dir.path().join("root"), &root_link_path).unwrap(); os::unix::fs::symlink( dir.path().join("root/fennel"), dir.path().join("root/finnochio"), ) .unwrap(); let project = Project::test( Arc::new(RealFs::new(None, cx.executor())), [root_link_path.as_ref()], cx, ) .await; project.update(cx, |project, cx| { let tree = project.worktrees(cx).next().unwrap().read(cx); assert_eq!(tree.file_count(), 5); assert_eq!( tree.entry_for_path(rel_path("fennel/grape")).unwrap().inode, tree.entry_for_path(rel_path("finnochio/grape")) .unwrap() .inode ); }); } #[gpui::test] async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { init_test(cx); let dir = TempTree::new(json!({ ".editorconfig": r#" root = true [*.rs] indent_style = tab indent_size = 3 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true max_line_length = 120 [*.js] tab_width = 10 trim_trailing_whitespace = false max_line_length = off [*.json] trim_trailing_whitespace = true "#, ".zed": { "settings.json": r#"{ "tab_size": 8, "hard_tabs": false, "ensure_final_newline_on_save": false, "remove_trailing_whitespace_on_save": false, "preferred_line_length": 64, "soft_wrap": "editor_width", "languages": { "JavaScript": { "remove_trailing_whitespace_on_save": true, }, "JSON": { "remove_trailing_whitespace_on_save": true, }, "Markdown": { "remove_trailing_whitespace_on_save": true, }, }, }"#, }, "a.rs": "fn a() {\n A\n}", "b": { ".editorconfig": r#" [*.rs] indent_size = 2 max_line_length = off, "#, "b.rs": "fn b() {\n B\n}", }, "c.js": "def c\n C\nend", "d": { ".editorconfig": r#" [*.rs] indent_size = 1 "#, "d.rs": "fn d() {\n D\n}", }, "e": { ".editorconfig": r#" [*.rs] indent_size = 5 indent_style = space max_line_length = "#, "e.rs": "fn e() {\n E\n}", }, "README.json": "tabs are better\n", "README.md": "spaces are meaningful \n", })); let path = dir.path(); let fs = FakeFs::new(cx.executor()); fs.insert_tree_from_real_fs(path, path).await; let project = Project::test(fs, [path], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(js_lang()); language_registry.add(json_lang()); language_registry.add(markdown_lang()); language_registry.add(rust_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked(); let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings { let buffer = project .update(cx, |project, cx| { project.open_buffer((worktree.read(cx).id(), rel_path(path)), cx) }) .await .unwrap(); cx.update(|cx| LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned()) }; let settings_a = settings_for("a.rs", cx).await; let settings_b = settings_for("b/b.rs", cx).await; let settings_c = settings_for("c.js", cx).await; let settings_d = settings_for("d/d.rs", cx).await; let settings_e = settings_for("e/e.rs", cx).await; let settings_readme = settings_for("README.json", cx).await; let settings_markdown = settings_for("README.md", cx).await; // .editorconfig overrides .zed/settings assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3)); assert_eq!(settings_a.hard_tabs, true); assert_eq!(settings_a.ensure_final_newline_on_save, true); // .editorconfig can disable trailing whitespace removal, but should not // re-enable it when Zed settings have disabled it. assert_eq!(settings_a.remove_trailing_whitespace_on_save, false); assert_eq!(settings_a.line_ending, LineEndingSetting::EnforceLf); assert_eq!(settings_a.preferred_line_length, 120); // .editorconfig in b/ overrides .editorconfig in root assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); // .editorconfig in subdirectory overrides .editorconfig in root assert_eq!(Some(settings_d.tab_size), NonZeroU32::new(1)); // Non-empty values in e/ are parsed and applied as usual. assert_eq!(Some(settings_e.tab_size), NonZeroU32::new(5)); assert_eq!(settings_e.hard_tabs, false); // An empty value opts out of the inherited `max_line_length = 120`, // falling back to .zed/settings.json instead of rejecting the whole file. assert_eq!(settings_e.preferred_line_length, 64); // "indent_size" is not set, so "tab_width" is used assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); assert_eq!(settings_c.remove_trailing_whitespace_on_save, false); assert_eq!(settings_readme.remove_trailing_whitespace_on_save, true); assert_eq!(settings_markdown.remove_trailing_whitespace_on_save, true); // When max_line_length is "off", default to .zed/settings.json assert_eq!(settings_b.preferred_line_length, 64); assert_eq!(settings_c.preferred_line_length, 64); // README.md should not be affected by .editorconfig's globe "*.rs" assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); } #[gpui::test] async fn test_external_editorconfig_support(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/grandparent"), json!({ ".editorconfig": "[*]\nindent_size = 4\n", "parent": { ".editorconfig": "[*.rs]\nindent_size = 2\n", "worktree": { ".editorconfig": "[*.md]\nindent_size = 3\n", "main.rs": "fn main() {}", "README.md": "# README", "other.txt": "other content", } } }), ) .await; let project = Project::test(fs, [path!("/grandparent/parent/worktree").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); language_registry.add(markdown_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked(); let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings { let buffer = project .update(cx, |project, cx| { project.open_buffer((worktree.read(cx).id(), rel_path(path)), cx) }) .await .unwrap(); cx.update(|cx| LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned()) }; let settings_rs = settings_for("main.rs", cx).await; let settings_md = settings_for("README.md", cx).await; let settings_txt = settings_for("other.txt", cx).await; // main.rs gets indent_size = 2 from parent's external .editorconfig assert_eq!(Some(settings_rs.tab_size), NonZeroU32::new(2)); // README.md gets indent_size = 3 from internal worktree .editorconfig assert_eq!(Some(settings_md.tab_size), NonZeroU32::new(3)); // other.txt gets indent_size = 4 from grandparent's external .editorconfig assert_eq!(Some(settings_txt.tab_size), NonZeroU32::new(4)); } #[gpui::test] async fn test_internal_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/worktree"), json!({ ".editorconfig": "[*]\nindent_size = 99\n", "src": { ".editorconfig": "root = true\n[*]\nindent_size = 2\n", "file.rs": "fn main() {}", } }), ) .await; let project = Project::test(fs, [path!("/worktree").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked(); let buffer = project .update(cx, |project, cx| { project.open_buffer((worktree.read(cx).id(), rel_path("src/file.rs")), cx) }) .await .unwrap(); cx.update(|cx| { let settings = LanguageSettings::for_buffer(buffer.read(cx), cx).into_owned(); assert_eq!(Some(settings.tab_size), NonZeroU32::new(2)); }); } #[gpui::test] async fn test_external_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.executor()); fs.insert_tree( path!("/parent"), json!({ ".editorconfig": "[*]\nindent_size = 99\n", "worktree": { ".editorconfig": "root = true\n[*]\nindent_size = 2\n", "file.rs": "fn main() {}", } }), ) .await; let project = Project::test(fs, [path!("/parent/worktree").as_ref()], cx).await; let language_registry = project.read_with(cx, |project, _| project.languages().clone()); language_registry.add(rust_lang()); let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked();