feat: configure command and search popup anchors

This commit is contained in:
Valdemar Lorenzen
2026-10-12 13:41:45 +02:00
parent 406f45c6fb
commit 6eb99a792a
11 changed files with 440 additions and 10 deletions
+89 -4
View File
@@ -1,4 +1,4 @@
use crate::chrome::ChromeLine;
use crate::chrome::{ChromeLine, PopupConfig};
use crate::editor::{Context, Effect, Feature};
use crate::plugin::{DocumentEvent, PluginEvent, PluginUpdate, UpdateEffect};
use serde::{Deserialize, Serialize};
@@ -40,6 +40,7 @@ pub struct Config {
pub clipboard_mode: String,
pub top_bar: ChromeLine,
pub bottom_bar: ChromeLine,
pub popup: PopupConfig,
pub labels: BTreeMap<String, String>,
pub diff_context_lines: usize,
pub diff_expand_lines: usize,
@@ -79,6 +80,7 @@ impl Default for Config {
clipboard_mode: "system".into(),
top_bar: ChromeLine::top(),
bottom_bar: ChromeLine::bottom(),
popup: PopupConfig::default(),
labels: BTreeMap::new(),
diff_context_lines: 10,
diff_expand_lines: 15,
@@ -104,6 +106,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
clipboard_mode,
top_bar,
bottom_bar,
popup,
labels,
diff_context_lines,
diff_expand_lines,
@@ -131,6 +134,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
config.clipboard_mode = clipboard_mode;
config.top_bar = top_bar;
config.bottom_bar = bottom_bar;
config.popup = popup;
config.labels = labels;
config.diff_context_lines = diff_context_lines;
config.diff_expand_lines = diff_expand_lines;
@@ -454,6 +458,7 @@ enum Response {
top_bar: ChromeLine,
#[serde(deserialize_with = "crate::chrome::bottom")]
bottom_bar: ChromeLine,
popup: PopupConfig,
#[serde(deserialize_with = "crate::chrome::labels")]
labels: BTreeMap<String, String>,
#[serde(deserialize_with = "diff_context_lines")]
@@ -1183,9 +1188,9 @@ end
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
}
for frame in [
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15}"#,
] {
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
}
@@ -1226,6 +1231,75 @@ end
assert_eq!(loaded.labels["workspace"], "界 e\u{301} 👩‍🚀\t\n\u{1b}");
}
#[test]
fn popup_configuration_accepts_ruby_symbol_and_string_anchors_independently() {
use crate::chrome::PopupAnchor;
let fixture = Fixture::new();
let anchors = [
("top_left", PopupAnchor::TopLeft),
("top_center", PopupAnchor::TopCenter),
("top_right", PopupAnchor::TopRight),
("center_left", PopupAnchor::CenterLeft),
("center", PopupAnchor::Center),
("center_right", PopupAnchor::CenterRight),
("bottom_left", PopupAnchor::BottomLeft),
("bottom_center", PopupAnchor::BottomCenter),
("bottom_right", PopupAnchor::BottomRight),
];
for (index, (command_name, command_anchor)) in anchors.iter().enumerate() {
let (search_name, search_anchor) = anchors[(index + 1) % anchors.len()];
let path = fixture.file(
"config.rb",
&format!(
"Vamp.configure do |config|\n\
config.popup.command = {{anchor: :{command_name}}}\n\
config.popup.search = {{'anchor' => '{search_name}'}}\n\
end"
),
);
let loaded = load_config(&path).unwrap();
assert_eq!(loaded.popup.command.anchor, *command_anchor);
assert_eq!(loaded.popup.search.anchor, search_anchor);
}
}
#[test]
fn popup_configuration_rejects_unknown_options_anchors_callbacks_and_types() {
let fixture = Fixture::new();
for (value, expected) in [
("[]", "must be a Hash"),
("nil", "must be a Hash"),
("7", "must be a Hash"),
("{width: 72}", "unknown popup option"),
("{callback: -> { 'center' }}", "unknown popup option"),
("{anchor: :middle}", "unknown popup anchor"),
("{anchor: 'TopLeft'}", "unknown popup anchor"),
("{anchor: nil}", "must be a String"),
("{anchor: 7}", "must be a String"),
("{anchor: true}", "must be a String"),
("{anchor: []}", "must be a String"),
("{anchor: {}}", "must be a String"),
("{anchor: -> { :center }}", "must be a String"),
(
"{anchor: :center, 'anchor' => :top_left}",
"duplicate chrome key",
),
("{7 => :center}", "chrome key must be a String"),
] {
for mode in ["command", "search"] {
let path = fixture.file(
"config.rb",
&format!("Vamp.configure {{ |config| config.popup.{mode} = {value} }}"),
);
let error = load_config(&path).unwrap_err();
assert!(error.contains(expected), "{mode}={value}: {error}");
}
}
let path = fixture.file("config.rb", "Vamp.configure { |config| config.popup = {} }");
assert!(load_config(&path).unwrap_err().contains("undefined method"));
}
#[test]
fn bar_namespace_is_stable_and_accepts_swapped_builtins() {
let fixture = Fixture::new();
@@ -1334,6 +1408,7 @@ end
"bindings": [], "leader": "space", "file_explorer_side": "right",
"file_picker_view": "tree", "file_explorer_view": "tree",
"top_bar": {}, "bottom_bar": {}, "labels": {},
"popup": {"command": {}, "search": {}},
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7
});
for setting in ["diff_context_lines", "diff_expand_lines"] {
@@ -1747,6 +1822,7 @@ end"#,
"bindings": [], "leader": "space", "file_explorer_side": "right",
"file_picker_view": "tree", "file_explorer_view": "tree",
"top_bar": {}, "bottom_bar": {}, "labels": {},
"popup": {"command": {}, "search": {}},
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15
});
let Response::Config { lsp, .. } =
@@ -1832,6 +1908,15 @@ end"#,
"frame.delete('top_bar')",
"frame.delete('bottom_bar')",
"frame.delete('labels')",
"frame.delete('popup')",
"frame['popup'] = nil",
"frame['popup'] = []",
"frame['popup'] = {'selector' => {}}",
"frame['popup'] = {'command' => nil}",
"frame['popup'] = {'search' => 7}",
"frame['popup'] = {'command' => {'anchor' => nil}}",
"frame['popup'] = {'search' => {'anchor' => 'middle'}}",
"frame['popup'] = {'command' => {'width' => 72}}",
"frame['top_bar'] = 7",
"frame['bottom_bar'] = []",
"frame['labels'] = {'battery' => 7}",