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")]
|
||||
|
||||
+9
-1
@@ -1,4 +1,4 @@
|
||||
use crate::chrome::{ChromeLine, validate_name};
|
||||
use crate::chrome::{ChromeLine, PopupConfig, validate_name};
|
||||
use crate::clipboard::{ClipboardMode, SystemClipboard};
|
||||
use crate::diff::{DiffState, Focus, Operation, Outcome, Phase, RevisionTarget};
|
||||
use crate::files::{DockSide, FilePicker, FilePresentation, FileView};
|
||||
@@ -1354,6 +1354,7 @@ pub struct Editor {
|
||||
notification_visible: bool,
|
||||
top_bar: ChromeLine,
|
||||
bottom_bar: ChromeLine,
|
||||
popup: PopupConfig,
|
||||
labels: BTreeMap<String, String>,
|
||||
battery_charge: Option<u8>,
|
||||
battery_label: String,
|
||||
@@ -1447,6 +1448,7 @@ impl Editor {
|
||||
notification_visible: true,
|
||||
top_bar: ChromeLine::top(),
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
popup: PopupConfig::default(),
|
||||
labels: BTreeMap::new(),
|
||||
battery_charge: None,
|
||||
battery_label: String::new(),
|
||||
@@ -3846,6 +3848,12 @@ impl Editor {
|
||||
pub fn bottom_bar_config(&self) -> &ChromeLine {
|
||||
&self.bottom_bar
|
||||
}
|
||||
pub fn popup_config(&self) -> &PopupConfig {
|
||||
&self.popup
|
||||
}
|
||||
pub fn set_popup_config(&mut self, popup: PopupConfig) {
|
||||
self.popup = popup;
|
||||
}
|
||||
pub fn label(&self, name: &str) -> &str {
|
||||
self.labels.get(name).map_or("", String::as_str)
|
||||
}
|
||||
|
||||
+89
-4
@@ -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}",
|
||||
|
||||
@@ -75,6 +75,46 @@ module Vamp
|
||||
end
|
||||
end
|
||||
|
||||
# Collects anchor options for the independently positioned command and search popups.
|
||||
class PopupConfig
|
||||
# @return [Hash{String => String}]
|
||||
attr_reader :command, :search
|
||||
|
||||
# @return [void]
|
||||
def initialize
|
||||
@command = {}
|
||||
@search = {}
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => String, Symbol}] anchor in the workspace's 3x3 grid
|
||||
# @return [Hash{String => String}]
|
||||
def command=(options)
|
||||
@command = options!(options)
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => String, Symbol}] anchor in the workspace's 3x3 grid
|
||||
# @return [Hash{String => String}]
|
||||
def search=(options)
|
||||
@search = options!(options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param options [Hash{String, Symbol => String, Symbol}]
|
||||
# @return [Hash{String => String}]
|
||||
def options!(options)
|
||||
Vamp.chrome_hash!(options).to_h do |option, value|
|
||||
raise ArgumentError, "unknown popup option: #{option}" unless option == "anchor"
|
||||
value = value.to_s if value.is_a?(Symbol)
|
||||
value = Vamp.string!(value, "popup anchor")
|
||||
unless %w[top_left top_center top_right center_left center center_right bottom_left bottom_center bottom_right].include?(value)
|
||||
raise ArgumentError, "unknown popup anchor: #{value}"
|
||||
end
|
||||
[option, value]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Collects and validates the Ruby startup configuration DSL.
|
||||
class Config
|
||||
# @return [Hash{String => Boolean, Hash{String, Symbol => Object}}] language presets/options
|
||||
@@ -83,6 +123,9 @@ module Vamp
|
||||
# @return [Vamp::BarConfig]
|
||||
attr_reader :bar
|
||||
|
||||
# @return [Vamp::PopupConfig]
|
||||
attr_reader :popup
|
||||
|
||||
# @return [void]
|
||||
def initialize
|
||||
@plugins = []
|
||||
@@ -94,6 +137,7 @@ module Vamp
|
||||
@file_explorer_view = "tree"
|
||||
@clipboard_mode = "system"
|
||||
@bar = BarConfig.new
|
||||
@popup = PopupConfig.new
|
||||
@labels = {}
|
||||
@diff_context_lines = 10
|
||||
@diff_expand_lines = 15
|
||||
@@ -193,6 +237,7 @@ module Vamp
|
||||
"leader" => @leader, "file_explorer_side" => @file_explorer_side, "file_picker_view" => @file_picker_view,
|
||||
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
||||
"top_bar" => @bar.top, "bottom_bar" => @bar.bottom, "labels" => @labels,
|
||||
"popup" => { "command" => @popup.command, "search" => @popup.search },
|
||||
"diff_context_lines" => @diff_context_lines, "diff_expand_lines" => @diff_expand_lines }
|
||||
end
|
||||
|
||||
|
||||
@@ -227,6 +227,7 @@ impl EditorSession {
|
||||
{
|
||||
notices.push(format!("chrome: {error}"));
|
||||
}
|
||||
editor.set_popup_config(config.popup);
|
||||
let mut plugins = BTreeMap::new();
|
||||
for configured in config.plugins {
|
||||
let path = configured.path;
|
||||
|
||||
+139
-2
@@ -33,7 +33,7 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
chrome::{ChromeLine, SegmentConfig},
|
||||
chrome::{ChromeLine, PopupAnchor, SegmentConfig},
|
||||
diff::{ChangedFile, DiffState, DisplayRow, FileStatus, Focus, PaneLayout, Phase, RowKind},
|
||||
editor::{
|
||||
Completion, Cursor, Editor, Hover, Input, Mode, Palette, Selection, StartupDiagnostic,
|
||||
@@ -1840,7 +1840,32 @@ fn draw_floating_input(frame: &mut Frame, editor: &Editor, area: Rect) -> Option
|
||||
}
|
||||
let width = area.width.min(72);
|
||||
let height = area.height.min(3);
|
||||
let panel = Rect::new(area.x, area.bottom() - height, width, height);
|
||||
let anchor = if editor.mode() == Mode::Search {
|
||||
editor.popup_config().search.anchor
|
||||
} else {
|
||||
editor.popup_config().command.anchor
|
||||
};
|
||||
let x = area.x
|
||||
+ match anchor {
|
||||
PopupAnchor::TopLeft | PopupAnchor::CenterLeft | PopupAnchor::BottomLeft => 0,
|
||||
PopupAnchor::TopCenter | PopupAnchor::Center | PopupAnchor::BottomCenter => {
|
||||
(area.width - width) / 2
|
||||
}
|
||||
PopupAnchor::TopRight | PopupAnchor::CenterRight | PopupAnchor::BottomRight => {
|
||||
area.width - width
|
||||
}
|
||||
};
|
||||
let y = area.y
|
||||
+ match anchor {
|
||||
PopupAnchor::TopLeft | PopupAnchor::TopCenter | PopupAnchor::TopRight => 0,
|
||||
PopupAnchor::CenterLeft | PopupAnchor::Center | PopupAnchor::CenterRight => {
|
||||
(area.height - height) / 2
|
||||
}
|
||||
PopupAnchor::BottomLeft | PopupAnchor::BottomCenter | PopupAnchor::BottomRight => {
|
||||
area.height - height
|
||||
}
|
||||
};
|
||||
let panel = Rect::new(x, y, width, height);
|
||||
let bordered = panel.width >= 4 && panel.height >= 3;
|
||||
let padding = u16::from(panel.width.saturating_sub(2 * u16::from(bordered)) >= 3);
|
||||
let mut block = Block::default()
|
||||
@@ -5638,6 +5663,118 @@ mod tests {
|
||||
assert_eq!(panel.bottom(), 12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_floating_inputs_paint_each_anchor_independently_inside_the_workspace() {
|
||||
let anchors = [
|
||||
(PopupAnchor::TopLeft, 0, 0),
|
||||
(PopupAnchor::TopCenter, 14, 0),
|
||||
(PopupAnchor::TopRight, 29, 0),
|
||||
(PopupAnchor::CenterLeft, 0, 5),
|
||||
(PopupAnchor::Center, 14, 5),
|
||||
(PopupAnchor::CenterRight, 29, 5),
|
||||
(PopupAnchor::BottomLeft, 0, 10),
|
||||
(PopupAnchor::BottomCenter, 14, 10),
|
||||
(PopupAnchor::BottomRight, 29, 10),
|
||||
];
|
||||
for (index, &(command_anchor, command_x, command_y)) in anchors.iter().enumerate() {
|
||||
let (search_anchor, search_x, search_y) = anchors[(index + 1) % anchors.len()];
|
||||
for (prefix, title, x, y) in [
|
||||
(':', "Command", command_x, command_y),
|
||||
('/', "Search", search_x, search_y),
|
||||
] {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
let mut popup = *editor.popup_config();
|
||||
popup.command.anchor = command_anchor;
|
||||
popup.search.anchor = search_anchor;
|
||||
editor.set_popup_config(popup);
|
||||
editor.set_message("hidden notification");
|
||||
editor.handle(Input::Char(prefix));
|
||||
for ch in "needle".chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
for (width, height) in [
|
||||
(101, 13),
|
||||
(12, 3),
|
||||
(3, 2),
|
||||
(2, 1),
|
||||
(1, 1),
|
||||
(0, 1),
|
||||
(1, 0),
|
||||
(0, 0),
|
||||
] {
|
||||
let workspace = Rect::new(5, 4, width, height);
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(120, 24)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
frame.render_widget(
|
||||
Paragraph::new(vec![Line::from(".".repeat(120)); 24]),
|
||||
frame.area(),
|
||||
);
|
||||
draw_overlays(frame, &editor, workspace, None, &mut panels);
|
||||
})
|
||||
.unwrap();
|
||||
assert!(panels.notification.is_none());
|
||||
if width == 0 || height == 0 {
|
||||
assert!(panels.input.is_none());
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(workspace.x, workspace.y)].symbol(),
|
||||
"."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let expected = if width == 101 {
|
||||
Rect::new(workspace.x + x, workspace.y + y, 72, 3)
|
||||
} else {
|
||||
workspace
|
||||
};
|
||||
let panel = panels.input.unwrap();
|
||||
assert_eq!(
|
||||
panel, expected,
|
||||
"{prefix} {command_anchor:?} {search_anchor:?}"
|
||||
);
|
||||
assert_eq!(panel.intersection(workspace), panel);
|
||||
let cursor = terminal.get_cursor_position().unwrap();
|
||||
assert!(panel.contains(cursor));
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(0, 0)].symbol(), ".");
|
||||
assert_eq!(buffer[(cursor.x, cursor.y)].symbol(), " ");
|
||||
if width >= 12 {
|
||||
assert_eq!(buffer[(panel.x, panel.y)].symbol(), "┌");
|
||||
assert_eq!(buffer[(panel.right() - 1, panel.y)].symbol(), "┐");
|
||||
assert_eq!(buffer[(panel.x, panel.bottom() - 1)].symbol(), "└");
|
||||
assert_eq!(
|
||||
buffer[(panel.right() - 1, panel.bottom() - 1)].symbol(),
|
||||
"┘"
|
||||
);
|
||||
assert!(
|
||||
pane_screen(&terminal, Rect::new(panel.x, panel.y, panel.width, 1))
|
||||
.contains(title)
|
||||
);
|
||||
assert_eq!(buffer[(panel.x + 1, panel.y + 1)].symbol(), " ");
|
||||
assert_eq!(buffer[(panel.x + 2, panel.y + 1)].symbol(), "n");
|
||||
assert_eq!(
|
||||
pane_screen(
|
||||
&terminal,
|
||||
Rect::new(panel.x + 1, panel.y + 1, panel.width - 2, 1)
|
||||
)
|
||||
.trim(),
|
||||
"needle"
|
||||
);
|
||||
assert_eq!(cursor, Position::new(panel.x + 8, panel.y + 1));
|
||||
} else {
|
||||
let cursor_x = u16::from(width > 1);
|
||||
assert_eq!(cursor, Position::new(panel.x + cursor_x, panel.y));
|
||||
if width == 2 {
|
||||
assert_eq!(buffer[(panel.x, panel.y)].symbol(), "e");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn floating_inputs_share_workspace_geometry_across_documents_splits_and_diffs() {
|
||||
for context in ["document", "split", "diff", "split-diff", "selector"] {
|
||||
|
||||
Reference in New Issue
Block a user