This commit is contained in:
Antonio Scandurra
2023-10-21 17:52:47 +02:00
parent e4fe9538d7
commit b7d30fca2b
30 changed files with 3644 additions and 29 deletions
+163
View File
@@ -0,0 +1,163 @@
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
use anyhow::{anyhow, Context, Result};
use collections::BTreeMap;
use gpui2::{AppContext, KeyBinding};
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
JsonSchema,
};
use serde::Deserialize;
use serde_json::Value;
use util::{asset_str, ResultExt};
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)]
pub struct KeymapFile(Vec<KeymapBlock>);
#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
pub struct KeymapBlock {
#[serde(default)]
context: Option<String>,
bindings: BTreeMap<String, KeymapAction>,
}
#[derive(Debug, Deserialize, Default, Clone)]
#[serde(transparent)]
pub struct KeymapAction(Value);
impl JsonSchema for KeymapAction {
fn schema_name() -> String {
"KeymapAction".into()
}
fn json_schema(_: &mut SchemaGenerator) -> Schema {
Schema::Bool(true)
}
}
#[derive(Deserialize)]
struct ActionWithData(Box<str>, Value);
impl KeymapFile {
pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> {
let content = asset_str::<SettingsAssets>(asset_path);
Self::parse(content.as_ref())?.add_to_cx(cx)
}
pub fn parse(content: &str) -> Result<Self> {
parse_json_with_comments::<Self>(content)
}
pub fn add_to_cx(self, cx: &mut AppContext) -> Result<()> {
for KeymapBlock { context, bindings } in self.0 {
let bindings = bindings
.into_iter()
.filter_map(|(keystroke, action)| {
let action = action.0;
// This is a workaround for a limitation in serde: serde-rs/json#497
// We want to deserialize the action data as a `RawValue` so that we can
// deserialize the action itself dynamically directly from the JSON
// string. But `RawValue` currently does not work inside of an untagged enum.
match action {
Value::Array(items) => {
let Ok([name, data]): Result<[serde_json::Value; 2], _> =
items.try_into()
else {
return Some(Err(anyhow!("Expected array of length 2")));
};
let serde_json::Value::String(name) = name else {
return Some(Err(anyhow!(
"Expected first item in array to be a string."
)));
};
cx.build_action(&name, Some(data))
}
Value::String(name) => cx.build_action(&name, None),
Value::Null => Ok(no_action()),
_ => {
return Some(Err(anyhow!("Expected two-element array, got {action:?}")))
}
}
.with_context(|| {
format!(
"invalid binding value for keystroke {keystroke}, context {context:?}"
)
})
.log_err()
.map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
})
.collect::<Result<Vec<_>>>()?;
cx.bind_keys(bindings);
}
Ok(())
}
pub fn generate_json_schema(action_names: &[&'static str]) -> serde_json::Value {
let mut root_schema = SchemaSettings::draft07()
.with(|settings| settings.option_add_null_type = false)
.into_generator()
.into_root_schema_for::<KeymapFile>();
let action_schema = Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::String))),
enum_values: Some(
action_names
.iter()
.map(|name| Value::String(name.to_string()))
.collect(),
),
..Default::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
..Default::default()
}),
Schema::Object(SchemaObject {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
..Default::default()
}),
]),
..Default::default()
})),
..Default::default()
});
root_schema
.definitions
.insert("KeymapAction".to_owned(), action_schema);
serde_json::to_value(root_schema).unwrap()
}
}
fn no_action() -> Box<dyn gpui2::Action> {
todo!()
}
#[cfg(test)]
mod tests {
use crate::KeymapFile;
#[test]
fn can_deserialize_keymap_with_trailing_comma() {
let json = indoc::indoc! {"[
// Standard macOS bindings
{
\"bindings\": {
\"up\": \"menu::SelectPrev\",
},
},
]
"
};
KeymapFile::parse(json).unwrap();
}
}
+38
View File
@@ -0,0 +1,38 @@
mod keymap_file;
mod settings_file;
mod settings_store;
use rust_embed::RustEmbed;
use std::{borrow::Cow, str};
use util::asset_str;
pub use keymap_file::KeymapFile;
pub use settings_file::*;
pub use settings_store::{Setting, SettingsJsonSchemaParams, SettingsStore};
#[derive(RustEmbed)]
#[folder = "../../assets"]
#[include = "settings/*"]
#[include = "keymaps/*"]
#[exclude = "*.DS_Store"]
pub struct SettingsAssets;
pub fn default_settings() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/default.json")
}
pub fn default_keymap() -> Cow<'static, str> {
asset_str::<SettingsAssets>("keymaps/default.json")
}
pub fn vim_keymap() -> Cow<'static, str> {
asset_str::<SettingsAssets>("keymaps/vim.json")
}
pub fn initial_user_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_user_settings.json")
}
pub fn initial_local_settings_content() -> Cow<'static, str> {
asset_str::<SettingsAssets>("settings/initial_local_settings.json")
}
+135
View File
@@ -0,0 +1,135 @@
use crate::{settings_store::SettingsStore, Setting};
use anyhow::Result;
use fs::Fs;
use futures::{channel::mpsc, StreamExt};
use gpui2::{AppContext, Context};
use std::{
io::ErrorKind,
path::{Path, PathBuf},
str,
sync::Arc,
time::Duration,
};
use util::{paths, ResultExt};
pub fn register<T: Setting>(cx: &mut AppContext) {
cx.update_global(|store: &mut SettingsStore, cx| {
store.register_setting::<T>(cx);
});
}
pub fn get<'a, T: Setting>(cx: &'a AppContext) -> &'a T {
cx.global::<SettingsStore>().get(None)
}
pub fn get_local<'a, T: Setting>(location: Option<(usize, &Path)>, cx: &'a AppContext) -> &'a T {
cx.global::<SettingsStore>().get(location)
}
pub const EMPTY_THEME_NAME: &'static str = "empty-theme";
#[cfg(any(test, feature = "test-support"))]
pub fn test_settings() -> String {
let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
crate::default_settings().as_ref(),
)
.unwrap();
util::merge_non_null_json_value_into(
serde_json::json!({
"buffer_font_family": "Courier",
"buffer_font_features": {},
"buffer_font_size": 14,
"theme": EMPTY_THEME_NAME,
}),
&mut value,
);
value.as_object_mut().unwrap().remove("languages");
serde_json::to_string(&value).unwrap()
}
pub fn watch_config_file(
executor: Arc<Background>,
fs: Arc<dyn Fs>,
path: PathBuf,
) -> mpsc::UnboundedReceiver<String> {
let (tx, rx) = mpsc::unbounded();
executor
.spawn(async move {
let events = fs.watch(&path, Duration::from_millis(100)).await;
futures::pin_mut!(events);
let contents = fs.load(&path).await.unwrap_or_default();
if tx.unbounded_send(contents).is_err() {
return;
}
loop {
if events.next().await.is_none() {
break;
}
if let Ok(contents) = fs.load(&path).await {
if !tx.unbounded_send(contents).is_ok() {
break;
}
}
}
})
.detach();
rx
}
pub fn handle_settings_file_changes(
mut user_settings_file_rx: mpsc::UnboundedReceiver<String>,
cx: &mut AppContext,
) {
let user_settings_content = cx.background().block(user_settings_file_rx.next()).unwrap();
cx.update_global(|store: &mut SettingsStore, cx| {
store
.set_user_settings(&user_settings_content, cx)
.log_err();
});
cx.spawn(move |mut cx| async move {
while let Some(user_settings_content) = user_settings_file_rx.next().await {
cx.update(|cx| {
cx.update_global(|store: &mut SettingsStore, cx| {
store
.set_user_settings(&user_settings_content, cx)
.log_err();
});
cx.refresh_windows();
});
}
})
.detach();
}
async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
match fs.load(&paths::SETTINGS).await {
result @ Ok(_) => result,
Err(err) => {
if let Some(e) = err.downcast_ref::<std::io::Error>() {
if e.kind() == ErrorKind::NotFound {
return Ok(crate::initial_user_settings_content().to_string());
}
}
return Err(err);
}
}
}
pub fn update_settings_file<T: Setting>(
fs: Arc<dyn Fs>,
cx: &mut AppContext,
update: impl 'static + Send + FnOnce(&mut T::FileContent),
) {
cx.spawn(|cx| async move {
let old_text = load_settings(&fs).await;
let new_text = cx.read_global(|store: &SettingsStore, cx| {
store.new_text_for_update::<T>(old_text, update)
});
fs.atomic_write(paths::SETTINGS.clone(), new_text).await?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
File diff suppressed because it is too large Load Diff