Cargo.lock 🔗
@@ -8018,6 +8018,7 @@ name = "welcome"
version = "0.1.0"
dependencies = [
"anyhow",
+ "editor",
"gpui",
"log",
"project",
Mikayla Maki created
Cargo.lock | 1
crates/gpui/src/app.rs | 2
crates/settings/src/settings.rs | 180 +++++++++++++++++++++++++++--
crates/settings/src/settings_file.rs | 50 +-------
crates/theme/src/theme.rs | 15 ++
crates/welcome/Cargo.toml | 1
crates/welcome/src/welcome.rs | 107 ++++++++++++++++-
styles/src/styleTree/app.ts | 2
styles/src/styleTree/welcome.ts | 34 +++++
9 files changed, 326 insertions(+), 66 deletions(-)
@@ -8018,6 +8018,7 @@ name = "welcome"
version = "0.1.0"
dependencies = [
"anyhow",
+ "editor",
"gpui",
"log",
"project",
@@ -5086,7 +5086,7 @@ impl<T: Entity> From<WeakModelHandle<T>> for AnyWeakModelHandle {
}
}
-#[derive(Debug)]
+#[derive(Debug, Copy)]
pub struct WeakViewHandle<T> {
window_id: usize,
view_id: usize,
@@ -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<T: DeserializeOwned>(content: &str) -> Result<T>
)?)
}
+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<String, Value> {
+ 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<S1: Into<String>, S2: Into<String>>(
+ 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)
}
@@ -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::<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));
- }
- }
+ let old_text = fs.load(path).await?;
- // 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)
- }
+ let new_text = update_settings_file(old_text, current_file_content, update);
- fs.atomic_write(path.to_path_buf(), content).await?;
+ fs.atomic_write(path.to_path_buf(), new_text).await?;
Ok(()) as Result<()>
})
@@ -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,
@@ -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" }
@@ -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::<Settings>().theme;
+ fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
+ let settings = cx.global::<Settings>();
+ let theme = settings.theme.clone();
+
+ let (diagnostics, metrics) = {
+ let telemetry = settings.telemetry();
+ (telemetry.diagnostics(), telemetry.metrics())
+ };
+
+ enum Metrics {}
+ enum Diagnostics {}
- Flex::new(gpui::Axis::Vertical)
+ 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::<Metrics>(
+ &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::<Diagnostics>(
+ &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>) -> Self {
+ let handle = cx.weak_handle();
+
+ let settings_subscription = cx.observe_global::<Settings, _>(move |cx| {
+ if let Some(handle) = handle.upgrade(cx) {
+ handle.update(cx, |_, cx| cx.notify())
+ }
+ });
+
+ WelcomePage {
+ _settings_subscription: settings_subscription,
+ }
+ }
+
+ fn render_settings_checkbox<T: 'static>(
+ &self,
+ style: &CheckboxStyle,
+ checked: bool,
+ cx: &mut RenderContext<Self>,
+ set_value: fn(&mut SettingsFileContent, checked: bool) -> (),
+ ) -> ElementBox {
+ MouseEventHandler::<T>::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,
@@ -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),
@@ -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
+ }
+ }
+}