diff --git a/Cargo.lock b/Cargo.lock index 607f47b5cf..90da178c7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8018,6 +8018,7 @@ name = "welcome" version = "0.1.0" dependencies = [ "anyhow", + "editor", "gpui", "log", "project", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 31563010b7..0397032de8 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -5086,7 +5086,7 @@ impl From> for AnyWeakModelHandle { } } -#[derive(Debug)] +#[derive(Debug, Copy)] pub struct WeakViewHandle { window_id: usize, view_id: usize, diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 6b51b06c9c..229e11ff3a 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -66,9 +66,18 @@ impl TelemetrySettings { pub fn metrics(&self) -> bool { self.metrics.unwrap() } + pub fn diagnostics(&self) -> bool { self.diagnostics.unwrap() } + + pub fn set_metrics(&mut self, value: bool) { + self.metrics = Some(value); + } + + pub fn set_diagnostics(&mut self, value: bool) { + self.diagnostics = Some(value); + } } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -679,7 +688,7 @@ pub fn settings_file_json_schema( /// 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( +pub fn write_settings_key( mut settings_content: String, top_level_key: &str, new_val: &str, @@ -786,11 +795,160 @@ pub fn parse_json_with_comments(content: &str) -> Result )?) } +pub fn update_settings_file( + old_text: String, + old_file_content: SettingsFileContent, + update: impl FnOnce(&mut SettingsFileContent), +) -> String { + let mut new_file_content = old_file_content.clone(); + update(&mut new_file_content); + + let old_json = to_json_object(old_file_content); + let new_json = to_json_object(new_file_content); + + // 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)); + } + } + + let mut new_text = old_text; + for (key, new_value) in diffs { + new_text = write_settings_key(new_text, key, &new_value) + } + new_text +} + +fn to_json_object(settings_file: SettingsFileContent) -> serde_json::Map { + let tmp = serde_json::to_value(settings_file).unwrap(); + match tmp { + Value::Object(map) => map, + _ => unreachable!("SettingsFileContent represents a JSON map"), + } +} + #[cfg(test)] mod tests { - use crate::write_top_level_setting; + use super::*; use unindent::Unindent; + fn assert_new_settings, S2: Into>( + old_json: S1, + update: fn(&mut SettingsFileContent), + expected_new_json: S2, + ) { + let old_json = old_json.into(); + let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap(); + let new_json = update_settings_file(old_json, old_content, update); + assert_eq!(new_json, expected_new_json.into()); + } + + #[test] + fn test_update_telemetry_setting_multiple_fields() { + assert_new_settings( + r#"{ + "telemetry": { + "metrics": false, + "diagnostics": false + } + }"# + .unindent(), + |settings| { + settings.telemetry.set_diagnostics(true); + settings.telemetry.set_metrics(true); + }, + r#"{ + "telemetry": { + "metrics": true, + "diagnostics": true + } + }"# + .unindent(), + ); + } + + #[test] + fn test_update_telemetry_setting_weird_formatting() { + assert_new_settings( + r#"{ + "telemetry": { "metrics": false, "diagnostics": true } + }"# + .unindent(), + |settings| settings.telemetry.set_diagnostics(false), + r#"{ + "telemetry": { "metrics": false, "diagnostics": false } + }"# + .unindent(), + ); + } + + #[test] + fn test_update_telemetry_setting_other_fields() { + assert_new_settings( + r#"{ + "telemetry": { + "metrics": false, + "diagnostics": true + } + }"# + .unindent(), + |settings| settings.telemetry.set_diagnostics(false), + r#"{ + "telemetry": { + "metrics": false, + "diagnostics": false + } + }"# + .unindent(), + ); + } + + #[test] + fn test_update_telemetry_setting_pre_existing() { + assert_new_settings( + r#"{ + "telemetry": { + "diagnostics": true + } + }"# + .unindent(), + |settings| settings.telemetry.set_diagnostics(false), + r#"{ + "telemetry": { + "diagnostics": false + } + }"# + .unindent(), + ); + } + + #[test] + fn test_update_telemetry_setting() { + assert_new_settings( + "{}", + |settings| settings.telemetry.set_diagnostics(true), + r#"{ + "telemetry": { + "diagnostics": true + } + }"# + .unindent(), + ); + } + #[test] fn test_write_theme_into_settings_with_theme() { let settings = r#" @@ -807,8 +965,7 @@ mod tests { "# .unindent(); - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); + let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); assert_eq!(settings_after_theme, new_settings) } @@ -828,8 +985,7 @@ mod tests { "# .unindent(); - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); + let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); assert_eq!(settings_after_theme, new_settings) } @@ -845,8 +1001,7 @@ mod tests { "# .unindent(); - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); + let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); assert_eq!(settings_after_theme, new_settings) } @@ -856,8 +1011,7 @@ mod tests { 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\""); + let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); assert_eq!(settings_after_theme, new_settings) } @@ -867,8 +1021,7 @@ mod tests { 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\""); + let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); assert_eq!(settings_after_theme, new_settings) } @@ -890,8 +1043,7 @@ mod tests { "# .unindent(); - let settings_after_theme = - write_top_level_setting(settings, "theme", "\"summerfruit-light\""); + let settings_after_theme = write_settings_key(settings, "theme", "\"summerfruit-light\""); assert_eq!(settings_after_theme, new_settings) } diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 506ebc8c3d..575e9499d3 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -1,8 +1,7 @@ -use crate::{watched_json::WatchedJsonFile, write_top_level_setting, SettingsFileContent}; +use crate::{update_settings_file, watched_json::WatchedJsonFile, 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 @@ -27,57 +26,24 @@ impl SettingsFile { } } - pub fn update(cx: &mut MutableAppContext, update: impl FnOnce(&mut SettingsFileContent)) { + pub fn update( + cx: &mut MutableAppContext, + update: impl 'static + Send + FnOnce(&mut SettingsFileContent), + ) { let this = cx.global::(); 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 old_text = fs.load(path).await?; - let new_tmp = serde_json::to_value(new_file_content).unwrap(); - let new_json = new_tmp.as_object().unwrap(); + let new_text = update_settings_file(old_text, current_file_content, update); - // 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?; + fs.atomic_write(path.to_path_buf(), new_text).await?; Ok(()) as Result<()> }) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 484c542ede..49f2d982ba 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -37,6 +37,7 @@ pub struct Theme { pub tooltip: TooltipStyle, pub terminal: TerminalStyle, pub feedback: FeedbackStyle, + pub welcome: WelcomeStyle, pub color_scheme: ColorScheme, } @@ -850,6 +851,20 @@ pub struct FeedbackStyle { pub link_text_hover: ContainedText, } +#[derive(Clone, Deserialize, Default)] +pub struct WelcomeStyle { + pub checkbox: CheckboxStyle, +} + +#[derive(Clone, Deserialize, Default)] +pub struct CheckboxStyle { + pub width: f32, + pub height: f32, + pub unchecked: ContainerStyle, + pub checked: ContainerStyle, + pub hovered: ContainerStyle, +} + #[derive(Clone, Deserialize, Default)] pub struct ColorScheme { pub name: String, diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 6ac312c37f..3e450e2312 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -13,6 +13,7 @@ test-support = [] [dependencies] anyhow = "1.0.38" log = "0.4" +editor = { path = "../editor" } gpui = { path = "../gpui" } project = { path = "../project" } settings = { path = "../settings" } diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index face85c4b6..385a8a5f00 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,19 +1,22 @@ use gpui::{ color::Color, - elements::{Flex, Label, ParentElement, Svg}, - Element, Entity, MutableAppContext, View, + elements::{Empty, Flex, Label, MouseEventHandler, ParentElement, Svg}, + Element, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View, ViewContext, }; -use settings::Settings; +use settings::{settings_file::SettingsFile, Settings, SettingsFileContent}; +use theme::CheckboxStyle; use workspace::{item::Item, Welcome, Workspace}; pub fn init(cx: &mut MutableAppContext) { cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { - let welcome_page = cx.add_view(|_cx| WelcomePage); + let welcome_page = cx.add_view(WelcomePage::new); workspace.add_item(Box::new(welcome_page), cx) }) } -struct WelcomePage; +struct WelcomePage { + _settings_subscription: Subscription, +} impl Entity for WelcomePage { type Event = (); @@ -24,12 +27,21 @@ impl View for WelcomePage { "WelcomePage" } - fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let theme = &cx.global::().theme; + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox { + let settings = cx.global::(); + let theme = settings.theme.clone(); - Flex::new(gpui::Axis::Vertical) + let (diagnostics, metrics) = { + let telemetry = settings.telemetry(); + (telemetry.diagnostics(), telemetry.metrics()) + }; + + enum Metrics {} + enum Diagnostics {} + + Flex::column() .with_children([ - Flex::new(gpui::Axis::Horizontal) + Flex::row() .with_children([ Svg::new("icons/terminal_16.svg") .with_color(Color::red()) @@ -47,11 +59,88 @@ impl View for WelcomePage { theme.editor.hover_popover.prose.clone(), ) .boxed(), + Flex::row() + .with_children([ + self.render_settings_checkbox::( + &theme.welcome.checkbox, + metrics, + cx, + |content, checked| { + content.telemetry.set_metrics(checked); + }, + ), + Label::new( + "Do you want to send telemetry?", + theme.editor.hover_popover.prose.clone(), + ) + .boxed(), + ]) + .boxed(), + Flex::row() + .with_children([ + self.render_settings_checkbox::( + &theme.welcome.checkbox, + diagnostics, + cx, + |content, checked| content.telemetry.set_diagnostics(checked), + ), + Label::new( + "Send crash reports", + theme.editor.hover_popover.prose.clone(), + ) + .boxed(), + ]) + .boxed(), ]) + .aligned() .boxed() } } +impl WelcomePage { + fn new(cx: &mut ViewContext) -> Self { + let handle = cx.weak_handle(); + + let settings_subscription = cx.observe_global::(move |cx| { + if let Some(handle) = handle.upgrade(cx) { + handle.update(cx, |_, cx| cx.notify()) + } + }); + + WelcomePage { + _settings_subscription: settings_subscription, + } + } + + fn render_settings_checkbox( + &self, + style: &CheckboxStyle, + checked: bool, + cx: &mut RenderContext, + set_value: fn(&mut SettingsFileContent, checked: bool) -> (), + ) -> ElementBox { + MouseEventHandler::::new(0, cx, |state, _| { + Empty::new() + .constrained() + .with_width(style.width) + .with_height(style.height) + .contained() + .with_style(if checked { + style.checked + } else if state.hovered() { + style.hovered + } else { + style.unchecked + }) + .boxed() + }) + .on_click(gpui::MouseButton::Left, move |_, cx| { + SettingsFile::update(cx, move |content| set_value(content, !checked)) + }) + .boxed() + } +} + impl Item for WelcomePage { fn tab_content( &self, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index dc57468df6..423ce37d48 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -20,6 +20,7 @@ import contactList from "./contactList" import incomingCallNotification from "./incomingCallNotification" import { ColorScheme } from "../themes/common/colorScheme" import feedback from "./feedback" +import welcome from "./welcome" export default function app(colorScheme: ColorScheme): Object { return { @@ -33,6 +34,7 @@ export default function app(colorScheme: ColorScheme): Object { incomingCallNotification: incomingCallNotification(colorScheme), picker: picker(colorScheme), workspace: workspace(colorScheme), + welcome: welcome(colorScheme), contextMenu: contextMenu(colorScheme), editor: editor(colorScheme), projectDiagnostics: projectDiagnostics(colorScheme), diff --git a/styles/src/styleTree/welcome.ts b/styles/src/styleTree/welcome.ts new file mode 100644 index 0000000000..f1325514cd --- /dev/null +++ b/styles/src/styleTree/welcome.ts @@ -0,0 +1,34 @@ + +import { ColorScheme } from "../themes/common/colorScheme"; +import { border } from "./components"; + +export default function welcome(colorScheme: ColorScheme) { + let layer = colorScheme.highest; + + // TODO + let checkbox_base = { + background: colorScheme.ramps.red(0.5).hex(), + cornerRadius: 8, + padding: { + left: 8, + right: 8, + top: 4, + bottom: 4, + }, + shadow: colorScheme.popoverShadow, + border: border(layer), + margin: { + left: -8, + }, + }; + + return { + checkbox: { + width: 9, + height: 9, + unchecked: checkbox_base, + checked: checkbox_base, + hovered: checkbox_base + } + } +} \ No newline at end of file