feat: configure command and search popup anchors
This commit is contained in:
+134
@@ -1,6 +1,86 @@
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PopupAnchor {
|
||||
TopLeft,
|
||||
TopCenter,
|
||||
TopRight,
|
||||
CenterLeft,
|
||||
Center,
|
||||
CenterRight,
|
||||
#[default]
|
||||
BottomLeft,
|
||||
BottomCenter,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct PopupOptions {
|
||||
pub anchor: PopupAnchor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub struct PopupConfig {
|
||||
pub command: PopupOptions,
|
||||
pub search: PopupOptions,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PopupOptions {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Options {
|
||||
#[serde(default)]
|
||||
anchor: PopupAnchor,
|
||||
}
|
||||
let options: Options = deserialize_object(deserializer)?;
|
||||
Ok(Self {
|
||||
anchor: options.anchor,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PopupConfig {
|
||||
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Options {
|
||||
#[serde(default)]
|
||||
command: PopupOptions,
|
||||
#[serde(default)]
|
||||
search: PopupOptions,
|
||||
}
|
||||
let options: Options = deserialize_object(deserializer)?;
|
||||
Ok(Self {
|
||||
command: options.command,
|
||||
search: options.search,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Struct derives also accept sequences; popup configuration accepts only objects.
|
||||
fn deserialize_object<'de, D: Deserializer<'de>, T: Deserialize<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<T, D::Error> {
|
||||
struct ObjectVisitor<T>(std::marker::PhantomData<T>);
|
||||
|
||||
impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for ObjectVisitor<T> {
|
||||
type Value = T;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("a popup configuration object")
|
||||
}
|
||||
|
||||
fn visit_map<M: serde::de::MapAccess<'de>>(self, map: M) -> Result<T, M::Error> {
|
||||
T::deserialize(serde::de::value::MapAccessDeserializer::new(map))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_map(ObjectVisitor(std::marker::PhantomData))
|
||||
}
|
||||
|
||||
const DEFAULT_BOTTOM_SEGMENTS: &[&str] = &[
|
||||
"mode",
|
||||
"pending",
|
||||
@@ -260,6 +340,60 @@ pub fn labels<'de, D: Deserializer<'de>>(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn popup_partial_options_keep_command_and_search_independent() {
|
||||
let popup: PopupConfig = serde_json::from_value(serde_json::json!({
|
||||
"command": {"anchor": "top_right"}
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(popup.command.anchor, PopupAnchor::TopRight);
|
||||
assert_eq!(popup.search.anchor, PopupAnchor::BottomLeft);
|
||||
let popup: PopupConfig = serde_json::from_value(serde_json::json!({
|
||||
"command": {}, "search": {"anchor": "center_left"}
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(popup.command.anchor, PopupAnchor::BottomLeft);
|
||||
assert_eq!(popup.search.anchor, PopupAnchor::CenterLeft);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_popup_options_are_rejected_at_the_boundary() {
|
||||
for value in [
|
||||
serde_json::json!(null),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!(7),
|
||||
serde_json::json!({"selector": {}}),
|
||||
] {
|
||||
assert!(
|
||||
serde_json::from_value::<PopupConfig>(value.clone()).is_err(),
|
||||
"{value}"
|
||||
);
|
||||
}
|
||||
for options in [
|
||||
serde_json::json!(null),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!("center"),
|
||||
serde_json::json!({"anchor": null}),
|
||||
serde_json::json!({"anchor": 7}),
|
||||
serde_json::json!({"anchor": true}),
|
||||
serde_json::json!({"anchor": []}),
|
||||
serde_json::json!({"anchor": {}}),
|
||||
serde_json::json!({"anchor": "middle"}),
|
||||
serde_json::json!({"anchor": "TopLeft"}),
|
||||
serde_json::json!({"width": 72}),
|
||||
serde_json::json!({"anchor": "center", "callback": "run"}),
|
||||
] {
|
||||
for mode in ["command", "search"] {
|
||||
let mut popup = serde_json::json!({});
|
||||
popup[mode] = options.clone();
|
||||
assert!(
|
||||
serde_json::from_value::<PopupConfig>(popup).is_err(),
|
||||
"{mode}: {options}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Lines {
|
||||
#[serde(deserialize_with = "top")]
|
||||
|
||||
Reference in New Issue
Block a user