Merge branch 'main' into settings-for-journal
This commit is contained in:
+265
-19
@@ -1,4 +1,6 @@
|
||||
mod keymap_file;
|
||||
pub mod settings_file;
|
||||
pub mod watched_json;
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{
|
||||
@@ -10,10 +12,11 @@ use schemars::{
|
||||
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
|
||||
JsonSchema,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{collections::HashMap, num::NonZeroU32, str, sync::Arc};
|
||||
use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
|
||||
use theme::{Theme, ThemeRegistry};
|
||||
use tree_sitter::Query;
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub use keymap_file::{keymap_file_json_schema, KeymapFileContent};
|
||||
@@ -32,6 +35,8 @@ pub struct Settings {
|
||||
pub default_dock_anchor: DockAnchor,
|
||||
pub editor_defaults: EditorSettings,
|
||||
pub editor_overrides: EditorSettings,
|
||||
pub git: GitSettings,
|
||||
pub git_overrides: GitSettings,
|
||||
pub journal_defaults: JournalSettings,
|
||||
pub journal_overrides: JournalSettings,
|
||||
pub terminal_defaults: TerminalSettings,
|
||||
@@ -43,7 +48,7 @@ pub struct Settings {
|
||||
pub staff_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FeatureFlags {
|
||||
pub experimental_themes: bool,
|
||||
}
|
||||
@@ -54,7 +59,23 @@ impl FeatureFlags {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct GitSettings {
|
||||
pub git_gutter: Option<GitGutter>,
|
||||
pub gutter_debounce: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GitGutter {
|
||||
#[default]
|
||||
TrackedFiles,
|
||||
Hide,
|
||||
}
|
||||
|
||||
pub struct GitGutterConfig {}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditorSettings {
|
||||
pub tab_size: Option<NonZeroU32>,
|
||||
pub hard_tabs: Option<bool>,
|
||||
@@ -65,14 +86,14 @@ pub struct EditorSettings {
|
||||
pub enable_language_server: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SoftWrap {
|
||||
None,
|
||||
EditorWidth,
|
||||
PreferredLineLength,
|
||||
}
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FormatOnSave {
|
||||
On,
|
||||
@@ -84,7 +105,7 @@ pub enum FormatOnSave {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Formatter {
|
||||
LanguageServer,
|
||||
@@ -94,7 +115,7 @@ pub enum Formatter {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Autosave {
|
||||
Off,
|
||||
@@ -103,7 +124,7 @@ pub enum Autosave {
|
||||
OnWindowChange,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct JournalSettings {
|
||||
pub path: Option<String>,
|
||||
pub hour_format: Option<HourFormat>,
|
||||
@@ -131,7 +152,7 @@ impl Default for HourFormat {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TerminalSettings {
|
||||
pub shell: Option<Shell>,
|
||||
pub working_directory: Option<WorkingDirectory>,
|
||||
@@ -141,9 +162,10 @@ pub struct TerminalSettings {
|
||||
pub blinking: Option<TerminalBlink>,
|
||||
pub alternate_scroll: Option<AlternateScroll>,
|
||||
pub option_as_meta: Option<bool>,
|
||||
pub copy_on_select: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TerminalBlink {
|
||||
Off,
|
||||
@@ -157,7 +179,7 @@ impl Default for TerminalBlink {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Shell {
|
||||
System,
|
||||
@@ -171,7 +193,7 @@ impl Default for Shell {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AlternateScroll {
|
||||
On,
|
||||
@@ -184,7 +206,7 @@ impl Default for AlternateScroll {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkingDirectory {
|
||||
CurrentProjectDirectory,
|
||||
@@ -193,7 +215,7 @@ pub enum WorkingDirectory {
|
||||
Always { directory: String },
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Deserialize, JsonSchema)]
|
||||
#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DockAnchor {
|
||||
#[default]
|
||||
@@ -202,7 +224,7 @@ pub enum DockAnchor {
|
||||
Expanded,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct SettingsFileContent {
|
||||
pub experiments: Option<FeatureFlags>,
|
||||
#[serde(default)]
|
||||
@@ -228,6 +250,8 @@ pub struct SettingsFileContent {
|
||||
#[serde(default)]
|
||||
pub terminal: TerminalSettings,
|
||||
#[serde(default)]
|
||||
pub git: Option<GitSettings>,
|
||||
#[serde(default)]
|
||||
#[serde(alias = "language_overrides")]
|
||||
pub languages: HashMap<Arc<str>, EditorSettings>,
|
||||
#[serde(default)]
|
||||
@@ -238,7 +262,7 @@ pub struct SettingsFileContent {
|
||||
pub staff_mode: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct LspSettings {
|
||||
pub initialization_options: Option<Value>,
|
||||
@@ -284,9 +308,11 @@ impl Settings {
|
||||
enable_language_server: required(defaults.editor.enable_language_server),
|
||||
},
|
||||
editor_overrides: Default::default(),
|
||||
journal_defaults: Default::default(),
|
||||
git: defaults.git.unwrap(),
|
||||
git_overrides: Default::default(),
|
||||
journal_defaults: defaults.journal,
|
||||
journal_overrides: Default::default(),
|
||||
terminal_defaults: Default::default(),
|
||||
terminal_defaults: defaults.terminal,
|
||||
terminal_overrides: Default::default(),
|
||||
language_defaults: defaults.languages,
|
||||
language_overrides: Default::default(),
|
||||
@@ -337,8 +363,10 @@ impl Settings {
|
||||
}
|
||||
|
||||
self.editor_overrides = data.editor;
|
||||
self.git_overrides = data.git.unwrap_or_default();
|
||||
self.journal_overrides = data.journal;
|
||||
self.terminal_defaults.font_size = data.terminal.font_size;
|
||||
self.terminal_overrides.copy_on_select = data.terminal.copy_on_select;
|
||||
self.terminal_overrides = data.terminal;
|
||||
self.language_overrides = data.languages;
|
||||
self.lsp = data.lsp;
|
||||
@@ -393,6 +421,14 @@ impl Settings {
|
||||
.expect("missing default")
|
||||
}
|
||||
|
||||
pub fn git_gutter(&self) -> GitGutter {
|
||||
self.git_overrides.git_gutter.unwrap_or_else(|| {
|
||||
self.git
|
||||
.git_gutter
|
||||
.expect("git_gutter should be some by setting setup")
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &gpui::AppContext) -> Settings {
|
||||
Settings {
|
||||
@@ -419,6 +455,8 @@ impl Settings {
|
||||
journal_overrides: Default::default(),
|
||||
terminal_defaults: Default::default(),
|
||||
terminal_overrides: Default::default(),
|
||||
git: Default::default(),
|
||||
git_overrides: Default::default(),
|
||||
language_defaults: Default::default(),
|
||||
language_overrides: Default::default(),
|
||||
lsp: Default::default(),
|
||||
@@ -503,6 +541,103 @@ pub fn settings_file_json_schema(
|
||||
serde_json::to_value(root_schema).unwrap()
|
||||
}
|
||||
|
||||
/// Expects the key to be unquoted, and the value to be valid JSON
|
||||
/// (e.g. values should be unquoted for numbers and bools, quoted for strings)
|
||||
pub fn write_top_level_setting(
|
||||
mut settings_content: String,
|
||||
top_level_key: &str,
|
||||
new_val: &str,
|
||||
) -> String {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
parser.set_language(tree_sitter_json::language()).unwrap();
|
||||
let tree = parser.parse(&settings_content, None).unwrap();
|
||||
|
||||
let mut cursor = tree_sitter::QueryCursor::new();
|
||||
|
||||
let query = Query::new(
|
||||
tree_sitter_json::language(),
|
||||
"
|
||||
(document
|
||||
(object
|
||||
(pair
|
||||
key: (string) @key
|
||||
value: (_) @value)))
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut first_key_start = None;
|
||||
let mut existing_value_range = None;
|
||||
let matches = cursor.matches(&query, tree.root_node(), settings_content.as_bytes());
|
||||
for mat in matches {
|
||||
if mat.captures.len() != 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = mat.captures[0];
|
||||
let value = mat.captures[1];
|
||||
|
||||
first_key_start.get_or_insert_with(|| key.node.start_byte());
|
||||
|
||||
if let Some(key_text) = settings_content.get(key.node.byte_range()) {
|
||||
if key_text == format!("\"{top_level_key}\"") {
|
||||
existing_value_range = Some(value.node.byte_range());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match (first_key_start, existing_value_range) {
|
||||
(None, None) => {
|
||||
// No document, create a new object and overwrite
|
||||
settings_content.clear();
|
||||
write!(
|
||||
settings_content,
|
||||
"{{\n \"{}\": {new_val}\n}}\n",
|
||||
top_level_key
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
(_, Some(existing_value_range)) => {
|
||||
// Existing theme key, overwrite
|
||||
settings_content.replace_range(existing_value_range, &new_val);
|
||||
}
|
||||
|
||||
(Some(first_key_start), None) => {
|
||||
// No existing theme key, but other settings. Prepend new theme settings and
|
||||
// match style of first key
|
||||
let mut row = 0;
|
||||
let mut column = 0;
|
||||
for (ix, char) in settings_content.char_indices() {
|
||||
if ix == first_key_start {
|
||||
break;
|
||||
}
|
||||
if char == '\n' {
|
||||
row += 1;
|
||||
column = 0;
|
||||
} else {
|
||||
column += char.len_utf8();
|
||||
}
|
||||
}
|
||||
|
||||
let content = format!(r#""{top_level_key}": {new_val},"#);
|
||||
settings_content.insert_str(first_key_start, &content);
|
||||
|
||||
if row > 0 {
|
||||
settings_content.insert_str(
|
||||
first_key_start + content.len(),
|
||||
&format!("\n{:width$}", ' ', width = column),
|
||||
)
|
||||
} else {
|
||||
settings_content.insert_str(first_key_start + content.len(), " ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settings_content
|
||||
}
|
||||
|
||||
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
@@ -514,3 +649,114 @@ pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T>
|
||||
json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()),
|
||||
)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::write_top_level_setting;
|
||||
use unindent::Unindent;
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_settings_with_theme() {
|
||||
let settings = r#"
|
||||
{
|
||||
"theme": "one-dark"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new_settings = r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_empty_settings() {
|
||||
let settings = r#"
|
||||
{
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new_settings = r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_no_settings() {
|
||||
let settings = "".to_string();
|
||||
|
||||
let new_settings = r#"
|
||||
{
|
||||
"theme": "summerfruit-light"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_single_line_settings_without_theme() {
|
||||
let settings = r#"{ "a": "", "ok": true }"#.to_string();
|
||||
let new_settings = r#"{ "theme": "summerfruit-light", "a": "", "ok": true }"#;
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_pre_object_whitespace() {
|
||||
let settings = r#" { "a": "", "ok": true }"#.to_string();
|
||||
let new_settings = r#" { "theme": "summerfruit-light", "a": "", "ok": true }"#;
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_theme_into_multi_line_settings_without_theme() {
|
||||
let settings = r#"
|
||||
{
|
||||
"a": "b"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let new_settings = r#"
|
||||
{
|
||||
"theme": "summerfruit-light",
|
||||
"a": "b"
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let settings_after_theme =
|
||||
write_top_level_setting(settings, "theme", "\"summerfruit-light\"");
|
||||
|
||||
assert_eq!(settings_after_theme, new_settings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
use crate::{watched_json::WatchedJsonFile, write_top_level_setting, SettingsFileContent};
|
||||
use anyhow::Result;
|
||||
use fs::Fs;
|
||||
use gpui::MutableAppContext;
|
||||
use serde_json::Value;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
// TODO: Switch SettingsFile to open a worktree and buffer for synchronization
|
||||
// And instant updates in the Zed editor
|
||||
#[derive(Clone)]
|
||||
pub struct SettingsFile {
|
||||
path: &'static Path,
|
||||
settings_file_content: WatchedJsonFile<SettingsFileContent>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl SettingsFile {
|
||||
pub fn new(
|
||||
path: &'static Path,
|
||||
settings_file_content: WatchedJsonFile<SettingsFileContent>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> Self {
|
||||
SettingsFile {
|
||||
path,
|
||||
settings_file_content,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(cx: &mut MutableAppContext, update: impl FnOnce(&mut SettingsFileContent)) {
|
||||
let this = cx.global::<SettingsFile>();
|
||||
|
||||
let current_file_content = this.settings_file_content.current();
|
||||
let mut new_file_content = current_file_content.clone();
|
||||
|
||||
update(&mut new_file_content);
|
||||
|
||||
let fs = this.fs.clone();
|
||||
let path = this.path.clone();
|
||||
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
// Unwrap safety: These values are all guarnteed to be well formed, and we know
|
||||
// that they will deserialize to our settings object. All of the following unwraps
|
||||
// are therefore safe.
|
||||
let tmp = serde_json::to_value(current_file_content).unwrap();
|
||||
let old_json = tmp.as_object().unwrap();
|
||||
|
||||
let new_tmp = serde_json::to_value(new_file_content).unwrap();
|
||||
let new_json = new_tmp.as_object().unwrap();
|
||||
|
||||
// Find changed fields
|
||||
let mut diffs = vec![];
|
||||
for (key, old_value) in old_json.iter() {
|
||||
let new_value = new_json.get(key).unwrap();
|
||||
if old_value != new_value {
|
||||
if matches!(
|
||||
new_value,
|
||||
&Value::Null | &Value::Object(_) | &Value::Array(_)
|
||||
) {
|
||||
unimplemented!(
|
||||
"We only support updating basic values at the top level"
|
||||
);
|
||||
}
|
||||
|
||||
let new_json = serde_json::to_string_pretty(new_value)
|
||||
.expect("Could not serialize new json field to string");
|
||||
|
||||
diffs.push((key, new_json));
|
||||
}
|
||||
}
|
||||
|
||||
// Have diffs, rewrite the settings file now.
|
||||
let mut content = fs.load(path).await?;
|
||||
|
||||
for (key, new_value) in diffs {
|
||||
content = write_top_level_setting(content, key, &new_value)
|
||||
}
|
||||
|
||||
fs.atomic_write(path.to_path_buf(), content).await?;
|
||||
|
||||
Ok(()) as Result<()>
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{watched_json::watch_settings_file, EditorSettings, Settings, SoftWrap};
|
||||
use fs::FakeFs;
|
||||
use theme::ThemeRegistry;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_watch_settings_files(cx: &mut gpui::TestAppContext) {
|
||||
let executor = cx.background();
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
let font_cache = cx.font_cache();
|
||||
|
||||
fs.save(
|
||||
"/settings.json".as_ref(),
|
||||
&r#"
|
||||
{
|
||||
"buffer_font_size": 24,
|
||||
"soft_wrap": "editor_width",
|
||||
"tab_size": 8,
|
||||
"language_overrides": {
|
||||
"Markdown": {
|
||||
"tab_size": 2,
|
||||
"preferred_line_length": 100,
|
||||
"soft_wrap": "preferred_line_length"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
.into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let source = WatchedJsonFile::new(fs.clone(), &executor, "/settings.json".as_ref()).await;
|
||||
|
||||
let default_settings = cx.read(Settings::test).with_language_defaults(
|
||||
"JavaScript",
|
||||
EditorSettings {
|
||||
tab_size: Some(2.try_into().unwrap()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
cx.update(|cx| {
|
||||
watch_settings_file(
|
||||
default_settings.clone(),
|
||||
source,
|
||||
ThemeRegistry::new((), font_cache),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
let settings = cx.read(|cx| cx.global::<Settings>().clone());
|
||||
assert_eq!(settings.buffer_font_size, 24.0);
|
||||
|
||||
assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
|
||||
assert_eq!(
|
||||
settings.soft_wrap(Some("Markdown")),
|
||||
SoftWrap::PreferredLineLength
|
||||
);
|
||||
assert_eq!(
|
||||
settings.soft_wrap(Some("JavaScript")),
|
||||
SoftWrap::EditorWidth
|
||||
);
|
||||
|
||||
assert_eq!(settings.preferred_line_length(None), 80);
|
||||
assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
|
||||
assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
|
||||
|
||||
assert_eq!(settings.tab_size(None).get(), 8);
|
||||
assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
|
||||
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
|
||||
|
||||
fs.save(
|
||||
"/settings.json".as_ref(),
|
||||
&"(garbage)".into(),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
// fs.remove_file("/settings.json".as_ref(), Default::default())
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
let settings = cx.read(|cx| cx.global::<Settings>().clone());
|
||||
assert_eq!(settings.buffer_font_size, 24.0);
|
||||
|
||||
assert_eq!(settings.soft_wrap(None), SoftWrap::EditorWidth);
|
||||
assert_eq!(
|
||||
settings.soft_wrap(Some("Markdown")),
|
||||
SoftWrap::PreferredLineLength
|
||||
);
|
||||
assert_eq!(
|
||||
settings.soft_wrap(Some("JavaScript")),
|
||||
SoftWrap::EditorWidth
|
||||
);
|
||||
|
||||
assert_eq!(settings.preferred_line_length(None), 80);
|
||||
assert_eq!(settings.preferred_line_length(Some("Markdown")), 100);
|
||||
assert_eq!(settings.preferred_line_length(Some("JavaScript")), 80);
|
||||
|
||||
assert_eq!(settings.tab_size(None).get(), 8);
|
||||
assert_eq!(settings.tab_size(Some("Markdown")).get(), 2);
|
||||
assert_eq!(settings.tab_size(Some("JavaScript")).get(), 8);
|
||||
|
||||
fs.remove_file("/settings.json".as_ref(), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
cx.foreground().run_until_parked();
|
||||
let settings = cx.read(|cx| cx.global::<Settings>().clone());
|
||||
assert_eq!(settings.buffer_font_size, default_settings.buffer_font_size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{executor, MutableAppContext};
|
||||
use postage::sink::Sink as _;
|
||||
use postage::{prelude::Stream, watch};
|
||||
use serde::Deserialize;
|
||||
|
||||
use std::{path::Path, sync::Arc, time::Duration};
|
||||
use theme::ThemeRegistry;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::{parse_json_with_comments, KeymapFileContent, Settings, SettingsFileContent};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WatchedJsonFile<T>(pub watch::Receiver<T>);
|
||||
|
||||
impl<T> WatchedJsonFile<T>
|
||||
where
|
||||
T: 'static + for<'de> Deserialize<'de> + Clone + Default + Send + Sync,
|
||||
{
|
||||
pub async fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
executor: &executor::Background,
|
||||
path: impl Into<Arc<Path>>,
|
||||
) -> Self {
|
||||
let path = path.into();
|
||||
let settings = Self::load(fs.clone(), &path).await.unwrap_or_default();
|
||||
let mut events = fs.watch(&path, Duration::from_millis(500)).await;
|
||||
let (mut tx, rx) = watch::channel_with(settings);
|
||||
executor
|
||||
.spawn(async move {
|
||||
while events.next().await.is_some() {
|
||||
if let Some(settings) = Self::load(fs.clone(), &path).await {
|
||||
if tx.send(settings).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
Self(rx)
|
||||
}
|
||||
|
||||
///Loads the given watched JSON file. In the special case that the file is
|
||||
///empty (ignoring whitespace) or is not a file, this will return T::default()
|
||||
async fn load(fs: Arc<dyn Fs>, path: &Path) -> Option<T> {
|
||||
if !fs.is_file(path).await {
|
||||
return Some(T::default());
|
||||
}
|
||||
|
||||
fs.load(path).await.log_err().and_then(|data| {
|
||||
if data.trim().is_empty() {
|
||||
Some(T::default())
|
||||
} else {
|
||||
parse_json_with_comments(&data).log_err()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn current(&self) -> T {
|
||||
self.0.borrow().clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn watch_settings_file(
|
||||
defaults: Settings,
|
||||
mut file: WatchedJsonFile<SettingsFileContent>,
|
||||
theme_registry: Arc<ThemeRegistry>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
settings_updated(&defaults, file.0.borrow().clone(), &theme_registry, cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
while let Some(content) = file.0.recv().await {
|
||||
cx.update(|cx| settings_updated(&defaults, content, &theme_registry, cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn keymap_updated(content: KeymapFileContent, cx: &mut MutableAppContext) {
|
||||
cx.clear_bindings();
|
||||
KeymapFileContent::load_defaults(cx);
|
||||
content.add_to_cx(cx).log_err();
|
||||
}
|
||||
|
||||
pub fn settings_updated(
|
||||
defaults: &Settings,
|
||||
content: SettingsFileContent,
|
||||
theme_registry: &Arc<ThemeRegistry>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
let mut settings = defaults.clone();
|
||||
settings.set_user_settings(content, theme_registry, cx.font_cache());
|
||||
cx.set_global(settings);
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
pub fn watch_keymap_file(mut file: WatchedJsonFile<KeymapFileContent>, cx: &mut MutableAppContext) {
|
||||
cx.spawn(|mut cx| async move {
|
||||
while let Some(content) = file.0.recv().await {
|
||||
cx.update(|cx| keymap_updated(content, cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Reference in New Issue
Block a user