diff --git a/Cargo.lock b/Cargo.lock index 748ec5ef49..f01022e294 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1341,6 +1341,7 @@ dependencies = [ "env_logger 0.9.3", "fuzzy", "gpui", + "language", "picker", "project", "serde_json", @@ -1408,6 +1409,7 @@ dependencies = [ "fs", "futures 0.3.28", "gpui", + "language", "settings", "smol", "theme", @@ -2034,6 +2036,7 @@ dependencies = [ "pulldown-cmark", "rand 0.8.5", "rpc", + "schemars", "serde", "serde_derive", "settings", @@ -2243,6 +2246,7 @@ dependencies = [ "env_logger 0.9.3", "fuzzy", "gpui", + "language", "menu", "picker", "postage", @@ -3427,6 +3431,7 @@ dependencies = [ "futures 0.3.28", "fuzzy", "git", + "glob", "gpui", "indoc", "lazy_static", @@ -3437,6 +3442,7 @@ dependencies = [ "rand 0.8.5", "regex", "rpc", + "schemars", "serde", "serde_derive", "serde_json", @@ -4872,6 +4878,7 @@ dependencies = [ "editor", "futures 0.3.28", "gpui", + "language", "menu", "postage", "project", @@ -7818,6 +7825,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vim" version = "0.1.0" dependencies = [ + "anyhow", "assets", "async-compat", "async-trait", diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 54f3f93b00..ebfa50aaf8 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -65,12 +65,14 @@ impl Setting for AutoUpdateSetting { type FileContent = Option; - fn load(default_value: &Option, user_values: &[&Option], _: &AppContext) -> Self { - Self( - Self::json_merge(default_value, user_values) - .unwrap() - .unwrap(), - ) + fn load( + default_value: &Option, + user_values: &[&Option], + _: &AppContext, + ) -> Result { + Ok(Self( + Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?, + )) } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index bcf9152051..f1aee9540e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -350,17 +350,18 @@ impl settings::Setting for TelemetrySettings { default_value: &Self::FileContent, user_values: &[&Self::FileContent], _: &AppContext, - ) -> Self { - Self { - diagnostics: user_values - .first() - .and_then(|v| v.diagnostics) - .unwrap_or(default_value.diagnostics.unwrap()), + ) -> Result { + Ok(Self { + diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or( + default_value + .diagnostics + .ok_or_else(Self::missing_default)?, + ), metrics: user_values .first() .and_then(|v| v.metrics) - .unwrap_or(default_value.metrics.unwrap()), - } + .unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?), + }) } } diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 50fb9658fe..02e5fa56e4 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -186,7 +186,10 @@ impl TestServer { }) }); - cx.update(|cx| client::init(&client, cx)); + cx.update(|cx| { + client::init(&client, cx); + language::init(cx); + }); let fs = FakeFs::new(cx.background()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e3b5b0be7e..2896c47808 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -18,6 +18,7 @@ use gpui::{ }; use indoc::indoc; use language::{ + language_settings::{AllLanguageSettings, Formatter}, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, OffsetRangeExt, Point, Rope, }; @@ -26,7 +27,7 @@ use lsp::LanguageServerId; use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath}; use rand::prelude::*; use serde_json::json; -use settings::{Formatter, Settings}; +use settings::{SettingsStore}; use std::{ cell::{Cell, RefCell}, env, future, mem, @@ -4219,10 +4220,12 @@ async fn test_formatting_buffer( // Ensure buffer can be formatted using an external command. Notice how the // host's configuration is honored as opposed to using the guest's settings. cx_a.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.editor_defaults.formatter = Some(Formatter::External { - command: "awk".to_string(), - arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()], + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |file| { + file.defaults.formatter = Some(Formatter::External { + command: "awk".into(), + arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(), + }); }); }); }); diff --git a/crates/command_palette/Cargo.toml b/crates/command_palette/Cargo.toml index 8ad1843cb6..95ba452c14 100644 --- a/crates/command_palette/Cargo.toml +++ b/crates/command_palette/Cargo.toml @@ -23,6 +23,7 @@ workspace = { path = "../workspace" } [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } serde_json.workspace = true workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 4e0e776000..12134f6d6e 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -294,14 +294,7 @@ mod tests { #[gpui::test] async fn test_command_palette(deterministic: Arc, cx: &mut TestAppContext) { - deterministic.forbid_parking(); - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - editor::init(cx); - workspace::init(app_state.clone(), cx); - init(cx); - }); + let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); @@ -369,4 +362,15 @@ mod tests { assert!(palette.delegate().matches.is_empty()) }); } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.update(|cx| { + let app_state = AppState::test(cx); + language::init(cx); + editor::init(cx); + workspace::init(app_state.clone(), cx); + init(cx); + app_state + }) + } } diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 9ccd9c445d..b966348cd6 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -10,6 +10,7 @@ use gpui::{ actions, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, }; use language::{ + language_settings::{all_language_settings, language_settings}, point_from_lsp, point_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16, }; @@ -17,7 +18,7 @@ use log::{debug, error}; use lsp::{LanguageServer, LanguageServerId}; use node_runtime::NodeRuntime; use request::{LogMessage, StatusNotification}; -use settings::Settings; +use settings::SettingsStore; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ ffi::OsString, @@ -302,56 +303,34 @@ impl Copilot { node_runtime: Arc, cx: &mut ModelContext, ) -> Self { - cx.observe_global::({ - let http = http.clone(); - let node_runtime = node_runtime.clone(); - move |this, cx| { - if cx.global::().features.copilot { - if matches!(this.server, CopilotServer::Disabled) { - let start_task = cx - .spawn({ - let http = http.clone(); - let node_runtime = node_runtime.clone(); - move |this, cx| { - Self::start_language_server(http, node_runtime, this, cx) - } - }) - .shared(); - this.server = CopilotServer::Starting { task: start_task }; - cx.notify(); - } - } else { - this.server = CopilotServer::Disabled; - cx.notify(); - } - } - }) - .detach(); + let mut this = Self { + http, + node_runtime, + server: CopilotServer::Disabled, + buffers: Default::default(), + }; + this.enable_or_disable_copilot(cx); + cx.observe_global::(move |this, cx| this.enable_or_disable_copilot(cx)) + .detach(); + this + } - if cx.global::().features.copilot { - let start_task = cx - .spawn({ - let http = http.clone(); - let node_runtime = node_runtime.clone(); - move |this, cx| async { - Self::start_language_server(http, node_runtime, this, cx).await - } - }) - .shared(); - - Self { - http, - node_runtime, - server: CopilotServer::Starting { task: start_task }, - buffers: Default::default(), + fn enable_or_disable_copilot(&mut self, cx: &mut ModelContext) { + let http = self.http.clone(); + let node_runtime = self.node_runtime.clone(); + if all_language_settings(None, cx).copilot_enabled(None, None) { + if matches!(self.server, CopilotServer::Disabled) { + let start_task = cx + .spawn({ + move |this, cx| Self::start_language_server(http, node_runtime, this, cx) + }) + .shared(); + self.server = CopilotServer::Starting { task: start_task }; + cx.notify(); } } else { - Self { - http, - node_runtime, - server: CopilotServer::Disabled, - buffers: Default::default(), - } + self.server = CopilotServer::Disabled; + cx.notify(); } } @@ -805,13 +784,14 @@ impl Copilot { let snapshot = registered_buffer.report_changes(buffer, cx); let buffer = buffer.read(cx); let uri = registered_buffer.uri.clone(); - let settings = cx.global::(); let position = position.to_point_utf16(buffer); - let language = buffer.language_at(position); - let language_name = language.map(|language| language.name()); - let language_name = language_name.as_deref(); - let tab_size = settings.tab_size(language_name); - let hard_tabs = settings.hard_tabs(language_name); + let settings = language_settings( + None, + buffer.language_at(position).map(|l| l.name()).as_deref(), + cx, + ); + let tab_size = settings.tab_size; + let hard_tabs = settings.hard_tabs; let relative_path = buffer .file() .map(|file| file.path().to_path_buf()) diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index 77937e8bd0..ad3febd68c 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -15,6 +15,7 @@ editor = { path = "../editor" } fs = { path = "../fs" } context_menu = { path = "../context_menu" } gpui = { path = "../gpui" } +language = { path = "../language" } settings = { path = "../settings" } theme = { path = "../theme" } util = { path = "../util" } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 61c84bc517..d04f1e0e75 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -9,6 +9,7 @@ use gpui::{ AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; +use language::language_settings::{self, all_language_settings, AllLanguageSettings}; use settings::{update_settings_file, Settings, SettingsStore}; use std::{path::Path, sync::Arc}; use util::{paths, ResultExt}; @@ -40,12 +41,12 @@ impl View for CopilotButton { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let settings = cx.global::(); - - if !settings.features.copilot { + let all_language_settings = &all_language_settings(None, cx); + if !all_language_settings.copilot.feature_enabled { return Empty::new().into_any(); } + let settings = cx.global::(); let theme = settings.theme.clone(); let active = self.popup_menu.read(cx).visible(); let Some(copilot) = Copilot::global(cx) else { @@ -55,7 +56,7 @@ impl View for CopilotButton { let enabled = self .editor_enabled - .unwrap_or(settings.show_copilot_suggestions(None, None)); + .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None)); Stack::new() .with_child( @@ -192,14 +193,14 @@ impl CopilotButton { } pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext) { - let settings = cx.global::(); let fs = self.fs.clone(); - let mut menu_options = Vec::with_capacity(8); if let Some(language) = self.language.clone() { let fs = fs.clone(); - let language_enabled = settings.copilot_enabled_for_language(Some(language.as_ref())); + let language_enabled = + language_settings::language_settings(None, Some(language.as_ref()), cx) + .show_copilot_suggestions; menu_options.push(ContextMenuItem::handler( format!( "{} Suggestions for {}", @@ -210,6 +211,8 @@ impl CopilotButton { )); } + let settings = settings::get_setting::(None, cx); + if let Some(path) = self.path.as_ref() { let path_enabled = settings.copilot_enabled_for_path(path); let path = path.clone(); @@ -234,7 +237,7 @@ impl CopilotButton { )); } - let globally_enabled = cx.global::().features.copilot; + let globally_enabled = settings.copilot_enabled(None, None); menu_options.push(ContextMenuItem::handler( if globally_enabled { "Hide Suggestions for All Files" @@ -246,7 +249,7 @@ impl CopilotButton { menu_options.push(ContextMenuItem::Separator); - let icon_style = settings.theme.copilot.out_link_icon.clone(); + let icon_style = cx.global::().theme.copilot.out_link_icon.clone(); menu_options.push(ContextMenuItem::action( move |state: &mut MouseState, style: &theme::ContextMenuItem| { Flex::row() @@ -272,22 +275,19 @@ impl CopilotButton { pub fn update_enabled(&mut self, editor: ViewHandle, cx: &mut ViewContext) { let editor = editor.read(cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); - let settings = cx.global::(); let suggestion_anchor = editor.selections.newest_anchor().start; - let language_name = snapshot .language_at(suggestion_anchor) .map(|language| language.name()); - let path = snapshot - .file_at(suggestion_anchor) - .map(|file| file.path().clone()); + let path = snapshot.file_at(suggestion_anchor).map(|file| file.path()); - self.editor_enabled = - Some(settings.show_copilot_suggestions(language_name.as_deref(), path.as_deref())); + self.editor_enabled = Some( + all_language_settings(None, cx) + .copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())), + ); self.language = language_name; - self.path = path; + self.path = path.cloned(); cx.notify() } @@ -328,27 +328,27 @@ async fn configure_disabled_globs( settings_editor.downgrade().update(&mut cx, |item, cx| { let text = item.buffer().read(cx).snapshot(cx).text(); - let edits = cx - .global::() - .update::(&text, |file| { - let copilot = file.copilot.get_or_insert_with(Default::default); - let globs = copilot.disabled_globs.get_or_insert_with(|| { - cx.global::() - .copilot - .disabled_globs - .clone() - .iter() - .map(|glob| glob.as_str().to_string()) - .collect::>() - }); - - if let Some(path_to_disable) = &path_to_disable { - globs.push(path_to_disable.to_string_lossy().into_owned()); - } else { - globs.clear(); - } + let settings = cx.global::(); + let edits = settings.edits_for_update::(&text, |file| { + let copilot = file.copilot.get_or_insert_with(Default::default); + let globs = copilot.disabled_globs.get_or_insert_with(|| { + settings + .get::(None) + .copilot + .disabled_globs + .clone() + .iter() + .map(|glob| glob.as_str().to_string()) + .collect::>() }); + if let Some(path_to_disable) = &path_to_disable { + globs.push(path_to_disable.to_string_lossy().into_owned()); + } else { + globs.clear(); + } + }); + if !edits.is_empty() { item.change_selections(Some(Autoscroll::newest()), cx, |selections| { selections.select_ranges(edits.iter().map(|e| e.0.clone())); @@ -365,31 +365,26 @@ async fn configure_disabled_globs( } fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = cx.global::().show_copilot_suggestions(None, None); - update_settings_file::(fs, cx, move |file_contents| { - file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) + let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None); + update_settings_file::(fs, cx, move |file| { + file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) }); } fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = cx - .global::() - .show_copilot_suggestions(Some(&language), None); - - update_settings_file::(fs, cx, move |file_contents| { - file_contents.languages.insert( - language, - settings::EditorSettings { - show_copilot_suggestions: Some((!show_copilot_suggestions).into()), - ..Default::default() - }, - ); + let show_copilot_suggestions = + all_language_settings(None, cx).copilot_enabled(Some(&language), None); + update_settings_file::(fs, cx, move |file| { + file.languages + .entry(language) + .or_default() + .show_copilot_suggestions = Some(!show_copilot_suggestions); }); } fn hide_copilot(fs: Arc, cx: &mut AppContext) { - update_settings_file::(fs, cx, move |file_contents| { - file_contents.features.copilot = Some(false) + update_settings_file::(fs, cx, move |file| { + file.features.get_or_insert(Default::default()).copilot = Some(false); }); } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index b9db439e49..62e03ca54f 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -820,11 +820,13 @@ mod tests { use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; use project::FakeFs; use serde_json::json; + use settings::SettingsStore; use unindent::Unindent as _; #[gpui::test] async fn test_diagnostics(cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/test", @@ -1227,7 +1229,8 @@ mod tests { #[gpui::test] async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { - Settings::test_async(cx); + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/test", @@ -1491,6 +1494,14 @@ mod tests { }); } + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + cx.set_global(Settings::test(cx)); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + }); + } + fn editor_blocks(editor: &ViewHandle, cx: &mut WindowContext) -> Vec<(u32, String)> { editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index e4767a12e2..fc7bf4b8ab 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -49,6 +49,7 @@ workspace = { path = "../workspace" } aho-corasick = "0.7" anyhow.workspace = true futures.workspace = true +glob.workspace = true indoc = "1.0.4" itertools = "0.10" lazy_static.workspace = true @@ -58,6 +59,7 @@ parking_lot.workspace = true postage.workspace = true pulldown-cmark = { version = "0.9.2", default-features = false } rand = { workspace = true, optional = true } +schemars.workspace = true serde.workspace = true serde_derive.workspace = true smallvec.workspace = true diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index e190ec7717..4c36be682b 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -13,8 +13,9 @@ use gpui::{ fonts::{FontId, HighlightStyle}, Entity, ModelContext, ModelHandle, }; -use language::{OffsetUtf16, Point, Subscription as BufferSubscription}; -use settings::Settings; +use language::{ + language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, +}; use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; pub use suggestion_map::Suggestion; use suggestion_map::SuggestionMap; @@ -276,8 +277,7 @@ impl DisplayMap { .as_singleton() .and_then(|buffer| buffer.read(cx).language()) .map(|language| language.name()); - - cx.global::().tab_size(language_name.as_deref()) + language_settings(None, language_name.as_deref(), cx).tab_size } #[cfg(test)] @@ -844,8 +844,12 @@ pub mod tests { use super::*; use crate::{movement, test::marked_display_snapshot}; use gpui::{color::Color, elements::*, test::observe, AppContext}; - use language::{Buffer, Language, LanguageConfig, SelectionGoal}; + use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent}, + Buffer, Language, LanguageConfig, SelectionGoal, + }; use rand::{prelude::*, Rng}; + use settings::SettingsStore; use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; @@ -882,9 +886,7 @@ pub mod tests { log::info!("wrap width: {:?}", wrap_width); cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = NonZeroU32::new(tab_size); - cx.set_global(settings) + init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size)); }); let buffer = cx.update(|cx| { @@ -939,9 +941,11 @@ pub mod tests { tab_size = *tab_sizes.choose(&mut rng).unwrap(); log::info!("setting tab size to {:?}", tab_size); cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = NonZeroU32::new(tab_size); - cx.set_global(settings) + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(tab_size); + }); + }); }); } 30..=44 => { @@ -1119,7 +1123,7 @@ pub mod tests { #[gpui::test(retries = 5)] fn test_soft_wraps(cx: &mut AppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let font_cache = cx.font_cache(); @@ -1131,7 +1135,6 @@ pub mod tests { .unwrap(); let font_size = 12.0; let wrap_width = Some(64.); - cx.set_global(Settings::test(cx)); let text = "one two three four five\nsix seven eight"; let buffer = MultiBuffer::build_simple(text, cx); @@ -1211,7 +1214,8 @@ pub mod tests { #[gpui::test] fn test_text_chunks(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); + let text = sample_text(6, 6, 'a'); let buffer = MultiBuffer::build_simple(&text, cx); let family_id = cx @@ -1225,6 +1229,7 @@ pub mod tests { let font_size = 14.0; let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx)); + buffer.update(cx, |buffer, cx| { buffer.edit( vec![ @@ -1289,11 +1294,8 @@ pub mod tests { .unwrap(), ); language.set_theme(&theme); - cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_defaults.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); - }); + + cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; @@ -1382,7 +1384,7 @@ pub mod tests { ); language.set_theme(&theme); - cx.update(|cx| cx.set_global(Settings::test(cx))); + cx.update(|cx| init_test(cx, |_| {})); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; @@ -1429,9 +1431,8 @@ pub mod tests { #[gpui::test] async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { - cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + cx.update(|cx| init_test(cx, |_| {})); - cx.update(|cx| cx.set_global(Settings::test(cx))); let theme = SyntaxTheme::new(vec![ ("operator".to_string(), Color::red().into()), ("string".to_string(), Color::green().into()), @@ -1510,7 +1511,8 @@ pub mod tests { #[gpui::test] fn test_clip_point(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); + fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) { let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); @@ -1559,7 +1561,7 @@ pub mod tests { #[gpui::test] fn test_clip_at_line_ends(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); fn assert(text: &str, cx: &mut gpui::AppContext) { let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); @@ -1578,7 +1580,8 @@ pub mod tests { #[gpui::test] fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); + let text = "✅\t\tα\nβ\t\n🏀β\t\tγ"; let buffer = MultiBuffer::build_simple(text, cx); let font_cache = cx.font_cache(); @@ -1639,7 +1642,8 @@ pub mod tests { #[gpui::test] fn test_max_point(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx, |_| {}); + let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx); let font_cache = cx.font_cache(); let family_id = font_cache @@ -1718,4 +1722,13 @@ pub mod tests { } chunks } + + fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + cx.foreground().forbid_parking(); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index abd8885d30..41087c6eaa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -51,6 +51,7 @@ pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ + language_settings::{self, all_language_settings}, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, @@ -436,7 +437,7 @@ pub enum EditorMode { Full, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum SoftWrap { None, EditorWidth, @@ -471,7 +472,7 @@ pub struct Editor { select_larger_syntax_node_stack: Vec]>>, ime_transaction: Option, active_diagnostics: Option, - soft_wrap_mode_override: Option, + soft_wrap_mode_override: Option, get_field_editor_theme: Option>, override_text_style: Option>, project: Option>, @@ -1247,7 +1248,7 @@ impl Editor { let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); let soft_wrap_mode_override = - (mode == EditorMode::SingleLine).then(|| settings::SoftWrap::None); + (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); let mut this = Self { handle: cx.weak_handle(), buffer: buffer.clone(), @@ -3116,17 +3117,12 @@ impl Editor { snapshot: &MultiBufferSnapshot, cx: &mut ViewContext, ) -> bool { - let settings = cx.global::(); - - let path = snapshot.file_at(location).map(|file| file.path()); + let path = snapshot.file_at(location).map(|file| file.path().as_ref()); let language_name = snapshot .language_at(location) .map(|language| language.name()); - if !settings.show_copilot_suggestions(language_name.as_deref(), path.map(|p| p.as_ref())) { - return false; - } - - true + let settings = all_language_settings(None, cx); + settings.copilot_enabled(language_name.as_deref(), path) } fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { @@ -3427,12 +3423,9 @@ impl Editor { { let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row); - let language_name = buffer - .language_at(line_buffer_range.start) - .map(|language| language.name()); let indent_len = match indent_size.kind { IndentKind::Space => { - cx.global::().tab_size(language_name.as_deref()) + buffer.settings_at(line_buffer_range.start, cx).tab_size } IndentKind::Tab => NonZeroU32::new(1).unwrap(), }; @@ -3544,12 +3537,11 @@ impl Editor { } // Otherwise, insert a hard or soft tab. - let settings = cx.global::(); - let language_name = buffer.language_at(cursor, cx).map(|l| l.name()); - let tab_size = if settings.hard_tabs(language_name.as_deref()) { + let settings = buffer.settings_at(cursor, cx); + let tab_size = if settings.hard_tabs { IndentSize::tab() } else { - let tab_size = settings.tab_size(language_name.as_deref()).get(); + let tab_size = settings.tab_size.get(); let char_column = snapshot .text_for_range(Point::new(cursor.row, 0)..cursor) .flat_map(str::chars) @@ -3602,10 +3594,9 @@ impl Editor { delta_for_start_row: u32, cx: &AppContext, ) -> u32 { - let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); - let settings = cx.global::(); - let tab_size = settings.tab_size(language_name.as_deref()).get(); - let indent_kind = if settings.hard_tabs(language_name.as_deref()) { + let settings = buffer.settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); + let indent_kind = if settings.hard_tabs { IndentKind::Tab } else { IndentKind::Space @@ -3674,11 +3665,8 @@ impl Editor { let buffer = self.buffer.read(cx); let snapshot = buffer.snapshot(cx); for selection in &selections { - let language_name = buffer.language_at(selection.start, cx).map(|l| l.name()); - let tab_size = cx - .global::() - .tab_size(language_name.as_deref()) - .get(); + let settings = buffer.settings_at(selection.start, cx); + let tab_size = settings.tab_size.get(); let mut rows = selection.spanned_rows(false, &display_map); // Avoid re-outdenting a row that has already been outdented by a @@ -6439,27 +6427,24 @@ impl Editor { } pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { - let language_name = self - .buffer - .read(cx) - .as_singleton() - .and_then(|singleton_buffer| singleton_buffer.read(cx).language()) - .map(|l| l.name()); - - let settings = cx.global::(); + let settings = self.buffer.read(cx).settings_at(0, cx); let mode = self .soft_wrap_mode_override - .unwrap_or_else(|| settings.soft_wrap(language_name.as_deref())); + .unwrap_or_else(|| settings.soft_wrap); match mode { - settings::SoftWrap::None => SoftWrap::None, - settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, - settings::SoftWrap::PreferredLineLength => { - SoftWrap::Column(settings.preferred_line_length(language_name.as_deref())) + language_settings::SoftWrap::None => SoftWrap::None, + language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, + language_settings::SoftWrap::PreferredLineLength => { + SoftWrap::Column(settings.preferred_line_length) } } } - pub fn set_soft_wrap_mode(&mut self, mode: settings::SoftWrap, cx: &mut ViewContext) { + pub fn set_soft_wrap_mode( + &mut self, + mode: language_settings::SoftWrap, + cx: &mut ViewContext, + ) { self.soft_wrap_mode_override = Some(mode); cx.notify(); } @@ -6474,8 +6459,8 @@ impl Editor { self.soft_wrap_mode_override.take(); } else { let soft_wrap = match self.soft_wrap_mode(cx) { - SoftWrap::None => settings::SoftWrap::EditorWidth, - SoftWrap::EditorWidth | SoftWrap::Column(_) => settings::SoftWrap::None, + SoftWrap::None => language_settings::SoftWrap::EditorWidth, + SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None, }; self.soft_wrap_mode_override = Some(soft_wrap); } @@ -6874,7 +6859,12 @@ impl Editor { .get("vim_mode") == Some(&serde_json::Value::Bool(true)); let telemetry_settings = *settings::get_setting::(None, cx); - let settings = cx.global::(); + let copilot_enabled = all_language_settings(None, cx).copilot_enabled(None, None); + let copilot_enabled_for_language = self + .buffer + .read(cx) + .settings_at(0, cx) + .show_copilot_suggestions; let extension = Path::new(file.file_name(cx)) .extension() @@ -6893,15 +6883,8 @@ impl Editor { file_extension: extension.map(ToString::to_string), vim_mode, operation: name, - copilot_enabled: settings.features.copilot, - copilot_enabled_for_language: settings.show_copilot_suggestions( - self.language_at(0, cx) - .map(|language| language.name()) - .as_deref(), - self.file_at(0, cx) - .map(|file| file.path().clone()) - .as_deref(), - ), + copilot_enabled, + copilot_enabled_for_language, }; telemetry.report_clickhouse_event(event, telemetry_settings) } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 0f749bde48..7ee6a52955 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -12,10 +12,12 @@ use gpui::{ serde_json, TestAppContext, }; use indoc::indoc; -use language::{BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point}; +use language::{ + language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent}, + BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point, +}; use parking_lot::Mutex; use project::FakeFs; -use settings::EditorSettings; use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; use unindent::Unindent; use util::{ @@ -29,7 +31,8 @@ use workspace::{ #[gpui::test] fn test_edit_events(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| { let mut buffer = language::Buffer::new(0, "123456", cx); buffer.set_group_interval(Duration::from_secs(1)); @@ -156,7 +159,8 @@ fn test_edit_events(cx: &mut TestAppContext) { #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let mut now = Instant::now(); let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); @@ -226,7 +230,8 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { #[gpui::test] fn test_ime_composition(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| { let mut buffer = language::Buffer::new(0, "abcde", cx); // Ensure automatic grouping doesn't occur. @@ -328,7 +333,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { #[gpui::test] fn test_selection_with_mouse(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); @@ -395,7 +400,8 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { #[gpui::test] fn test_canceling_pending_selection(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); build_editor(buffer, cx) @@ -429,6 +435,8 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { #[gpui::test] fn test_clone(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let (text, selection_ranges) = marked_text_ranges( indoc! {" one @@ -439,7 +447,6 @@ fn test_clone(cx: &mut TestAppContext) { "}, true, ); - cx.update(|cx| cx.set_global(Settings::test(cx))); let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&text, cx); @@ -487,7 +494,8 @@ fn test_clone(cx: &mut TestAppContext) { #[gpui::test] async fn test_navigation_history(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + cx.set_global(DragAndDrop::::default()); use workspace::item::Item; @@ -600,7 +608,8 @@ async fn test_navigation_history(cx: &mut TestAppContext) { #[gpui::test] fn test_cancel(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); build_editor(buffer, cx) @@ -642,7 +651,8 @@ fn test_cancel(cx: &mut TestAppContext) { #[gpui::test] fn test_fold_action(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple( &" @@ -731,7 +741,8 @@ fn test_fold_action(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); @@ -806,7 +817,8 @@ fn test_move_cursor(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor_multibyte(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); build_editor(buffer.clone(), cx) @@ -910,7 +922,8 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { #[gpui::test] fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); build_editor(buffer.clone(), cx) @@ -959,7 +972,8 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { #[gpui::test] fn test_beginning_end_of_line(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\n def", cx); build_editor(buffer, cx) @@ -1121,7 +1135,8 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { #[gpui::test] fn test_prev_next_word_boundary(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); build_editor(buffer, cx) @@ -1172,7 +1187,8 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { #[gpui::test] fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); build_editor(buffer, cx) @@ -1229,6 +1245,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { #[gpui::test] async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx); let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); @@ -1343,6 +1360,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx); cx.set_state("one «two threeˇ» four"); cx.update_editor(|editor, cx| { @@ -1353,7 +1371,8 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_delete_to_word_boundary(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("one two three four", cx); build_editor(buffer.clone(), cx) @@ -1388,7 +1407,8 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { #[gpui::test] fn test_newline(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); build_editor(buffer.clone(), cx) @@ -1410,7 +1430,8 @@ fn test_newline(cx: &mut TestAppContext) { #[gpui::test] fn test_newline_with_old_selections(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple( " @@ -1491,11 +1512,8 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { #[gpui::test] async fn test_newline_above(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap()); - }); + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) }); let language = Arc::new( @@ -1506,8 +1524,9 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) { .with_indents_query(r#"(_ "(" ")" @end) @indent"#) .unwrap(), ); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + let mut cx = EditorTestContext::new(cx); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); cx.set_state(indoc! {" const a: ˇA = ( (ˇ @@ -1516,6 +1535,7 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) { )ˇ ˇ);ˇ "}); + cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx)); cx.assert_editor_state(indoc! {" ˇ @@ -1540,11 +1560,8 @@ async fn test_newline_above(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_newline_below(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.tab_size = Some(NonZeroU32::new(4).unwrap()); - }); + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) }); let language = Arc::new( @@ -1555,8 +1572,9 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) { .with_indents_query(r#"(_ "(" ")" @end) @indent"#) .unwrap(), ); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + let mut cx = EditorTestContext::new(cx); + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); cx.set_state(indoc! {" const a: ˇA = ( (ˇ @@ -1565,6 +1583,7 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) { )ˇ ˇ);ˇ "}); + cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx)); cx.assert_editor_state(indoc! {" const a: A = ( @@ -1589,7 +1608,8 @@ async fn test_newline_below(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_insert_with_old_selections(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); let mut editor = build_editor(buffer.clone(), cx); @@ -1615,12 +1635,11 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) { #[gpui::test] async fn test_tab(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.tab_size = Some(NonZeroU32::new(3).unwrap()); - }); + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(3) }); + + let mut cx = EditorTestContext::new(cx); cx.set_state(indoc! {" ˇabˇc ˇ🏀ˇ🏀ˇefg @@ -1646,6 +1665,8 @@ async fn test_tab(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let language = Arc::new( Language::new( @@ -1704,7 +1725,10 @@ async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAp #[gpui::test] async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + let language = Arc::new( Language::new( LanguageConfig::default(), @@ -1713,14 +1737,9 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { .with_indents_query(r#"(_ "{" "}" @end) @indent"#) .unwrap(), ); + + let mut cx = EditorTestContext::new(cx); cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.tab_size = Some(4.try_into().unwrap()); - }); - }); - cx.set_state(indoc! {" fn a() { if b { @@ -1741,6 +1760,10 @@ async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4); + }); + let mut cx = EditorTestContext::new(cx); cx.set_state(indoc! {" @@ -1810,13 +1833,12 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.editor_overrides.hard_tabs = Some(true); - }); + init_test(cx, |settings| { + settings.defaults.hard_tabs = Some(true); }); + let mut cx = EditorTestContext::new(cx); + // select two ranges on one line cx.set_state(indoc! {" «oneˇ» «twoˇ» @@ -1907,25 +1929,25 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { - cx.update(|cx| { - cx.set_global( - Settings::test(cx) - .with_language_defaults( - "TOML", - EditorSettings { - tab_size: Some(2.try_into().unwrap()), - ..Default::default() - }, - ) - .with_language_defaults( - "Rust", - EditorSettings { - tab_size: Some(4.try_into().unwrap()), - ..Default::default() - }, - ), - ); + init_test(cx, |settings| { + settings.languages.extend([ + ( + "TOML".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(2), + ..Default::default() + }, + ), + ( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(4), + ..Default::default() + }, + ), + ]); }); + let toml_language = Arc::new(Language::new( LanguageConfig { name: "TOML".into(), @@ -2020,6 +2042,8 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { #[gpui::test] async fn test_backspace(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); // Basic backspace @@ -2067,8 +2091,9 @@ async fn test_backspace(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); cx.set_state(indoc! {" onˇe two three fou«rˇ» five six @@ -2095,7 +2120,8 @@ async fn test_delete(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_delete_line(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) @@ -2119,7 +2145,6 @@ fn test_delete_line(cx: &mut TestAppContext) { ); }); - cx.update(|cx| cx.set_global(Settings::test(cx))); let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) @@ -2139,7 +2164,8 @@ fn test_delete_line(cx: &mut TestAppContext) { #[gpui::test] fn test_duplicate_line(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) @@ -2191,7 +2217,8 @@ fn test_duplicate_line(cx: &mut TestAppContext) { #[gpui::test] fn test_move_line_up_down(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); build_editor(buffer, cx) @@ -2289,7 +2316,8 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { #[gpui::test] fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); build_editor(buffer, cx) @@ -2315,7 +2343,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { #[gpui::test] fn test_transpose(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); _ = cx .add_window(|cx| { @@ -2417,6 +2445,8 @@ fn test_transpose(cx: &mut TestAppContext) { #[gpui::test] async fn test_clipboard(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); @@ -2497,6 +2527,8 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let language = Arc::new(Language::new( LanguageConfig::default(), @@ -2609,7 +2641,8 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_select_all(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); build_editor(buffer, cx) @@ -2625,7 +2658,8 @@ fn test_select_all(cx: &mut TestAppContext) { #[gpui::test] fn test_select_line(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); build_editor(buffer, cx) @@ -2671,7 +2705,8 @@ fn test_select_line(cx: &mut TestAppContext) { #[gpui::test] fn test_split_selection_into_lines(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); build_editor(buffer, cx) @@ -2741,7 +2776,8 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { #[gpui::test] fn test_add_selection_above_below(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, view) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); build_editor(buffer, cx) @@ -2935,6 +2971,8 @@ fn test_add_selection_above_below(cx: &mut TestAppContext) { #[gpui::test] async fn test_select_next(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); cx.set_state("abc\nˇabc abc\ndefabc\nabc"); @@ -2959,7 +2997,8 @@ async fn test_select_next(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new(Language::new( LanguageConfig::default(), Some(tree_sitter_rust::language()), @@ -3100,7 +3139,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new( Language::new( LanguageConfig { @@ -3160,6 +3200,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let language = Arc::new(Language::new( @@ -3329,6 +3371,8 @@ async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let html_language = Arc::new( @@ -3563,6 +3607,8 @@ async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let rust_language = Arc::new( @@ -3660,7 +3706,8 @@ async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new(Language::new( LanguageConfig { brackets: BracketPairConfig { @@ -3814,7 +3861,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new(Language::new( LanguageConfig { brackets: BracketPairConfig { @@ -3919,7 +3967,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); let (text, insertion_ranges) = marked_text_ranges( indoc! {" @@ -4027,7 +4075,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut language = Language::new( LanguageConfig { @@ -4111,16 +4159,14 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { assert!(!cx.read(|cx| editor.is_dirty(cx))); // Set rust language override and assert overriden tabsize is sent to language server - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.language_overrides.insert( - "Rust".into(), - EditorSettings { - tab_size: Some(8.try_into().unwrap()), - ..Default::default() - }, - ); - }) + update_test_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); }); let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); @@ -4141,7 +4187,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut language = Language::new( LanguageConfig { @@ -4227,16 +4273,14 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { assert!(!cx.read(|cx| editor.is_dirty(cx))); // Set rust language override and assert overriden tabsize is sent to language server - cx.update(|cx| { - cx.update_global::(|settings, _| { - settings.language_overrides.insert( - "Rust".into(), - EditorSettings { - tab_size: Some(8.try_into().unwrap()), - ..Default::default() - }, - ); - }) + update_test_settings(cx, |settings| { + settings.languages.insert( + "Rust".into(), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); }); let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx)); @@ -4257,7 +4301,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut language = Language::new( LanguageConfig { @@ -4342,7 +4386,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { @@ -4399,7 +4443,7 @@ async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx, |_| {}); let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { @@ -4514,6 +4558,8 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { completion_provider: Some(lsp::CompletionOptions { @@ -4681,7 +4727,8 @@ async fn test_completion(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new(Language::new( LanguageConfig { line_comment: Some("// ".into()), @@ -4764,8 +4811,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) { - let mut cx = EditorTestContext::new(cx); - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); let language = Arc::new(Language::new( LanguageConfig { @@ -4778,6 +4824,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) let registry = Arc::new(LanguageRegistry::test()); registry.add(language.clone()); + let mut cx = EditorTestContext::new(cx); cx.update_buffer(|buffer, cx| { buffer.set_language_registry(registry); buffer.set_language(Some(language), cx); @@ -4897,6 +4944,8 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) #[gpui::test] async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let html_language = Arc::new( @@ -5021,7 +5070,8 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -5067,7 +5117,8 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { #[gpui::test] fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let markers = vec![('[', ']').into(), ('(', ')').into()]; let (initial_text, mut excerpt_ranges) = marked_text_ranges_by( indoc! {" @@ -5140,7 +5191,8 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { #[gpui::test] fn test_refresh_selections(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { @@ -5224,7 +5276,8 @@ fn test_refresh_selections(cx: &mut TestAppContext) { #[gpui::test] fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { @@ -5282,7 +5335,8 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { #[gpui::test] async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let language = Arc::new( Language::new( LanguageConfig { @@ -5355,7 +5409,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { #[gpui::test] fn test_highlighted_ranges(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); build_editor(buffer.clone(), cx) @@ -5437,7 +5492,8 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { #[gpui::test] async fn test_following(cx: &mut gpui::TestAppContext) { - Settings::test_async(cx); + init_test(cx, |_| {}); + let fs = FakeFs::new(cx.background()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; @@ -5576,7 +5632,8 @@ async fn test_following(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { - Settings::test_async(cx); + init_test(cx, |_| {}); + let fs = FakeFs::new(cx.background()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); @@ -5805,6 +5862,8 @@ fn test_combine_syntax_and_fuzzy_match_highlights() { #[gpui::test] async fn go_to_hunk(deterministic: Arc, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx); let diff_base = r#" @@ -5924,6 +5983,8 @@ fn test_split_words() { #[gpui::test] async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await; let mut assert = |before, after| { let _state_context = cx.set_state(before); @@ -5972,6 +6033,8 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { #[gpui::test(iterations = 10)] async fn test_copilot(deterministic: Arc, cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); let mut cx = EditorLspTestContext::new_rust( @@ -6223,6 +6286,8 @@ async fn test_copilot_completion_invalidation( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { + init_test(cx, |_| {}); + let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); let mut cx = EditorLspTestContext::new_rust( @@ -6288,11 +6353,10 @@ async fn test_copilot_multibuffer( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { + init_test(cx, |_| {}); + let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| { - cx.set_global(Settings::test(cx)); - cx.set_global(copilot) - }); + cx.update(|cx| cx.set_global(copilot)); let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx)); let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx)); @@ -6392,14 +6456,16 @@ async fn test_copilot_disabled_globs( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { - let (copilot, copilot_lsp) = Copilot::fake(cx); - cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.copilot.disabled_globs = vec![glob::Pattern::new(".env*").unwrap()]; - cx.set_global(settings); - cx.set_global(copilot) + init_test(cx, |settings| { + settings + .copilot + .get_or_insert(Default::default()) + .disabled_globs = Some(vec![".env*".to_string()]); }); + let (copilot, copilot_lsp) = Copilot::fake(cx); + cx.update(|cx| cx.set_global(copilot)); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/test", @@ -6596,3 +6662,27 @@ fn handle_copilot_completion_request( } }); } + +pub(crate) fn update_test_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut AllLanguageSettingsContent), +) { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + }); +} + +pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { + cx.foreground().forbid_parking(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + cx.set_global(Settings::test(cx)); + language::init(cx); + crate::init(cx); + }); + + update_test_settings(cx, f); +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c1710b7337..8194d51b25 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -35,9 +35,12 @@ use gpui::{ }; use itertools::Itertools; use json::json; -use language::{Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, Selection}; +use language::{ + language_settings::ShowWhitespaceSetting, Bias, CursorShape, DiagnosticSeverity, OffsetUtf16, + Selection, +}; use project::ProjectPath; -use settings::{GitGutter, Settings, ShowWhitespaces}; +use settings::{GitGutter, Settings}; use smallvec::SmallVec; use std::{ borrow::Cow, @@ -708,6 +711,7 @@ impl EditorElement { let scroll_left = scroll_position.x() * max_glyph_width; let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.); let line_end_overshoot = 0.15 * layout.position_map.line_height; + let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; scene.push_layer(Some(bounds)); @@ -882,9 +886,10 @@ impl EditorElement { content_origin, scroll_left, visible_text_bounds, - cx, + whitespace_setting, &invisible_display_ranges, visible_bounds, + cx, ) } } @@ -1738,9 +1743,10 @@ impl LineWithInvisibles { content_origin: Vector2F, scroll_left: f32, visible_text_bounds: RectF, - cx: &mut ViewContext, + whitespace_setting: ShowWhitespaceSetting, selection_ranges: &[Range], visible_bounds: RectF, + cx: &mut ViewContext, ) { let line_height = layout.position_map.line_height; let line_y = row as f32 * line_height - scroll_top; @@ -1754,7 +1760,6 @@ impl LineWithInvisibles { ); self.draw_invisibles( - cx, &selection_ranges, layout, content_origin, @@ -1764,12 +1769,13 @@ impl LineWithInvisibles { scene, visible_bounds, line_height, + whitespace_setting, + cx, ); } fn draw_invisibles( &self, - cx: &mut ViewContext, selection_ranges: &[Range], layout: &LayoutState, content_origin: Vector2F, @@ -1779,17 +1785,13 @@ impl LineWithInvisibles { scene: &mut SceneBuilder, visible_bounds: RectF, line_height: f32, + whitespace_setting: ShowWhitespaceSetting, + cx: &mut ViewContext, ) { - let settings = cx.global::(); - let allowed_invisibles_regions = match settings - .editor_overrides - .show_whitespaces - .or(settings.editor_defaults.show_whitespaces) - .unwrap_or_default() - { - ShowWhitespaces::None => return, - ShowWhitespaces::Selection => Some(selection_ranges), - ShowWhitespaces::All => None, + let allowed_invisibles_regions = match whitespace_setting { + ShowWhitespaceSetting::None => return, + ShowWhitespaceSetting::Selection => Some(selection_ranges), + ShowWhitespaceSetting::All => None, }; for invisible in &self.invisibles { @@ -2773,17 +2775,19 @@ mod tests { use super::*; use crate::{ display_map::{BlockDisposition, BlockProperties}, + editor_tests::{init_test, update_test_settings}, Editor, MultiBuffer, }; use gpui::TestAppContext; + use language::language_settings; use log::info; - use settings::Settings; use std::{num::NonZeroU32, sync::Arc}; use util::test::sample_text; #[gpui::test] fn test_layout_line_numbers(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); Editor::new(EditorMode::Full, buffer, None, None, cx) @@ -2801,7 +2805,8 @@ mod tests { #[gpui::test] fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx, |_| {}); + let (_, editor) = cx.add_window(|cx| { let buffer = MultiBuffer::build_simple("", cx); Editor::new(EditorMode::Full, buffer, None, None, cx) @@ -2861,26 +2866,27 @@ mod tests { #[gpui::test] fn test_all_invisibles_drawing(cx: &mut TestAppContext) { - let tab_size = 4; + const TAB_SIZE: u32 = 4; + let input_text = "\t \t|\t| a b"; let expected_invisibles = vec![ Invisible::Tab { line_start_offset: 0, }, Invisible::Whitespace { - line_offset: tab_size as usize, + line_offset: TAB_SIZE as usize, }, Invisible::Tab { - line_start_offset: tab_size as usize + 1, + line_start_offset: TAB_SIZE as usize + 1, }, Invisible::Tab { - line_start_offset: tab_size as usize * 2 + 1, + line_start_offset: TAB_SIZE as usize * 2 + 1, }, Invisible::Whitespace { - line_offset: tab_size as usize * 3 + 1, + line_offset: TAB_SIZE as usize * 3 + 1, }, Invisible::Whitespace { - line_offset: tab_size as usize * 3 + 3, + line_offset: TAB_SIZE as usize * 3 + 3, }, ]; assert_eq!( @@ -2892,12 +2898,11 @@ mod tests { "Hardcoded expected invisibles differ from the actual ones in '{input_text}'" ); - cx.update(|cx| { - let mut test_settings = Settings::test(cx); - test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); - test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap()); - cx.set_global(test_settings); + init_test(cx, |s| { + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.tab_size = NonZeroU32::new(TAB_SIZE); }); + let actual_invisibles = collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0); @@ -2906,11 +2911,9 @@ mod tests { #[gpui::test] fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) { - cx.update(|cx| { - let mut test_settings = Settings::test(cx); - test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); - test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(4).unwrap()); - cx.set_global(test_settings); + init_test(cx, |s| { + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.tab_size = NonZeroU32::new(4); }); for editor_mode_without_invisibles in [ @@ -2961,19 +2964,18 @@ mod tests { ); info!("Expected invisibles: {expected_invisibles:?}"); + init_test(cx, |_| {}); + // Put the same string with repeating whitespace pattern into editors of various size, // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point. let resize_step = 10.0; let mut editor_width = 200.0; while editor_width <= 1000.0 { - cx.update(|cx| { - let mut test_settings = Settings::test(cx); - test_settings.editor_defaults.tab_size = Some(NonZeroU32::new(tab_size).unwrap()); - test_settings.editor_defaults.show_whitespaces = Some(ShowWhitespaces::All); - test_settings.editor_defaults.preferred_line_length = Some(editor_width as u32); - test_settings.editor_defaults.soft_wrap = - Some(settings::SoftWrap::PreferredLineLength); - cx.set_global(test_settings); + update_test_settings(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(tab_size); + s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); + s.defaults.preferred_line_length = Some(editor_width as u32); + s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength); }); let actual_invisibles = @@ -3021,7 +3023,7 @@ mod tests { let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let (_, layout_state) = editor.update(cx, |editor, cx| { - editor.set_soft_wrap_mode(settings::SoftWrap::EditorWidth, cx); + editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); editor.set_wrap_width(Some(editor_width), cx); let mut new_parents = Default::default(); diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index ce3864f56a..a0baf6882f 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -33,12 +33,14 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon #[cfg(test)] mod tests { use super::*; - use crate::test::editor_lsp_test_context::EditorLspTestContext; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; use indoc::indoc; use language::{BracketPair, BracketPairConfig, Language, LanguageConfig}; #[gpui::test] async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new( Language::new( LanguageConfig { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 438c662ed1..c40cb0fbe3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -694,7 +694,7 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { use super::*; - use crate::test::editor_lsp_test_context::EditorLspTestContext; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; use gpui::fonts::Weight; use indoc::indoc; use language::{Diagnostic, DiagnosticSet}; @@ -706,6 +706,8 @@ mod tests { #[gpui::test] async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -773,6 +775,8 @@ mod tests { #[gpui::test] async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -816,6 +820,8 @@ mod tests { #[gpui::test] async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -882,7 +888,8 @@ mod tests { #[gpui::test] fn test_render_blocks(cx: &mut gpui::TestAppContext) { - Settings::test_async(cx); + init_test(cx, |_| {}); + cx.add_window(|cx| { let editor = Editor::single_line(None, cx); let style = editor.style(cx); diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index b2105c1c81..04cf4611f1 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,10 +1,9 @@ -use std::ops::Range; - use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; use settings::Settings; +use std::ops::Range; use util::TryFutureExt; #[derive(Debug, Default)] @@ -297,6 +296,8 @@ fn go_to_fetched_definition_of_kind( #[cfg(test)] mod tests { + use super::*; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; use futures::StreamExt; use gpui::{ platform::{self, Modifiers, ModifiersChangedEvent}, @@ -305,12 +306,10 @@ mod tests { use indoc::indoc; use lsp::request::{GotoDefinition, GotoTypeDefinition}; - use crate::test::editor_lsp_test_context::EditorLspTestContext; - - use super::*; - #[gpui::test] async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), @@ -417,6 +416,8 @@ mod tests { #[gpui::test] async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index b892fffbca..8dfdcdff53 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -57,13 +57,14 @@ pub fn deploy_context_menu( #[cfg(test)] mod tests { - use crate::test::editor_lsp_test_context::EditorLspTestContext; - use super::*; + use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; use indoc::indoc; #[gpui::test] async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorLspTestContext::new_rust( lsp::ServerCapabilities { hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 284f0c94bc..311616cfe0 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -367,13 +367,15 @@ pub fn split_display_range_by_lines( #[cfg(test)] mod tests { + use settings::{Settings, SettingsStore}; + use super::*; use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer}; - use settings::Settings; #[gpui::test] fn test_previous_word_start(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -400,7 +402,8 @@ mod tests { #[gpui::test] fn test_previous_subword_start(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -434,7 +437,8 @@ mod tests { #[gpui::test] fn test_find_preceding_boundary(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert( marked_text: &str, cx: &mut gpui::AppContext, @@ -466,7 +470,8 @@ mod tests { #[gpui::test] fn test_next_word_end(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -490,7 +495,8 @@ mod tests { #[gpui::test] fn test_next_subword_end(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -523,7 +529,8 @@ mod tests { #[gpui::test] fn test_find_boundary(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert( marked_text: &str, cx: &mut gpui::AppContext, @@ -555,7 +562,8 @@ mod tests { #[gpui::test] fn test_surrounding_word(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + fn assert(marked_text: &str, cx: &mut gpui::AppContext) { let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( @@ -576,7 +584,8 @@ mod tests { #[gpui::test] fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_test(cx); + let family_id = cx .font_cache() .load_family(&["Helvetica"], &Default::default()) @@ -691,4 +700,11 @@ mod tests { (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); } + + fn init_test(cx: &mut gpui::AppContext) { + cx.set_global(SettingsStore::test(cx)); + cx.set_global(Settings::test(cx)); + language::init(cx); + crate::init(cx); + } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index a2160b47e5..1e07bba065 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -9,7 +9,9 @@ use git::diff::DiffHunk; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, + char_kind, + language_settings::{language_settings, LanguageSettings}, + AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape, DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped, @@ -1372,6 +1374,15 @@ impl MultiBuffer { .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset)) } + pub fn settings_at<'a, T: ToOffset>( + &self, + point: T, + cx: &'a AppContext, + ) -> &'a LanguageSettings { + let language = self.language_at(point, cx); + language_settings(None, language.map(|l| l.name()).as_deref(), cx) + } + pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle)) { self.buffers .borrow() @@ -2764,6 +2775,16 @@ impl MultiBufferSnapshot { .and_then(|(buffer, offset)| buffer.language_at(offset)) } + pub fn settings_at<'a, T: ToOffset>( + &'a self, + point: T, + cx: &'a AppContext, + ) -> &'a LanguageSettings { + self.point_to_buffer_offset(point) + .map(|(buffer, offset)| buffer.settings_at(offset, cx)) + .unwrap_or_else(|| language_settings(None, None, cx)) + } + pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option { self.point_to_buffer_offset(point) .and_then(|(buffer, offset)| buffer.language_scope_at(offset)) diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index e268e2a0ce..e9aaa3df1b 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -37,6 +37,7 @@ impl<'a> EditorLspTestContext<'a> { let app_state = cx.update(AppState::test); cx.update(|cx| { + language::init(cx); crate::init(cx); pane::init(cx); }); diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 269a2f4f3c..ced99a3f23 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -1,19 +1,16 @@ +use crate::{ + display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, +}; +use futures::Future; +use gpui::{ + keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle, +}; +use indoc::indoc; +use language::{Buffer, BufferSnapshot}; use std::{ any::TypeId, ops::{Deref, DerefMut, Range}, }; - -use futures::Future; -use indoc::indoc; - -use crate::{ - display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, -}; -use gpui::{ - keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle, -}; -use language::{Buffer, BufferSnapshot}; -use settings::Settings; use util::{ assert_set_eq, test::{generate_marked_text, marked_text_ranges}, @@ -30,15 +27,10 @@ pub struct EditorTestContext<'a> { impl<'a> EditorTestContext<'a> { pub fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> { let (window_id, editor) = cx.update(|cx| { - cx.set_global(Settings::test(cx)); - crate::init(cx); - - let (window_id, editor) = cx.add_window(Default::default(), |cx| { + cx.add_window(Default::default(), |cx| { cx.focus_self(); build_editor(MultiBuffer::build_simple("", cx), cx) - }); - - (window_id, editor) + }) }); Self { diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 30a4650ad7..024054c005 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -23,7 +23,9 @@ postage.workspace = true [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } -serde_json.workspace = true +language = { path = "../language", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } + +serde_json.workspace = true ctor.workspace = true env_logger.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index f00430feb7..7d5aff79d0 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -270,6 +270,7 @@ impl PickerDelegate for FileFinderDelegate { mod tests { use super::*; use editor::Editor; + use gpui::TestAppContext; use menu::{Confirm, SelectNext}; use serde_json::json; use workspace::{AppState, Workspace}; @@ -283,12 +284,7 @@ mod tests { #[gpui::test] async fn test_matching_paths(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(|cx| { - super::init(cx); - editor::init(cx); - AppState::test(cx) - }); - + let app_state = init_test(cx); app_state .fs .as_fake() @@ -339,7 +335,7 @@ mod tests { #[gpui::test] async fn test_matching_cancellation(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(AppState::test); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -408,7 +404,7 @@ mod tests { #[gpui::test] async fn test_ignored_files(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(AppState::test); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -462,7 +458,7 @@ mod tests { #[gpui::test] async fn test_single_file_worktrees(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(AppState::test); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -516,9 +512,7 @@ mod tests { #[gpui::test] async fn test_multiple_matches_with_same_relative_path(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let app_state = cx.update(AppState::test); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -570,9 +564,7 @@ mod tests { #[gpui::test] async fn test_path_distance_ordering(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - - let app_state = cx.update(AppState::test); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -622,7 +614,7 @@ mod tests { #[gpui::test] async fn test_search_worktree_without_files(cx: &mut gpui::TestAppContext) { - let app_state = cx.update(AppState::test); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -658,4 +650,15 @@ mod tests { assert_eq!(finder.delegate().matches.len(), 0); }); } + + fn init_test(cx: &mut TestAppContext) -> Arc { + cx.foreground().forbid_parking(); + cx.update(|cx| { + let state = AppState::test(cx); + language::init(cx); + super::init(cx); + editor::init(cx); + state + }) + } } diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 7baffab2d9..028656a027 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -477,6 +477,14 @@ impl Deterministic { state.rng = StdRng::seed_from_u64(state.seed); } + pub fn allow_parking(&self) { + use rand::prelude::*; + + let mut state = self.state.lock(); + state.forbid_parking = false; + state.rng = StdRng::seed_from_u64(state.seed); + } + pub async fn simulate_random_delay(&self) { use rand::prelude::*; use smol::future::yield_now; @@ -698,6 +706,14 @@ impl Foreground { } } + #[cfg(any(test, feature = "test-support"))] + pub fn allow_parking(&self) { + match self { + Self::Deterministic { executor, .. } => executor.allow_parking(), + _ => panic!("this method can only be called on a deterministic executor"), + } + } + #[cfg(any(test, feature = "test-support"))] pub fn advance_clock(&self, duration: Duration) { match self { diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 938cd82922..34af4ed0e4 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use chrono::{Datelike, Local, NaiveTime, Timelike}; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{actions, AppContext}; @@ -40,21 +41,8 @@ impl settings::Setting for JournalSettings { type FileContent = Self; - fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Self { - Self { - path: Some( - user_values - .first() - .and_then(|s| s.path.clone()) - .unwrap_or(default_value.path.clone().unwrap()), - ), - hour_format: Some( - user_values - .first() - .and_then(|s| s.hour_format.clone()) - .unwrap_or(default_value.hour_format.clone().unwrap()), - ), - } + fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result { + Self::load_via_json_merge(default_value, user_values) } } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 6e03a07bc3..5a7644d98e 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -36,16 +36,19 @@ sum_tree = { path = "../sum_tree" } text = { path = "../text" } theme = { path = "../theme" } util = { path = "../util" } + anyhow.workspace = true async-broadcast = "0.4" async-trait.workspace = true futures.workspace = true +glob.workspace = true lazy_static.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true rand = { workspace = true, optional = true } regex.workspace = true +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 43766192eb..fd1430b1e4 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -5,6 +5,7 @@ pub use crate::{ }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, + language_settings::{language_settings, LanguageSettings}, outline::OutlineItem, syntax_map::{ SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint, @@ -18,7 +19,6 @@ use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; use lsp::LanguageServerId; use parking_lot::Mutex; -use settings::Settings; use similar::{ChangeTag, TextDiff}; use smallvec::SmallVec; use smol::future::yield_now; @@ -1827,11 +1827,11 @@ impl BufferSnapshot { pub fn language_indent_size_at(&self, position: T, cx: &AppContext) -> IndentSize { let language_name = self.language_at(position).map(|language| language.name()); - let settings = cx.global::(); - if settings.hard_tabs(language_name.as_deref()) { + let settings = language_settings(None, language_name.as_deref(), cx); + if settings.hard_tabs { IndentSize::tab() } else { - IndentSize::spaces(settings.tab_size(language_name.as_deref()).get()) + IndentSize::spaces(settings.tab_size.get()) } } @@ -2146,6 +2146,15 @@ impl BufferSnapshot { .or(self.language.as_ref()) } + pub fn settings_at<'a, D: ToOffset>( + &self, + position: D, + cx: &'a AppContext, + ) -> &'a LanguageSettings { + let language = self.language_at(position); + language_settings(None, language.map(|l| l.name()).as_deref(), cx) + } + pub fn language_scope_at(&self, position: D) -> Option { let offset = position.to_offset(self); diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index eeac1a4818..be573aa895 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1,3 +1,7 @@ +use crate::language_settings::{ + AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent, +}; + use super::*; use clock::ReplicaId; use collections::BTreeMap; @@ -7,7 +11,7 @@ use indoc::indoc; use proto::deserialize_operation; use rand::prelude::*; use regex::RegexBuilder; -use settings::Settings; +use settings::SettingsStore; use std::{ cell::RefCell, env, @@ -36,7 +40,8 @@ fn init_logger() { #[gpui::test] fn test_line_endings(cx: &mut gpui::AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let mut buffer = Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); @@ -862,8 +867,7 @@ fn test_range_for_syntax_ancestor(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { - let settings = Settings::test(cx); - cx.set_global(settings); + init_settings(cx, |_| {}); cx.add_model(|cx| { let text = "fn a() {}"; @@ -903,9 +907,9 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { - let mut settings = Settings::test(cx); - settings.editor_overrides.hard_tabs = Some(true); - cx.set_global(settings); + init_settings(cx, |settings| { + settings.defaults.hard_tabs = Some(true); + }); cx.add_model(|cx| { let text = "fn a() {}"; @@ -945,8 +949,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppContext) { - let settings = Settings::test(cx); - cx.set_global(settings); + init_settings(cx, |_| {}); cx.add_model(|cx| { let mut buffer = Buffer::new( @@ -1082,8 +1085,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC #[gpui::test] fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut AppContext) { - let settings = Settings::test(cx); - cx.set_global(settings); + init_settings(cx, |_| {}); cx.add_model(|cx| { let mut buffer = Buffer::new( @@ -1145,7 +1147,8 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap #[gpui::test] fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let mut buffer = Buffer::new( 0, @@ -1201,7 +1204,8 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = "a\nb"; let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); @@ -1217,7 +1221,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = " const a: usize = 1; @@ -1257,7 +1262,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_block_mode(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = r#" fn a() { @@ -1339,7 +1345,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = r#" fn a() { @@ -1417,7 +1424,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex #[gpui::test] fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let text = " * one @@ -1460,25 +1468,23 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_with_injected_languages(cx: &mut AppContext) { - cx.set_global({ - let mut settings = Settings::test(cx); - settings.language_overrides.extend([ + init_settings(cx, |settings| { + settings.languages.extend([ ( "HTML".into(), - settings::EditorSettings { + LanguageSettingsContent { tab_size: Some(2.try_into().unwrap()), ..Default::default() }, ), ( "JavaScript".into(), - settings::EditorSettings { + LanguageSettingsContent { tab_size: Some(8.try_into().unwrap()), ..Default::default() }, ), - ]); - settings + ]) }); let html_language = Arc::new( @@ -1574,9 +1580,10 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) { #[gpui::test] fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { - let mut settings = Settings::test(cx); - settings.editor_defaults.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + init_settings(cx, |settings| { + settings.defaults.tab_size = Some(2.try_into().unwrap()); + }); + cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx); @@ -1617,7 +1624,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { #[gpui::test] fn test_language_config_at(cx: &mut AppContext) { - cx.set_global(Settings::test(cx)); + init_settings(cx, |_| {}); + cx.add_model(|cx| { let language = Language::new( LanguageConfig { @@ -2199,7 +2207,6 @@ fn assert_bracket_pairs( language: Language, cx: &mut AppContext, ) { - cx.set_global(Settings::test(cx)); let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false); let buffer = cx.add_model(|cx| { Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx) @@ -2222,3 +2229,11 @@ fn assert_bracket_pairs( bracket_pairs ); } + +fn init_settings(cx: &mut AppContext, f: fn(&mut AllLanguageSettingsContent)) { + cx.set_global(SettingsStore::test(cx)); + crate::init(cx); + cx.update_global::(|settings, cx| { + settings.update_user_settings::(cx, f); + }); +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 85c9089952..87e4880b99 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1,6 +1,7 @@ mod buffer; mod diagnostic_set; mod highlight_map; +pub mod language_settings; mod outline; pub mod proto; mod syntax_map; @@ -58,6 +59,10 @@ pub use lsp::LanguageServerId; pub use outline::{Outline, OutlineItem}; pub use tree_sitter::{Parser, Tree}; +pub fn init(cx: &mut AppContext) { + language_settings::init(cx); +} + thread_local! { static PARSER: RefCell = RefCell::new(Parser::new()); } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs new file mode 100644 index 0000000000..80cc81a4c3 --- /dev/null +++ b/crates/language/src/language_settings.rs @@ -0,0 +1,285 @@ +use anyhow::Result; +use collections::HashMap; +use gpui::AppContext; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{num::NonZeroU32, path::Path, sync::Arc}; + +pub fn init(cx: &mut AppContext) { + settings::register_setting::(cx); +} + +pub fn language_settings<'a>( + path: Option<&Path>, + language: Option<&str>, + cx: &'a AppContext, +) -> &'a LanguageSettings { + settings::get_setting::(path, cx).language(language) +} + +pub fn all_language_settings<'a>( + path: Option<&Path>, + cx: &'a AppContext, +) -> &'a AllLanguageSettings { + settings::get_setting::(path, cx) +} + +#[derive(Debug, Clone)] +pub struct AllLanguageSettings { + pub copilot: CopilotSettings, + defaults: LanguageSettings, + languages: HashMap, LanguageSettings>, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LanguageSettings { + pub tab_size: NonZeroU32, + pub hard_tabs: bool, + pub soft_wrap: SoftWrap, + pub preferred_line_length: u32, + pub format_on_save: FormatOnSave, + pub remove_trailing_whitespace_on_save: bool, + pub ensure_final_newline_on_save: bool, + pub formatter: Formatter, + pub enable_language_server: bool, + pub show_copilot_suggestions: bool, + pub show_whitespaces: ShowWhitespaceSetting, +} + +#[derive(Clone, Debug, Default)] +pub struct CopilotSettings { + pub feature_enabled: bool, + pub disabled_globs: Vec, +} + +#[derive(Clone, Serialize, Deserialize, JsonSchema)] +pub struct AllLanguageSettingsContent { + #[serde(default)] + pub features: Option, + #[serde(default)] + pub copilot: Option, + #[serde(flatten)] + pub defaults: LanguageSettingsContent, + #[serde(default, alias = "language_overrides")] + pub languages: HashMap, LanguageSettingsContent>, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +pub struct LanguageSettingsContent { + #[serde(default)] + pub tab_size: Option, + #[serde(default)] + pub hard_tabs: Option, + #[serde(default)] + pub soft_wrap: Option, + #[serde(default)] + pub preferred_line_length: Option, + #[serde(default)] + pub format_on_save: Option, + #[serde(default)] + pub remove_trailing_whitespace_on_save: Option, + #[serde(default)] + pub ensure_final_newline_on_save: Option, + #[serde(default)] + pub formatter: Option, + #[serde(default)] + pub enable_language_server: Option, + #[serde(default)] + pub show_copilot_suggestions: Option, + #[serde(default)] + pub show_whitespaces: Option, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct CopilotSettingsContent { + #[serde(default)] + pub disabled_globs: Option>, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct FeaturesContent { + pub copilot: Option, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum SoftWrap { + None, + EditorWidth, + PreferredLineLength, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum FormatOnSave { + On, + Off, + LanguageServer, + External { + command: Arc, + arguments: Arc<[String]>, + }, +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ShowWhitespaceSetting { + Selection, + None, + All, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Formatter { + LanguageServer, + External { + command: Arc, + arguments: Arc<[String]>, + }, +} + +impl AllLanguageSettings { + pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings { + if let Some(name) = language_name { + if let Some(overrides) = self.languages.get(name) { + return overrides; + } + } + &self.defaults + } + + pub fn copilot_enabled_for_path(&self, path: &Path) -> bool { + !self + .copilot + .disabled_globs + .iter() + .any(|glob| glob.matches_path(path)) + } + + pub fn copilot_enabled(&self, language_name: Option<&str>, path: Option<&Path>) -> bool { + if !self.copilot.feature_enabled { + return false; + } + + if let Some(path) = path { + if !self.copilot_enabled_for_path(path) { + return false; + } + } + + self.language(language_name).show_copilot_suggestions + } +} + +impl settings::Setting for AllLanguageSettings { + const KEY: Option<&'static str> = None; + + type FileContent = AllLanguageSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_settings: &[&Self::FileContent], + _: &AppContext, + ) -> Result { + // A default is provided for all settings. + let mut defaults: LanguageSettings = + serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?; + + let mut languages = HashMap::default(); + for (language_name, settings) in &default_value.languages { + let mut language_settings = defaults.clone(); + merge_settings(&mut language_settings, &settings); + languages.insert(language_name.clone(), language_settings); + } + + let mut copilot_enabled = default_value + .features + .as_ref() + .and_then(|f| f.copilot) + .ok_or_else(Self::missing_default)?; + let mut copilot_globs = default_value + .copilot + .as_ref() + .and_then(|c| c.disabled_globs.as_ref()) + .ok_or_else(Self::missing_default)?; + + for user_settings in user_settings { + if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) { + copilot_enabled = copilot; + } + if let Some(globs) = user_settings + .copilot + .as_ref() + .and_then(|f| f.disabled_globs.as_ref()) + { + copilot_globs = globs; + } + + // A user's global settings override the default global settings and + // all default language-specific settings. + merge_settings(&mut defaults, &user_settings.defaults); + for language_settings in languages.values_mut() { + merge_settings(language_settings, &user_settings.defaults); + } + + // A user's language-specific settings override default language-specific settings. + for (language_name, user_language_settings) in &user_settings.languages { + merge_settings( + languages + .entry(language_name.clone()) + .or_insert_with(|| defaults.clone()), + &user_language_settings, + ); + } + } + + Ok(Self { + copilot: CopilotSettings { + feature_enabled: copilot_enabled, + disabled_globs: copilot_globs + .iter() + .filter_map(|pattern| glob::Pattern::new(pattern).ok()) + .collect(), + }, + defaults, + languages, + }) + } +} + +fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent) { + merge(&mut settings.tab_size, src.tab_size); + merge(&mut settings.hard_tabs, src.hard_tabs); + merge(&mut settings.soft_wrap, src.soft_wrap); + merge( + &mut settings.preferred_line_length, + src.preferred_line_length, + ); + merge(&mut settings.formatter, src.formatter.clone()); + merge(&mut settings.format_on_save, src.format_on_save.clone()); + merge( + &mut settings.remove_trailing_whitespace_on_save, + src.remove_trailing_whitespace_on_save, + ); + merge( + &mut settings.ensure_final_newline_on_save, + src.ensure_final_newline_on_save, + ); + merge( + &mut settings.enable_language_server, + src.enable_language_server, + ); + merge( + &mut settings.show_copilot_suggestions, + src.show_copilot_suggestions, + ); + merge(&mut settings.show_whitespaces, src.show_whitespaces); + + fn merge(target: &mut T, value: Option) { + if let Some(value) = value { + *target = value; + } + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 94872708c4..5d0b0a8884 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -23,6 +23,7 @@ use gpui::{ ModelHandle, Task, WeakModelHandle, }; use language::{ + language_settings::{all_language_settings, language_settings, FormatOnSave, Formatter}, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, @@ -44,7 +45,7 @@ use postage::watch; use rand::prelude::*; use search::SearchQuery; use serde::Serialize; -use settings::{FormatOnSave, Formatter, Settings}; +use settings::{Settings, SettingsStore}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use std::{ @@ -64,9 +65,7 @@ use std::{ }, time::{Duration, Instant, SystemTime}, }; - use terminals::Terminals; - use util::{debug_panic, defer, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _}; pub use fs::*; @@ -454,7 +453,9 @@ impl Project { client_state: None, opened_buffer: watch::channel(), client_subscriptions: Vec::new(), - _subscriptions: vec![cx.observe_global::(Self::on_settings_changed)], + _subscriptions: vec![ + cx.observe_global::(Self::on_settings_changed) + ], _maintain_buffer_languages: Self::maintain_buffer_languages(&languages, cx), _maintain_workspace_config: Self::maintain_workspace_config(languages.clone(), cx), active_entry: None, @@ -622,7 +623,7 @@ impl Project { } fn on_settings_changed(&mut self, cx: &mut ModelContext) { - let settings = cx.global::(); + let settings = all_language_settings(None, cx); let mut language_servers_to_start = Vec::new(); for buffer in self.opened_buffers.values() { @@ -630,7 +631,10 @@ impl Project { let buffer = buffer.read(cx); if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { - if settings.enable_language_server(Some(&language.name())) { + if settings + .language(Some(&language.name())) + .enable_language_server + { let worktree = file.worktree.read(cx); language_servers_to_start.push(( worktree.id(), @@ -645,7 +649,10 @@ impl Project { let mut language_servers_to_stop = Vec::new(); for language in self.languages.to_vec() { for lsp_adapter in language.lsp_adapters() { - if !settings.enable_language_server(Some(&language.name())) { + if !settings + .language(Some(&language.name())) + .enable_language_server + { let lsp_name = &lsp_adapter.name; for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { if lsp_name == started_lsp_name { @@ -2178,10 +2185,7 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - if !cx - .global::() - .enable_language_server(Some(&language.name())) - { + if !language_settings(None, Some(&language.name()), cx).enable_language_server { return; } @@ -3228,24 +3232,18 @@ impl Project { let mut project_transaction = ProjectTransaction::default(); for (buffer, buffer_abs_path, language_server) in &buffers_with_paths_and_servers { - let ( - format_on_save, - remove_trailing_whitespace, - ensure_final_newline, - formatter, - tab_size, - ) = buffer.read_with(&cx, |buffer, cx| { - let settings = cx.global::(); + let settings = buffer.read_with(&cx, |buffer, cx| { let language_name = buffer.language().map(|language| language.name()); - ( - settings.format_on_save(language_name.as_deref()), - settings.remove_trailing_whitespace_on_save(language_name.as_deref()), - settings.ensure_final_newline_on_save(language_name.as_deref()), - settings.formatter(language_name.as_deref()), - settings.tab_size(language_name.as_deref()), - ) + language_settings(buffer_abs_path.as_deref(), language_name.as_deref(), cx) + .clone() }); + let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; + let ensure_final_newline = settings.ensure_final_newline_on_save; + let format_on_save = settings.format_on_save.clone(); + let formatter = settings.formatter.clone(); + let tab_size = settings.tab_size; + // First, format buffer's whitespace according to the settings. let trailing_whitespace_diff = if remove_trailing_whitespace { Some( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 894b27f2ee..1cbef629f3 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,10 +1,9 @@ use crate::{worktree::WorktreeHandle, Event, *}; -use fs::LineEnding; -use fs::{FakeFs, RealFs}; +use fs::{FakeFs, LineEnding, RealFs}; use futures::{future, StreamExt}; -use gpui::AppContext; -use gpui::{executor::Deterministic, test::subscribe}; +use gpui::{executor::Deterministic, test::subscribe, AppContext}; use language::{ + language_settings::{AllLanguageSettings, LanguageSettingsContent}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig, OffsetRangeExt, Point, ToPoint, }; @@ -26,6 +25,9 @@ fn init_logger() { #[gpui::test] async fn test_symlinks(cx: &mut gpui::TestAppContext) { + init_test(cx); + cx.foreground().allow_parking(); + let dir = temp_tree(json!({ "root": { "apple": "", @@ -65,7 +67,7 @@ async fn test_managing_language_servers( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { - cx.foreground().forbid_parking(); + init_test(cx); let mut rust_language = Language::new( LanguageConfig { @@ -451,7 +453,7 @@ async fn test_managing_language_servers( #[gpui::test] async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -556,7 +558,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon #[gpui::test] async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -648,7 +650,7 @@ async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -719,7 +721,7 @@ async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let progress_token = "the-progress-token"; let mut language = Language::new( @@ -847,7 +849,7 @@ async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let progress_token = "the-progress-token"; let mut language = Language::new( @@ -925,7 +927,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC #[gpui::test] async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -973,11 +975,8 @@ async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::T } #[gpui::test] -async fn test_toggling_enable_language_server( - deterministic: Arc, - cx: &mut gpui::TestAppContext, -) { - deterministic.forbid_parking(); +async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { + init_test(cx); let mut rust = Language::new( LanguageConfig { @@ -1051,14 +1050,16 @@ async fn test_toggling_enable_language_server( // Disable Rust language server, ensuring only that server gets stopped. cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.language_overrides.insert( - Arc::from("Rust"), - settings::EditorSettings { - enable_language_server: Some(false), - ..Default::default() - }, - ); + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.languages.insert( + Arc::from("Rust"), + LanguageSettingsContent { + enable_language_server: Some(false), + ..Default::default() + }, + ); + }); }) }); fake_rust_server_1 @@ -1068,21 +1069,23 @@ async fn test_toggling_enable_language_server( // Enable Rust and disable JavaScript language servers, ensuring that the // former gets started again and that the latter stops. cx.update(|cx| { - cx.update_global(|settings: &mut Settings, _| { - settings.language_overrides.insert( - Arc::from("Rust"), - settings::EditorSettings { - enable_language_server: Some(true), - ..Default::default() - }, - ); - settings.language_overrides.insert( - Arc::from("JavaScript"), - settings::EditorSettings { - enable_language_server: Some(false), - ..Default::default() - }, - ); + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.languages.insert( + Arc::from("Rust"), + LanguageSettingsContent { + enable_language_server: Some(true), + ..Default::default() + }, + ); + settings.languages.insert( + Arc::from("JavaScript"), + LanguageSettingsContent { + enable_language_server: Some(false), + ..Default::default() + }, + ); + }); }) }); let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap(); @@ -1102,7 +1105,7 @@ async fn test_toggling_enable_language_server( #[gpui::test] async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -1388,7 +1391,7 @@ async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let text = concat!( "let one = ;\n", // @@ -1457,9 +1460,7 @@ async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) { - println!("hello from stdout"); - eprintln!("hello from stderr"); - cx.foreground().forbid_parking(); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({ "a.rs": "one two three" })) @@ -1515,7 +1516,7 @@ async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppC #[gpui::test] async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -1673,7 +1674,7 @@ async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let text = " use a::b; @@ -1781,7 +1782,7 @@ async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestApp #[gpui::test] async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let text = " use a::b; @@ -1902,6 +1903,8 @@ fn chunks_with_diagnostics( #[gpui::test(iterations = 10)] async fn test_definition(cx: &mut gpui::TestAppContext) { + init_test(cx); + let mut language = Language::new( LanguageConfig { name: "Rust".into(), @@ -2001,6 +2004,8 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { + init_test(cx); + let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), @@ -2085,6 +2090,8 @@ async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { + init_test(cx); + let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), @@ -2138,6 +2145,8 @@ async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) { #[gpui::test(iterations = 10)] async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { + init_test(cx); + let mut language = Language::new( LanguageConfig { name: "TypeScript".into(), @@ -2254,6 +2263,8 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { #[gpui::test(iterations = 10)] async fn test_save_file(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2284,6 +2295,8 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2313,6 +2326,8 @@ async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_save_as(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree("/dir", json!({})).await; @@ -2373,6 +2388,9 @@ async fn test_rescan_and_remote_updates( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { + init_test(cx); + cx.foreground().allow_parking(); + let dir = temp_tree(json!({ "a": { "file1": "", @@ -2529,6 +2547,8 @@ async fn test_buffer_identity_across_renames( deterministic: Arc, cx: &mut gpui::TestAppContext, ) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2577,6 +2597,8 @@ async fn test_buffer_identity_across_renames( #[gpui::test] async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2621,6 +2643,8 @@ async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2765,6 +2789,8 @@ async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { + init_test(cx); + let initial_contents = "aaa\nbbbbb\nc\n"; let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -2844,6 +2870,8 @@ async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -2904,7 +2932,7 @@ async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -3146,7 +3174,7 @@ async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_rename(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -3284,6 +3312,8 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_search(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); fs.insert_tree( "/dir", @@ -3339,6 +3369,8 @@ async fn test_search(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + let search_query = "file"; let fs = FakeFs::new(cx.background()); @@ -3447,6 +3479,8 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + let search_query = "file"; let fs = FakeFs::new(cx.background()); @@ -3554,6 +3588,8 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) { + init_test(cx); + let search_query = "file"; let fs = FakeFs::new(cx.background()); @@ -3680,3 +3716,12 @@ async fn search( }) .collect()) } + +fn init_test(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + }); +} diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index d3b2876675..d52ec403fb 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -24,6 +24,7 @@ futures.workspace = true unicase = "2.6" [dev-dependencies] +language = { path = "../language", features = ["test-support"] } editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7602ff7db8..97684d874a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1360,15 +1360,12 @@ mod tests { use gpui::{TestAppContext, ViewHandle}; use project::FakeFs; use serde_json::json; + use settings::SettingsStore; use std::{collections::HashSet, path::Path}; #[gpui::test] async fn test_visible_list(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| { - let settings = Settings::test(cx); - cx.set_global(settings); - }); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -1456,11 +1453,7 @@ mod tests { #[gpui::test(iterations = 30)] async fn test_editing_files(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| { - let settings = Settings::test(cx); - cx.set_global(settings); - }); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -1776,11 +1769,7 @@ mod tests { #[gpui::test] async fn test_copy_paste(cx: &mut gpui::TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| { - let settings = Settings::test(cx); - cx.set_global(settings); - }); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -1940,4 +1929,12 @@ mod tests { result } + + fn init_test(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + }); + } } diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml index 1c0d3a6165..be2510c895 100644 --- a/crates/project_symbols/Cargo.toml +++ b/crates/project_symbols/Cargo.toml @@ -30,3 +30,4 @@ gpui = { path = "../gpui", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 25828f17ca..3176cad308 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -244,12 +244,12 @@ mod tests { use gpui::{serde_json::json, TestAppContext}; use language::{FakeLspAdapter, Language, LanguageConfig}; use project::FakeFs; + use settings::SettingsStore; use std::{path::Path, sync::Arc}; #[gpui::test] async fn test_project_symbols(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); - cx.update(|cx| cx.set_global(Settings::test(cx))); + init_test(cx); let mut language = Language::new( LanguageConfig { @@ -368,6 +368,15 @@ mod tests { }); } + fn init_test(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(|cx| { + cx.set_global(Settings::test(cx)); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + }); + } + fn symbol(name: &str, path: impl AsRef) -> lsp::SymbolInformation { #[allow(deprecated)] lsp::SymbolInformation { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b0af51379d..38f3894743 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -655,19 +655,11 @@ mod tests { use editor::{DisplayPoint, Editor}; use gpui::{color::Color, test::EmptyView, TestAppContext}; use language::Buffer; - use std::sync::Arc; use unindent::Unindent as _; #[gpui::test] async fn test_search_simple(cx: &mut TestAppContext) { - let fonts = cx.font_cache(); - let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); - theme.search.match_background = Color::red(); - cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.theme = Arc::new(theme); - cx.set_global(settings) - }); + crate::project_search::tests::init_test(cx); let buffer = cx.add_model(|cx| { Buffer::new( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 05d27b824c..4dc947e5f6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1146,25 +1146,18 @@ impl ToolbarItemView for ProjectSearchBar { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use editor::DisplayPoint; use gpui::{color::Color, executor::Deterministic, TestAppContext}; use project::FakeFs; use serde_json::json; + use settings::SettingsStore; use std::sync::Arc; #[gpui::test] async fn test_project_search(deterministic: Arc, cx: &mut TestAppContext) { - let fonts = cx.font_cache(); - let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); - theme.search.match_background = Color::red(); - cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.theme = Arc::new(theme); - cx.set_global(settings); - cx.set_global(ActiveSearches::default()); - }); + init_test(cx); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -1279,4 +1272,20 @@ mod tests { ); }); } + + pub fn init_test(cx: &mut TestAppContext) { + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); + theme.search.match_background = Color::red(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + cx.set_global(ActiveSearches::default()); + let mut settings = Settings::test(cx); + settings.theme = Arc::new(theme); + cx.set_global(settings); + + language::init(cx); + }); + } } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index c476024d62..f5e50aaffb 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -3,7 +3,7 @@ mod keymap_file; mod settings_file; mod settings_store; -use anyhow::bail; +use anyhow::{bail, Result}; use gpui::{ font_cache::{FamilyId, FontCache}, fonts, AppContext, AssetSource, @@ -19,7 +19,7 @@ use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; -use std::{borrow::Cow, collections::HashMap, num::NonZeroU32, path::Path, str, sync::Arc}; +use std::{borrow::Cow, collections::HashMap, str, sync::Arc}; use theme::{Theme, ThemeRegistry}; use util::ResultExt as _; @@ -33,7 +33,6 @@ pub const INITIAL_USER_SETTINGS_ASSET_PATH: &str = "settings/initial_user_settin #[derive(Clone)] pub struct Settings { - pub features: Features, pub buffer_font_family_name: String, pub buffer_font_features: fonts::Features, pub buffer_font_family: FamilyId, @@ -46,13 +45,8 @@ pub struct Settings { pub show_call_status_icon: bool, pub autosave: Autosave, pub default_dock_anchor: DockAnchor, - pub editor_defaults: EditorSettings, - pub editor_overrides: EditorSettings, pub git: GitSettings, pub git_overrides: GitSettings, - pub copilot: CopilotSettings, - pub language_defaults: HashMap, EditorSettings>, - pub language_overrides: HashMap, EditorSettings>, pub lsp: HashMap, LspSettings>, pub theme: Arc, pub base_keymap: BaseKeymap, @@ -67,7 +61,7 @@ impl Setting for Settings { defaults: &Self::FileContent, user_values: &[&Self::FileContent], cx: &AppContext, - ) -> Self { + ) -> Result { let buffer_font_features = defaults.buffer_font_features.clone().unwrap(); let themes = cx.global::>(); @@ -90,50 +84,18 @@ impl Setting for Settings { show_call_status_icon: defaults.show_call_status_icon.unwrap(), autosave: defaults.autosave.unwrap(), default_dock_anchor: defaults.default_dock_anchor.unwrap(), - editor_defaults: EditorSettings { - tab_size: defaults.editor.tab_size, - hard_tabs: defaults.editor.hard_tabs, - soft_wrap: defaults.editor.soft_wrap, - preferred_line_length: defaults.editor.preferred_line_length, - remove_trailing_whitespace_on_save: defaults - .editor - .remove_trailing_whitespace_on_save, - ensure_final_newline_on_save: defaults.editor.ensure_final_newline_on_save, - format_on_save: defaults.editor.format_on_save.clone(), - formatter: defaults.editor.formatter.clone(), - enable_language_server: defaults.editor.enable_language_server, - show_copilot_suggestions: defaults.editor.show_copilot_suggestions, - show_whitespaces: defaults.editor.show_whitespaces, - }, - editor_overrides: Default::default(), - copilot: CopilotSettings { - disabled_globs: defaults - .copilot - .clone() - .unwrap() - .disabled_globs - .unwrap() - .into_iter() - .map(|s| glob::Pattern::new(&s).unwrap()) - .collect(), - }, git: defaults.git.unwrap(), git_overrides: Default::default(), - language_defaults: defaults.languages.clone(), - language_overrides: Default::default(), lsp: defaults.lsp.clone(), theme: themes.get(defaults.theme.as_ref().unwrap()).unwrap(), base_keymap: Default::default(), - features: Features { - copilot: defaults.features.copilot.unwrap(), - }, }; for value in user_values.into_iter().copied().cloned() { this.set_user_settings(value, themes.as_ref(), cx.font_cache()); } - this + Ok(this) } fn json_schema( @@ -247,18 +209,6 @@ impl BaseKeymap { .unwrap_or_default() } } - -#[derive(Clone, Debug, Default)] -pub struct CopilotSettings { - pub disabled_globs: Vec, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct CopilotSettingsContent { - #[serde(default)] - pub disabled_globs: Option>, -} - #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { pub git_gutter: Option, @@ -273,52 +223,6 @@ pub enum GitGutter { Hide, } -pub struct GitGutterConfig {} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct EditorSettings { - pub tab_size: Option, - pub hard_tabs: Option, - pub soft_wrap: Option, - pub preferred_line_length: Option, - pub format_on_save: Option, - pub remove_trailing_whitespace_on_save: Option, - pub ensure_final_newline_on_save: Option, - pub formatter: Option, - pub enable_language_server: Option, - pub show_copilot_suggestions: Option, - pub show_whitespaces: Option, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SoftWrap { - None, - EditorWidth, - PreferredLineLength, -} -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum FormatOnSave { - On, - Off, - LanguageServer, - External { - command: String, - arguments: Vec, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Formatter { - LanguageServer, - External { - command: String, - arguments: Vec, - }, -} - #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Autosave { @@ -374,8 +278,6 @@ pub struct SettingsFileContent { #[serde(default)] pub buffer_font_features: Option, #[serde(default)] - pub copilot: Option, - #[serde(default)] pub active_pane_magnification: Option, #[serde(default)] pub cursor_blink: Option, @@ -391,21 +293,14 @@ pub struct SettingsFileContent { pub autosave: Option, #[serde(default)] pub default_dock_anchor: Option, - #[serde(flatten)] - pub editor: EditorSettings, #[serde(default)] pub git: Option, #[serde(default)] - #[serde(alias = "language_overrides")] - pub languages: HashMap, EditorSettings>, - #[serde(default)] pub lsp: HashMap, LspSettings>, #[serde(default)] pub theme: Option, #[serde(default)] pub base_keymap: Option, - #[serde(default)] - pub features: FeaturesContent, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -414,26 +309,6 @@ pub struct LspSettings { pub initialization_options: Option, } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Features { - pub copilot: bool, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct FeaturesContent { - pub copilot: Option, -} - -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ShowWhitespaces { - #[default] - Selection, - None, - All, -} - impl Settings { pub fn initial_user_settings_content(assets: &'static impl AssetSource) -> Cow<'static, str> { match assets.load(INITIAL_USER_SETTINGS_ASSET_PATH).unwrap() { @@ -448,12 +323,6 @@ impl Settings { font_cache: &FontCache, themes: &ThemeRegistry, ) -> Self { - #[track_caller] - fn required(value: Option) -> Option { - assert!(value.is_some(), "missing default setting value"); - value - } - let defaults: SettingsFileContent = settings_store::parse_json_with_comments( str::from_utf8(assets.load(DEFAULT_SETTINGS_ASSET_PATH).unwrap().as_ref()).unwrap(), ) @@ -478,44 +347,11 @@ impl Settings { show_call_status_icon: defaults.show_call_status_icon.unwrap(), autosave: defaults.autosave.unwrap(), default_dock_anchor: defaults.default_dock_anchor.unwrap(), - editor_defaults: EditorSettings { - tab_size: required(defaults.editor.tab_size), - hard_tabs: required(defaults.editor.hard_tabs), - soft_wrap: required(defaults.editor.soft_wrap), - preferred_line_length: required(defaults.editor.preferred_line_length), - remove_trailing_whitespace_on_save: required( - defaults.editor.remove_trailing_whitespace_on_save, - ), - ensure_final_newline_on_save: required( - defaults.editor.ensure_final_newline_on_save, - ), - format_on_save: required(defaults.editor.format_on_save), - formatter: required(defaults.editor.formatter), - enable_language_server: required(defaults.editor.enable_language_server), - show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions), - show_whitespaces: required(defaults.editor.show_whitespaces), - }, - editor_overrides: Default::default(), - copilot: CopilotSettings { - disabled_globs: defaults - .copilot - .unwrap() - .disabled_globs - .unwrap() - .into_iter() - .map(|s| glob::Pattern::new(&s).unwrap()) - .collect(), - }, git: defaults.git.unwrap(), git_overrides: Default::default(), - language_defaults: defaults.languages, - language_overrides: Default::default(), lsp: defaults.lsp.clone(), theme: themes.get(&defaults.theme.unwrap()).unwrap(), base_keymap: Default::default(), - features: Features { - copilot: defaults.features.copilot.unwrap(), - }, } } @@ -565,121 +401,11 @@ impl Settings { merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.base_keymap, data.base_keymap); - merge(&mut self.features.copilot, data.features.copilot); - if let Some(copilot) = data.copilot { - if let Some(disabled_globs) = copilot.disabled_globs { - self.copilot.disabled_globs = disabled_globs - .into_iter() - .filter_map(|s| glob::Pattern::new(&s).ok()) - .collect() - } - } - self.editor_overrides = data.editor; self.git_overrides = data.git.unwrap_or_default(); - self.language_overrides = data.languages; self.lsp = data.lsp; } - pub fn with_language_defaults( - mut self, - language_name: impl Into>, - overrides: EditorSettings, - ) -> Self { - self.language_defaults - .insert(language_name.into(), overrides); - self - } - - pub fn features(&self) -> &Features { - &self.features - } - - pub fn show_copilot_suggestions(&self, language: Option<&str>, path: Option<&Path>) -> bool { - if !self.features.copilot { - return false; - } - - if !self.copilot_enabled_for_language(language) { - return false; - } - - if let Some(path) = path { - if !self.copilot_enabled_for_path(path) { - return false; - } - } - - true - } - - pub fn copilot_enabled_for_path(&self, path: &Path) -> bool { - !self - .copilot - .disabled_globs - .iter() - .any(|glob| glob.matches_path(path)) - } - - pub fn copilot_enabled_for_language(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| settings.show_copilot_suggestions) - } - - pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 { - self.language_setting(language, |settings| settings.tab_size) - } - - pub fn show_whitespaces(&self, language: Option<&str>) -> ShowWhitespaces { - self.language_setting(language, |settings| settings.show_whitespaces) - } - - pub fn hard_tabs(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| settings.hard_tabs) - } - - pub fn soft_wrap(&self, language: Option<&str>) -> SoftWrap { - self.language_setting(language, |settings| settings.soft_wrap) - } - - pub fn preferred_line_length(&self, language: Option<&str>) -> u32 { - self.language_setting(language, |settings| settings.preferred_line_length) - } - - pub fn remove_trailing_whitespace_on_save(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| { - settings.remove_trailing_whitespace_on_save.clone() - }) - } - - pub fn ensure_final_newline_on_save(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| { - settings.ensure_final_newline_on_save.clone() - }) - } - - pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave { - self.language_setting(language, |settings| settings.format_on_save.clone()) - } - - pub fn formatter(&self, language: Option<&str>) -> Formatter { - self.language_setting(language, |settings| settings.formatter.clone()) - } - - pub fn enable_language_server(&self, language: Option<&str>) -> bool { - self.language_setting(language, |settings| settings.enable_language_server) - } - - fn language_setting(&self, language: Option<&str>, f: F) -> R - where - F: Fn(&EditorSettings) -> Option, - { - None.or_else(|| language.and_then(|l| self.language_overrides.get(l).and_then(&f))) - .or_else(|| f(&self.editor_overrides)) - .or_else(|| language.and_then(|l| self.language_defaults.get(l).and_then(&f))) - .or_else(|| f(&self.editor_defaults)) - .expect("missing default") - } - pub fn git_gutter(&self) -> GitGutter { self.git_overrides.git_gutter.unwrap_or_else(|| { self.git @@ -706,29 +432,11 @@ impl Settings { show_call_status_icon: true, autosave: Autosave::Off, default_dock_anchor: DockAnchor::Bottom, - editor_defaults: EditorSettings { - tab_size: Some(4.try_into().unwrap()), - hard_tabs: Some(false), - soft_wrap: Some(SoftWrap::None), - preferred_line_length: Some(80), - remove_trailing_whitespace_on_save: Some(true), - ensure_final_newline_on_save: Some(true), - format_on_save: Some(FormatOnSave::On), - formatter: Some(Formatter::LanguageServer), - enable_language_server: Some(true), - show_copilot_suggestions: Some(true), - show_whitespaces: Some(ShowWhitespaces::None), - }, - editor_overrides: Default::default(), - copilot: Default::default(), git: Default::default(), git_overrides: Default::default(), - language_defaults: Default::default(), - language_overrides: Default::default(), lsp: Default::default(), theme: gpui::fonts::with_font_cache(cx.font_cache().clone(), Default::default), base_keymap: Default::default(), - features: Features { copilot: true }, } } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 936f3a7099..070da5d1ea 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -42,12 +42,12 @@ pub fn test_settings() -> String { serde_json::json!({ "buffer_font_family": "Courier", "buffer_font_features": {}, - "default_buffer_font_size": 14, - "preferred_line_length": 80, + "buffer_font_size": 14, "theme": theme::EMPTY_THEME_NAME, }), &mut value, ); + value.as_object_mut().unwrap().remove("languages"); serde_json::to_string(&value).unwrap() } diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index caa77a3603..c389a7bf84 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -1,5 +1,5 @@ -use anyhow::{anyhow, Result}; -use collections::{btree_map, hash_map, BTreeMap, HashMap, HashSet}; +use anyhow::Result; +use collections::{btree_map, hash_map, BTreeMap, HashMap}; use gpui::AppContext; use lazy_static::lazy_static; use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; @@ -8,7 +8,6 @@ use smallvec::SmallVec; use std::{ any::{type_name, Any, TypeId}, fmt::Debug, - mem, ops::Range, path::Path, str, @@ -37,7 +36,9 @@ pub trait Setting: 'static { default_value: &Self::FileContent, user_values: &[&Self::FileContent], cx: &AppContext, - ) -> Self; + ) -> Result + where + Self: Sized; fn json_schema(generator: &mut SchemaGenerator, _: &SettingsJsonSchemaParams) -> RootSchema { generator.root_schema_for::() @@ -57,7 +58,7 @@ pub trait Setting: 'static { fn load_via_json_merge( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - ) -> Self + ) -> Result where Self: DeserializeOwned, { @@ -65,7 +66,11 @@ pub trait Setting: 'static { for value in [default_value].iter().chain(user_values) { merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged); } - serde_json::from_value(merged).unwrap() + Ok(serde_json::from_value(merged)?) + } + + fn missing_default() -> anyhow::Error { + anyhow::anyhow!("missing default") } } @@ -78,10 +83,9 @@ pub struct SettingsJsonSchemaParams<'a> { #[derive(Default)] pub struct SettingsStore { setting_values: HashMap>, - default_deserialized_settings: Option, - user_deserialized_settings: Option, - local_deserialized_settings: BTreeMap, DeserializedSettingMap>, - changed_setting_types: HashSet, + default_deserialized_settings: Option, + user_deserialized_settings: Option, + local_deserialized_settings: BTreeMap, serde_json::Value>, tab_size_callback: Option<(TypeId, Box Option>)>, } @@ -98,9 +102,9 @@ trait AnySettingValue { fn load_setting( &self, default_value: &DeserializedSetting, - custom: &[&DeserializedSetting], + custom: &[DeserializedSetting], cx: &AppContext, - ) -> Box; + ) -> Result>; fn value_for_path(&self, path: Option<&Path>) -> &dyn Any; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, path: Arc, value: Box); @@ -113,11 +117,6 @@ trait AnySettingValue { struct DeserializedSetting(Box); -struct DeserializedSettingMap { - untyped: serde_json::Value, - typed: HashMap, -} - impl SettingsStore { /// Add a new type of setting to the store. pub fn register_setting(&mut self, cx: &AppContext) { @@ -132,23 +131,27 @@ impl SettingsStore { local_values: Vec::new(), })); - if let Some(default_settings) = self.default_deserialized_settings.as_mut() { - Self::load_setting_in_map(setting_type_id, setting_value, default_settings); + if let Some(default_settings) = &self.default_deserialized_settings { + if let Some(default_settings) = setting_value + .deserialize_setting(default_settings) + .log_err() + { + let mut user_values_stack = Vec::new(); - let mut user_values_stack = Vec::new(); - if let Some(user_settings) = self.user_deserialized_settings.as_mut() { - Self::load_setting_in_map(setting_type_id, setting_value, user_settings); - if let Some(user_value) = user_settings.typed.get(&setting_type_id) { - user_values_stack = vec![user_value]; + if let Some(user_settings) = &self.user_deserialized_settings { + if let Some(user_settings) = + setting_value.deserialize_setting(user_settings).log_err() + { + user_values_stack = vec![user_settings]; + } } - } - if let Some(default_deserialized_value) = default_settings.typed.get(&setting_type_id) { - setting_value.set_global_value(setting_value.load_setting( - default_deserialized_value, - &user_values_stack, - cx, - )); + if let Some(setting) = setting_value + .load_setting(&default_settings, &user_values_stack, cx) + .log_err() + { + setting_value.set_global_value(setting); + } } } } @@ -173,7 +176,7 @@ impl SettingsStore { pub fn untyped_user_settings(&self) -> &serde_json::Value { self.user_deserialized_settings .as_ref() - .map_or(&serde_json::Value::Null, |s| &s.untyped) + .unwrap_or(&serde_json::Value::Null) } #[cfg(any(test, feature = "test-support"))] @@ -181,6 +184,7 @@ impl SettingsStore { let mut this = Self::default(); this.set_default_settings(&crate::test_settings(), cx) .unwrap(); + this.set_user_settings("{}", cx).unwrap(); this } @@ -194,11 +198,11 @@ impl SettingsStore { cx: &AppContext, update: impl FnOnce(&mut T::FileContent), ) { - let old_text = if let Some(user_settings) = &self.user_deserialized_settings { - serde_json::to_string(&user_settings.untyped).unwrap() - } else { - String::new() - }; + if self.user_deserialized_settings.is_none() { + self.set_user_settings("{}", cx).unwrap(); + } + let old_text = + serde_json::to_string(self.user_deserialized_settings.as_ref().unwrap()).unwrap(); let new_text = self.new_text_for_update::(old_text, update); self.set_user_settings(&new_text, cx).unwrap(); } @@ -212,7 +216,7 @@ impl SettingsStore { ) -> String { let edits = self.edits_for_update::(&old_text, update); let mut new_text = old_text; - for (range, replacement) in edits.into_iter().rev() { + for (range, replacement) in edits.into_iter() { new_text.replace_range(range, &replacement); } new_text @@ -226,26 +230,31 @@ impl SettingsStore { update: impl FnOnce(&mut T::FileContent), ) -> Vec<(Range, String)> { let setting_type_id = TypeId::of::(); + let old_content = self - .user_deserialized_settings - .as_ref() - .unwrap() - .typed + .setting_values .get(&setting_type_id) - .unwrap() + .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) + .deserialize_setting( + self.user_deserialized_settings + .as_ref() + .expect("no user settings loaded"), + ) + .unwrap_or_else(|e| { + panic!( + "could not deserialize setting type {} from user settings: {}", + type_name::(), + e + ) + }) .0 - .downcast_ref::() - .unwrap() - .clone(); + .downcast::() + .unwrap(); let mut new_content = old_content.clone(); update(&mut new_content); - let mut parser = tree_sitter::Parser::new(); - parser.set_language(tree_sitter_json::language()).unwrap(); - let tree = parser.parse(text, None).unwrap(); - - let old_value = &serde_json::to_value(old_content).unwrap(); - let new_value = &serde_json::to_value(new_content).unwrap(); + let old_value = &serde_json::to_value(&old_content).unwrap(); + let new_value = serde_json::to_value(new_content).unwrap(); let mut key_path = Vec::new(); if let Some(key) = T::KEY { @@ -254,16 +263,15 @@ impl SettingsStore { let mut edits = Vec::new(); let tab_size = self.json_tab_size(); + let mut text = text.to_string(); update_value_in_json_text( - &text, - &tree, + &mut text, &mut key_path, tab_size, &old_value, &new_value, &mut edits, ); - edits.sort_unstable_by_key(|e| e.0.start); return edits; } @@ -300,19 +308,8 @@ impl SettingsStore { default_settings_content: &str, cx: &AppContext, ) -> Result<()> { - let deserialized_setting_map = self.load_setting_map(default_settings_content)?; - if deserialized_setting_map.typed.len() != self.setting_values.len() { - return Err(anyhow!( - "default settings file is missing fields: {:?}", - self.setting_values - .iter() - .filter(|(type_id, _)| !deserialized_setting_map.typed.contains_key(type_id)) - .map(|(name, _)| *name) - .collect::>() - )); - } - self.default_deserialized_settings = Some(deserialized_setting_map); - self.recompute_values(false, None, None, cx); + self.default_deserialized_settings = Some(serde_json::from_str(default_settings_content)?); + self.recompute_values(None, cx)?; Ok(()) } @@ -322,10 +319,8 @@ impl SettingsStore { user_settings_content: &str, cx: &AppContext, ) -> Result<()> { - let user_settings = self.load_setting_map(user_settings_content)?; - let old_user_settings = - mem::replace(&mut self.user_deserialized_settings, Some(user_settings)); - self.recompute_values(true, None, old_user_settings, cx); + self.user_deserialized_settings = Some(serde_json::from_str(user_settings_content)?); + self.recompute_values(None, cx)?; Ok(()) } @@ -336,14 +331,13 @@ impl SettingsStore { settings_content: Option<&str>, cx: &AppContext, ) -> Result<()> { - let removed_map = if let Some(settings_content) = settings_content { + if let Some(content) = settings_content { self.local_deserialized_settings - .insert(path.clone(), self.load_setting_map(settings_content)?); - None + .insert(path.clone(), serde_json::from_str(content)?); } else { - self.local_deserialized_settings.remove(&path) - }; - self.recompute_values(true, Some(&path), removed_map, cx); + self.local_deserialized_settings.remove(&path); + } + self.recompute_values(Some(&path), cx)?; Ok(()) } @@ -422,136 +416,78 @@ impl SettingsStore { fn recompute_values( &mut self, - user_settings_changed: bool, changed_local_path: Option<&Path>, - old_settings_map: Option, cx: &AppContext, - ) { - // Identify all of the setting types that have changed. - let new_settings_map = if let Some(changed_path) = changed_local_path { - self.local_deserialized_settings.get(changed_path) - } else if user_settings_changed { - self.user_deserialized_settings.as_ref() - } else { - self.default_deserialized_settings.as_ref() - }; - self.changed_setting_types.clear(); - for map in [old_settings_map.as_ref(), new_settings_map] { - if let Some(map) = map { - self.changed_setting_types.extend(map.typed.keys()); - } - } + ) -> Result<()> { + // Reload the global and local values for every setting. + let mut user_settings_stack = Vec::::new(); + let mut paths_stack = Vec::>::new(); + for setting_value in self.setting_values.values_mut() { + if let Some(default_settings) = &self.default_deserialized_settings { + let default_settings = setting_value.deserialize_setting(default_settings)?; - // Reload the global and local values for every changed setting. - let mut user_values_stack = Vec::<&DeserializedSetting>::new(); - for setting_type_id in self.changed_setting_types.iter() { - let setting_value = self.setting_values.get_mut(setting_type_id).unwrap(); + user_settings_stack.clear(); + paths_stack.clear(); - // Build the prioritized list of deserialized values to pass to the setting's - // load function. - user_values_stack.clear(); - if let Some(user_settings) = &self.user_deserialized_settings { - if let Some(user_value) = user_settings.typed.get(setting_type_id) { - user_values_stack.push(&user_value); - } - } - - let default_deserialized_value = if let Some(value) = self - .default_deserialized_settings - .as_ref() - .and_then(|map| map.typed.get(setting_type_id)) - { - value - } else { - continue; - }; - - // If the global settings file changed, reload the global value for the field. - if changed_local_path.is_none() { - setting_value.set_global_value(setting_value.load_setting( - default_deserialized_value, - &user_values_stack, - cx, - )); - } - - // Reload the local values for the setting. - let user_value_stack_len = user_values_stack.len(); - for (path, deserialized_values) in &self.local_deserialized_settings { - // If a local settings file changed, then avoid recomputing local - // settings for any path outside of that directory. - if changed_local_path.map_or(false, |changed_local_path| { - !path.starts_with(changed_local_path) - }) { - continue; + if let Some(user_settings) = &self.user_deserialized_settings { + if let Some(user_settings) = + setting_value.deserialize_setting(user_settings).log_err() + { + user_settings_stack.push(user_settings); + paths_stack.push(None); + } } - // Ignore recomputing settings for any path that hasn't customized that setting. - let Some(deserialized_value) = deserialized_values.typed.get(setting_type_id) else { - continue; - }; + // If the global settings file changed, reload the global value for the field. + if changed_local_path.is_none() { + setting_value.set_global_value(setting_value.load_setting( + &default_settings, + &user_settings_stack, + cx, + )?); + } - // Build a stack of all of the local values for that setting. - user_values_stack.truncate(user_value_stack_len); - for (preceding_path, preceding_deserialized_values) in - &self.local_deserialized_settings - { - if preceding_path >= path { + // Reload the local values for the setting. + for (path, local_settings) in &self.local_deserialized_settings { + // Build a stack of all of the local values for that setting. + while let Some(prev_path) = paths_stack.last() { + if let Some(prev_path) = prev_path { + if !path.starts_with(prev_path) { + paths_stack.pop(); + user_settings_stack.pop(); + continue; + } + } break; } - if !path.starts_with(preceding_path) { - continue; - } - if let Some(preceding_deserialized_value) = - preceding_deserialized_values.typed.get(setting_type_id) + if let Some(local_settings) = + setting_value.deserialize_setting(&local_settings).log_err() { - user_values_stack.push(&*preceding_deserialized_value); + paths_stack.push(Some(path.as_ref())); + user_settings_stack.push(local_settings); + + // If a local settings file changed, then avoid recomputing local + // settings for any path outside of that directory. + if changed_local_path.map_or(false, |changed_local_path| { + !path.starts_with(changed_local_path) + }) { + continue; + } + + setting_value.set_local_value( + path.clone(), + setting_value.load_setting( + &default_settings, + &user_settings_stack, + cx, + )?, + ); } } - user_values_stack.push(&*deserialized_value); - - // Load the local value for the field. - setting_value.set_local_value( - path.clone(), - setting_value.load_setting(default_deserialized_value, &user_values_stack, cx), - ); } } - } - - /// Deserialize the given JSON string into a map keyed by setting type. - /// - /// Returns an error if the string doesn't contain a valid JSON object. - fn load_setting_map(&self, json: &str) -> Result { - let mut map = DeserializedSettingMap { - untyped: parse_json_with_comments(json)?, - typed: HashMap::default(), - }; - for (setting_type_id, setting_value) in self.setting_values.iter() { - Self::load_setting_in_map(*setting_type_id, setting_value, &mut map); - } - Ok(map) - } - - fn load_setting_in_map( - setting_type_id: TypeId, - setting_value: &Box, - map: &mut DeserializedSettingMap, - ) { - let value = if let Some(setting_key) = setting_value.key() { - if let Some(value) = map.untyped.get(setting_key) { - value - } else { - return; - } - } else { - &map.untyped - }; - - if let Some(deserialized_value) = setting_value.deserialize_setting(&value).log_err() { - map.typed.insert(setting_type_id, deserialized_value); - } + Ok(()) } } @@ -567,18 +503,21 @@ impl AnySettingValue for SettingValue { fn load_setting( &self, default_value: &DeserializedSetting, - user_values: &[&DeserializedSetting], + user_values: &[DeserializedSetting], cx: &AppContext, - ) -> Box { + ) -> Result> { let default_value = default_value.0.downcast_ref::().unwrap(); let values: SmallVec<[&T::FileContent; 6]> = user_values .iter() .map(|value| value.0.downcast_ref().unwrap()) .collect(); - Box::new(T::load(default_value, &values, cx)) + Ok(Box::new(T::load(default_value, &values, cx)?)) } - fn deserialize_setting(&self, json: &serde_json::Value) -> Result { + fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result { + if let Some(key) = T::KEY { + json = json.get(key).unwrap_or(&serde_json::Value::Null); + } let value = T::FileContent::deserialize(json)?; Ok(DeserializedSetting(Box::new(value))) } @@ -593,7 +532,7 @@ impl AnySettingValue for SettingValue { } self.global_value .as_ref() - .expect("no default value for setting") + .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name())) } fn set_global_value(&mut self, value: Box) { @@ -634,8 +573,7 @@ impl AnySettingValue for SettingValue { // } fn update_value_in_json_text<'a>( - text: &str, - syntax_tree: &tree_sitter::Tree, + text: &mut String, key_path: &mut Vec<&'a str>, tab_size: usize, old_value: &'a serde_json::Value, @@ -653,7 +591,6 @@ fn update_value_in_json_text<'a>( let new_sub_value = new_object.get(key).unwrap_or(&serde_json::Value::Null); update_value_in_json_text( text, - syntax_tree, key_path, tab_size, old_sub_value, @@ -667,7 +604,6 @@ fn update_value_in_json_text<'a>( if !old_object.contains_key(key) { update_value_in_json_text( text, - syntax_tree, key_path, tab_size, &serde_json::Value::Null, @@ -679,14 +615,14 @@ fn update_value_in_json_text<'a>( } } else if old_value != new_value { let (range, replacement) = - replace_value_in_json_text(text, syntax_tree, &key_path, tab_size, &new_value); + replace_value_in_json_text(text, &key_path, tab_size, &new_value); + text.replace_range(range.clone(), &replacement); edits.push((range, replacement)); } } fn replace_value_in_json_text( text: &str, - syntax_tree: &tree_sitter::Tree, key_path: &[&str], tab_size: usize, new_value: impl Serialize, @@ -702,6 +638,10 @@ fn replace_value_in_json_text( .unwrap(); } + let mut parser = tree_sitter::Parser::new(); + parser.set_language(tree_sitter_json::language()).unwrap(); + let syntax_tree = parser.parse(text, None).unwrap(); + let mut cursor = tree_sitter::QueryCursor::new(); let has_language_overrides = text.contains(LANGUAGE_OVERRIDES); @@ -1152,7 +1092,7 @@ mod tests { store.set_user_settings(&old_json, cx).ok(); let edits = store.edits_for_update::(&old_json, update); let mut new_json = old_json; - for (range, replacement) in edits.into_iter().rev() { + for (range, replacement) in edits.into_iter() { new_json.replace_range(range, &replacement); } pretty_assertions::assert_eq!(new_json, expected_new_json); @@ -1180,7 +1120,7 @@ mod tests { default_value: &UserSettingsJson, user_values: &[&UserSettingsJson], _: &AppContext, - ) -> Self { + ) -> Result { Self::load_via_json_merge(default_value, user_values) } } @@ -1196,7 +1136,7 @@ mod tests { default_value: &Option, user_values: &[&Option], _: &AppContext, - ) -> Self { + ) -> Result { Self::load_via_json_merge(default_value, user_values) } } @@ -1224,7 +1164,7 @@ mod tests { default_value: &MultiKeySettingsJson, user_values: &[&MultiKeySettingsJson], _: &AppContext, - ) -> Self { + ) -> Result { Self::load_via_json_merge(default_value, user_values) } } @@ -1257,7 +1197,7 @@ mod tests { default_value: &JournalSettingsJson, user_values: &[&JournalSettingsJson], _: &AppContext, - ) -> Self { + ) -> Result { Self::load_via_json_merge(default_value, user_values) } } @@ -1278,7 +1218,7 @@ mod tests { type FileContent = Self; - fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Self { + fn load(default_value: &Self, user_values: &[&Self], _: &AppContext) -> Result { Self::load_via_json_merge(default_value, user_values) } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index bf4fc46ee3..cc4580daa1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -159,7 +159,7 @@ impl settings::Setting for TerminalSettings { default_value: &Self::FileContent, user_values: &[&Self::FileContent], _: &AppContext, - ) -> Self { + ) -> Result { Self::load_via_json_merge(default_value, user_values) } } diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 5f7cf0a5a3..c34a5b469b 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -12,6 +12,7 @@ doctest = false neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"] [dependencies] +anyhow.workspace = true serde.workspace = true serde_derive.workspace = true itertools = "0.10" diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index ac86a08235..9ac40831d1 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -17,14 +17,16 @@ pub struct VimTestContext<'a> { impl<'a> VimTestContext<'a> { pub async fn new(cx: &'a mut gpui::TestAppContext, enabled: bool) -> VimTestContext<'a> { let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await; + cx.update(|cx| { search::init(cx); crate::init(cx); + }); + cx.update(|cx| { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); }); - settings::KeymapFileContent::load("keymaps/vim.json", cx).unwrap(); }); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 65c46e81cf..23a28b3544 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -10,8 +10,7 @@ mod state; mod utils; mod visual; -use std::sync::Arc; - +use anyhow::Result; use collections::CommandPaletteFilter; use editor::{Bias, Cancel, Editor, EditorMode, Event}; use gpui::{ @@ -24,6 +23,7 @@ use normal::normal_replace; use serde::Deserialize; use settings::{Setting, SettingsStore}; use state::{Mode, Operator, VimState}; +use std::sync::Arc; use visual::visual_replace; use workspace::{self, Workspace}; @@ -343,14 +343,10 @@ impl Setting for VimModeSetting { default_value: &Self::FileContent, user_values: &[&Self::FileContent], _: &AppContext, - ) -> Self { - Self( - user_values - .first() - .map(|e| **e) - .flatten() - .unwrap_or(default_value.unwrap()), - ) + ) -> Result { + Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or( + default_value.ok_or_else(Self::missing_default)?, + ))) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0e3ce2a724..768142d729 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -369,8 +369,12 @@ pub struct AppState { impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { - cx.set_global(settings::SettingsStore::test(cx)); - cx.set_global(Settings::test(cx)); + use settings::SettingsStore; + + if !cx.has_global::() { + cx.set_global(SettingsStore::test(cx)); + cx.set_global(Settings::test(cx)); + } let fs = fs::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::test()); diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 84c5798b07..7e4ddcef19 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -249,16 +249,21 @@ impl super::LspAdapter for CLspAdapter { #[cfg(test)] mod tests { use gpui::TestAppContext; - use language::{AutoindentMode, Buffer}; - use settings::Settings; + use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer}; + use settings::SettingsStore; + use std::num::NonZeroU32; #[gpui::test] async fn test_c_autoindent(cx: &mut TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); }); let language = crate::languages::language("c", tree_sitter_c::language(), None).await; diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index acd31e8205..7aaddf5fe8 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -170,8 +170,9 @@ impl LspAdapter for PythonLspAdapter { #[cfg(test)] mod tests { use gpui::{ModelContext, TestAppContext}; - use language::{AutoindentMode, Buffer}; - use settings::Settings; + use language::{language_settings::AllLanguageSettings, AutoindentMode, Buffer}; + use settings::SettingsStore; + use std::num::NonZeroU32; #[gpui::test] async fn test_python_autoindent(cx: &mut TestAppContext) { @@ -179,9 +180,13 @@ mod tests { let language = crate::languages::language("python", tree_sitter_python::language(), None).await; cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); }); cx.add_model(|cx| { diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 92fb5bc3b2..15700ec80a 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -253,10 +253,13 @@ impl LspAdapter for RustLspAdapter { #[cfg(test)] mod tests { + use std::num::NonZeroU32; + use super::*; use crate::languages::language; use gpui::{color::Color, TestAppContext}; - use settings::Settings; + use language::language_settings::AllLanguageSettings; + use settings::SettingsStore; use theme::SyntaxTheme; #[gpui::test] @@ -435,9 +438,13 @@ mod tests { async fn test_rust_autoindent(cx: &mut TestAppContext) { cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); cx.update(|cx| { - let mut settings = Settings::test(cx); - settings.editor_overrides.tab_size = Some(2.try_into().unwrap()); - cx.set_global(settings); + cx.set_global(SettingsStore::test(cx)); + language::init(cx); + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |s| { + s.defaults.tab_size = NonZeroU32::new(2); + }); + }); }); let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index fed76cd5b9..b62168775c 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -2,10 +2,11 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; -use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use language::{ + language_settings::language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, +}; use node_runtime::NodeRuntime; use serde_json::Value; -use settings::Settings; use smol::fs; use std::{ any::Any, @@ -100,14 +101,13 @@ impl LspAdapter for YamlLspAdapter { } fn workspace_configuration(&self, cx: &mut AppContext) -> Option> { - let settings = cx.global::(); Some( future::ready(serde_json::json!({ "yaml": { "keyOrdering": false }, "[yaml]": { - "editor.tabSize": settings.tab_size(Some("YAML")) + "editor.tabSize": language_settings(None, Some("YAML"), cx).tab_size, } })) .boxed(), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1398369e75..b8ffab4a3b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -597,7 +597,7 @@ mod tests { #[gpui::test] async fn test_open_paths_action(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -697,7 +697,7 @@ mod tests { #[gpui::test] async fn test_window_edit_state(executor: Arc, cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -777,7 +777,7 @@ mod tests { #[gpui::test] async fn test_new_empty_workspace(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); cx.update(|cx| { open_new(&app_state, cx, |workspace, cx| { Editor::new_file(workspace, &Default::default(), cx) @@ -816,7 +816,7 @@ mod tests { #[gpui::test] async fn test_open_entry(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -929,7 +929,7 @@ mod tests { #[gpui::test] async fn test_open_paths(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs @@ -1099,7 +1099,7 @@ mod tests { #[gpui::test] async fn test_save_conflicting_item(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -1143,7 +1143,7 @@ mod tests { #[gpui::test] async fn test_open_and_save_new_file(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state.fs.create_dir(Path::new("/root")).await.unwrap(); let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; @@ -1232,7 +1232,7 @@ mod tests { #[gpui::test] async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state.fs.create_dir(Path::new("/root")).await.unwrap(); let project = Project::test(app_state.fs.clone(), [], cx).await; @@ -1271,7 +1271,7 @@ mod tests { #[gpui::test] async fn test_pane_actions(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -1345,7 +1345,7 @@ mod tests { #[gpui::test] async fn test_navigation(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -1622,7 +1622,7 @@ mod tests { #[gpui::test] async fn test_reopening_closed_items(cx: &mut TestAppContext) { - let app_state = init(cx); + let app_state = init_test(cx); app_state .fs .as_fake() @@ -1843,7 +1843,7 @@ mod tests { cx.foreground().run_until_parked(); } - fn init(cx: &mut TestAppContext) -> Arc { + fn init_test(cx: &mut TestAppContext) -> Arc { cx.foreground().forbid_parking(); cx.update(|cx| { let mut app_state = AppState::test(cx); @@ -1852,6 +1852,7 @@ mod tests { state.build_window_options = build_window_options; call::init(app_state.client.clone(), app_state.user_store.clone(), cx); workspace::init(app_state.clone(), cx); + language::init(cx); editor::init(cx); pane::init(cx); app_state