feat: configure interchangeable top and bottom bars
This commit is contained in:
+61
-32
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const STATUS_SEGMENTS: &[&str] = &[
|
||||
const DEFAULT_BOTTOM_SEGMENTS: &[&str] = &[
|
||||
"mode",
|
||||
"pending",
|
||||
"buffer",
|
||||
@@ -43,7 +43,7 @@ impl Default for SegmentConfig {
|
||||
}
|
||||
|
||||
impl ChromeLine {
|
||||
pub fn tabline() -> Self {
|
||||
pub fn top() -> Self {
|
||||
Self {
|
||||
left: vec!["tabs".into()],
|
||||
center: Vec::new(),
|
||||
@@ -52,15 +52,15 @@ impl ChromeLine {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn statusline() -> Self {
|
||||
pub fn bottom() -> Self {
|
||||
Self {
|
||||
left: STATUS_SEGMENTS[..STATUS_SEGMENTS.len() - 1]
|
||||
left: DEFAULT_BOTTOM_SEGMENTS[..DEFAULT_BOTTOM_SEGMENTS.len() - 1]
|
||||
.iter()
|
||||
.map(|name| (*name).into())
|
||||
.collect(),
|
||||
center: Vec::new(),
|
||||
right: vec!["position".into()],
|
||||
segments: STATUS_SEGMENTS
|
||||
segments: DEFAULT_BOTTOM_SEGMENTS
|
||||
.iter()
|
||||
.map(|name| ((*name).into(), default_segment(name)))
|
||||
.collect(),
|
||||
@@ -72,7 +72,7 @@ impl ChromeLine {
|
||||
self.segments.get(name).expect("validated chrome segment")
|
||||
}
|
||||
|
||||
pub fn validate(&mut self, tabline: bool) -> Result<(), String> {
|
||||
pub fn validate(&mut self) -> Result<(), String> {
|
||||
for name in self
|
||||
.left
|
||||
.iter()
|
||||
@@ -81,13 +81,6 @@ impl ChromeLine {
|
||||
.chain(self.segments.keys())
|
||||
{
|
||||
validate_name(name)?;
|
||||
if (tabline && STATUS_SEGMENTS.contains(&name.as_str())) || (!tabline && name == "tabs")
|
||||
{
|
||||
return Err(format!(
|
||||
"Chrome segment {name:?} is only available in the {}",
|
||||
if tabline { "statusline" } else { "tabline" }
|
||||
));
|
||||
}
|
||||
}
|
||||
for (name, segment) in &self.segments {
|
||||
if segment.padding > 64 {
|
||||
@@ -188,7 +181,7 @@ struct SegmentOptions {
|
||||
|
||||
fn deserialize_line<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
tabline: bool,
|
||||
mut line: ChromeLine,
|
||||
) -> Result<ChromeLine, D::Error> {
|
||||
// Decode through a value so explicit null is rejected rather than treated as an omitted option.
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
@@ -207,11 +200,6 @@ fn deserialize_line<'de, D: Deserializer<'de>>(
|
||||
}
|
||||
}
|
||||
let options: LineOptions = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
|
||||
let mut line = if tabline {
|
||||
ChromeLine::tabline()
|
||||
} else {
|
||||
ChromeLine::statusline()
|
||||
};
|
||||
if let Some(left) = options.left {
|
||||
line.left = left;
|
||||
}
|
||||
@@ -242,16 +230,16 @@ fn deserialize_line<'de, D: Deserializer<'de>>(
|
||||
segment.first_leading_edge = Some(edge);
|
||||
}
|
||||
}
|
||||
line.validate(tabline).map_err(serde::de::Error::custom)?;
|
||||
line.validate().map_err(serde::de::Error::custom)?;
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
pub fn tabline<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
|
||||
deserialize_line(deserializer, true)
|
||||
pub fn top<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
|
||||
deserialize_line(deserializer, ChromeLine::top())
|
||||
}
|
||||
|
||||
pub fn statusline<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
|
||||
deserialize_line(deserializer, false)
|
||||
pub fn bottom<'de, D: Deserializer<'de>>(deserializer: D) -> Result<ChromeLine, D::Error> {
|
||||
deserialize_line(deserializer, ChromeLine::bottom())
|
||||
}
|
||||
|
||||
pub fn labels<'de, D: Deserializer<'de>>(
|
||||
@@ -270,9 +258,9 @@ mod tests {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Lines {
|
||||
#[serde(deserialize_with = "tabline")]
|
||||
#[serde(deserialize_with = "top")]
|
||||
top: ChromeLine,
|
||||
#[serde(deserialize_with = "statusline")]
|
||||
#[serde(deserialize_with = "bottom")]
|
||||
bottom: ChromeLine,
|
||||
}
|
||||
|
||||
@@ -290,11 +278,51 @@ mod tests {
|
||||
assert_eq!(lines.bottom.segment("workspace"), &SegmentConfig::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_positions_accept_all_builtins_and_custom_styles() {
|
||||
let names = [
|
||||
"tabs",
|
||||
"mode",
|
||||
"pending",
|
||||
"buffer",
|
||||
"diagnostics",
|
||||
"path",
|
||||
"selections",
|
||||
"count",
|
||||
"language",
|
||||
"position",
|
||||
"workspace",
|
||||
];
|
||||
let options = serde_json::json!({
|
||||
"left": names,
|
||||
"center": ["language"],
|
||||
"right": ["tabs"],
|
||||
"tabs": {"title": "{index}:{name}:{path}{modified}", "padding": 0},
|
||||
"mode": {"leading_edge": "<", "trailing_edge": ">"},
|
||||
"workspace": {"first_leading_edge": ""}
|
||||
});
|
||||
let lines: Lines = serde_json::from_value(serde_json::json!({
|
||||
"top": options,
|
||||
"bottom": options,
|
||||
}))
|
||||
.unwrap();
|
||||
for line in [&lines.top, &lines.bottom] {
|
||||
assert_eq!(line.left, names);
|
||||
assert_eq!(line.center, ["language"]);
|
||||
assert_eq!(line.right, ["tabs"]);
|
||||
assert_eq!(line.segment("tabs").padding, 0);
|
||||
assert_eq!(line.segment("mode").leading_edge, "<");
|
||||
assert_eq!(
|
||||
line.segment("workspace").first_leading_edge.as_deref(),
|
||||
Some("")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_chrome_is_rejected_at_the_boundary() {
|
||||
for top in [
|
||||
for options in [
|
||||
serde_json::json!({"left": ["bad-name"]}),
|
||||
serde_json::json!({"left": ["mode"]}),
|
||||
serde_json::json!({"left": null}),
|
||||
serde_json::json!({"tabs": {"padding": 65}}),
|
||||
serde_json::json!({"tabs": {"padding": null}}),
|
||||
@@ -304,10 +332,11 @@ mod tests {
|
||||
serde_json::json!({"tabs": {"title": "name}"}}),
|
||||
serde_json::json!({"tabs": {"colour": "red"}}),
|
||||
] {
|
||||
assert!(
|
||||
serde_json::from_value::<Lines>(serde_json::json!({"top": top, "bottom": {}}))
|
||||
.is_err()
|
||||
);
|
||||
for position in ["top", "bottom"] {
|
||||
let mut frame = serde_json::json!({"top": {}, "bottom": {}});
|
||||
frame[position] = options.clone();
|
||||
assert!(serde_json::from_value::<Lines>(frame).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
-26
@@ -1351,8 +1351,8 @@ pub struct Editor {
|
||||
owned_system_clipboard: Option<(String, Vec<String>, bool)>,
|
||||
last_edit: Option<EditRecipe>,
|
||||
message: String,
|
||||
tabline: ChromeLine,
|
||||
statusline: ChromeLine,
|
||||
top_bar: ChromeLine,
|
||||
bottom_bar: ChromeLine,
|
||||
labels: BTreeMap<String, String>,
|
||||
quit: bool,
|
||||
capabilities: Vec<String>,
|
||||
@@ -1439,8 +1439,8 @@ impl Editor {
|
||||
owned_system_clipboard: None,
|
||||
last_edit: None,
|
||||
message: String::new(),
|
||||
tabline: ChromeLine::tabline(),
|
||||
statusline: ChromeLine::statusline(),
|
||||
top_bar: ChromeLine::top(),
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
labels: BTreeMap::new(),
|
||||
quit: false,
|
||||
capabilities,
|
||||
@@ -3831,11 +3831,11 @@ impl Editor {
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
pub fn tabline_config(&self) -> &ChromeLine {
|
||||
&self.tabline
|
||||
pub fn top_bar_config(&self) -> &ChromeLine {
|
||||
&self.top_bar
|
||||
}
|
||||
pub fn statusline_config(&self) -> &ChromeLine {
|
||||
&self.statusline
|
||||
pub fn bottom_bar_config(&self) -> &ChromeLine {
|
||||
&self.bottom_bar
|
||||
}
|
||||
pub fn label(&self, name: &str) -> &str {
|
||||
self.labels.get(name).map_or("", String::as_str)
|
||||
@@ -3881,12 +3881,12 @@ impl Editor {
|
||||
|
||||
pub fn set_chrome_config(
|
||||
&mut self,
|
||||
mut tabline: ChromeLine,
|
||||
mut statusline: ChromeLine,
|
||||
mut top: ChromeLine,
|
||||
mut bottom: ChromeLine,
|
||||
labels: BTreeMap<String, String>,
|
||||
) -> Result<(), String> {
|
||||
tabline.validate(true)?;
|
||||
statusline.validate(false)?;
|
||||
top.validate()?;
|
||||
bottom.validate()?;
|
||||
let mut total = 0usize;
|
||||
for (name, text) in &labels {
|
||||
validate_label(name, text)?;
|
||||
@@ -3898,8 +3898,8 @@ impl Editor {
|
||||
return Err("Labels exceed 4 MiB".into());
|
||||
}
|
||||
}
|
||||
self.tabline = tabline;
|
||||
self.statusline = statusline;
|
||||
self.top_bar = top;
|
||||
self.bottom_bar = bottom;
|
||||
self.labels = labels
|
||||
.into_iter()
|
||||
.map(|(name, text)| (name, sanitize_label(text)))
|
||||
@@ -17749,9 +17749,11 @@ mod tests {
|
||||
fn chrome_installation_and_invalid_label_batches_are_atomic() {
|
||||
let mut editor = editor();
|
||||
editor.set_label("battery".into(), "ready".into()).unwrap();
|
||||
let mut top = ChromeLine::tabline();
|
||||
let mut top = ChromeLine::top();
|
||||
top.left = vec!["mode".into(), "language".into()];
|
||||
top.center.push("workspace".into());
|
||||
let bottom = ChromeLine::statusline();
|
||||
let mut bottom = ChromeLine::bottom();
|
||||
bottom.left = vec!["tabs".into()];
|
||||
editor
|
||||
.set_chrome_config(
|
||||
top.clone(),
|
||||
@@ -17764,18 +17766,34 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(editor.label("workspace"), "tools�");
|
||||
assert_eq!(
|
||||
editor.tabline_config().segment("workspace"),
|
||||
editor.top_bar_config().segment("workspace"),
|
||||
&crate::chrome::SegmentConfig::default()
|
||||
);
|
||||
let installed = editor.tabline_config().clone();
|
||||
top.segments.get_mut("tabs").unwrap().title = "{missing}".into();
|
||||
assert!(
|
||||
editor
|
||||
.set_chrome_config(top, bottom, BTreeMap::new())
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(editor.tabline_config(), &installed);
|
||||
assert_eq!(editor.label("battery"), "ready");
|
||||
let installed_top = editor.top_bar_config().clone();
|
||||
let installed_bottom = editor.bottom_bar_config().clone();
|
||||
assert_eq!(installed_top.left, ["mode", "language"]);
|
||||
assert_eq!(installed_bottom.left, ["tabs"]);
|
||||
top.right.push("uninstalled".into());
|
||||
bottom.center.push("uninstalled".into());
|
||||
let mut invalid_top = top.clone();
|
||||
invalid_top.segments.get_mut("tabs").unwrap().title = "{missing}".into();
|
||||
let mut invalid_bottom = bottom.clone();
|
||||
invalid_bottom.segments.get_mut("mode").unwrap().padding = 65;
|
||||
for (top, bottom, labels) in [
|
||||
(invalid_top, bottom.clone(), BTreeMap::new()),
|
||||
(top.clone(), invalid_bottom, BTreeMap::new()),
|
||||
(
|
||||
top,
|
||||
bottom,
|
||||
BTreeMap::from([("bad-name".into(), "invalid".into())]),
|
||||
),
|
||||
] {
|
||||
assert!(editor.set_chrome_config(top, bottom, labels).is_err());
|
||||
assert_eq!(editor.top_bar_config(), &installed_top);
|
||||
assert_eq!(editor.bottom_bar_config(), &installed_bottom);
|
||||
assert_eq!(editor.label("battery"), "ready");
|
||||
assert_eq!(editor.label("workspace"), "tools�");
|
||||
}
|
||||
assert!(
|
||||
editor
|
||||
.apply(vec![
|
||||
|
||||
+82
-60
@@ -38,8 +38,8 @@ pub struct Config {
|
||||
pub file_picker_view: String,
|
||||
pub file_explorer_view: String,
|
||||
pub clipboard_mode: String,
|
||||
pub tabline: ChromeLine,
|
||||
pub statusline: ChromeLine,
|
||||
pub top_bar: ChromeLine,
|
||||
pub bottom_bar: ChromeLine,
|
||||
pub labels: BTreeMap<String, String>,
|
||||
pub diff_context_lines: usize,
|
||||
pub diff_expand_lines: usize,
|
||||
@@ -77,8 +77,8 @@ impl Default for Config {
|
||||
file_picker_view: "tree".into(),
|
||||
file_explorer_view: "tree".into(),
|
||||
clipboard_mode: "system".into(),
|
||||
tabline: ChromeLine::tabline(),
|
||||
statusline: ChromeLine::statusline(),
|
||||
top_bar: ChromeLine::top(),
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
labels: BTreeMap::new(),
|
||||
diff_context_lines: 10,
|
||||
diff_expand_lines: 15,
|
||||
@@ -102,8 +102,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
file_picker_view,
|
||||
file_explorer_view,
|
||||
clipboard_mode,
|
||||
tabline,
|
||||
statusline,
|
||||
top_bar,
|
||||
bottom_bar,
|
||||
labels,
|
||||
diff_context_lines,
|
||||
diff_expand_lines,
|
||||
@@ -129,8 +129,8 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
config.file_picker_view = file_picker_view;
|
||||
config.file_explorer_view = file_explorer_view;
|
||||
config.clipboard_mode = clipboard_mode;
|
||||
config.tabline = tabline;
|
||||
config.statusline = statusline;
|
||||
config.top_bar = top_bar;
|
||||
config.bottom_bar = bottom_bar;
|
||||
config.labels = labels;
|
||||
config.diff_context_lines = diff_context_lines;
|
||||
config.diff_expand_lines = diff_expand_lines;
|
||||
@@ -450,10 +450,10 @@ enum Response {
|
||||
file_picker_view: String,
|
||||
file_explorer_view: String,
|
||||
clipboard_mode: String,
|
||||
#[serde(deserialize_with = "crate::chrome::tabline")]
|
||||
tabline: ChromeLine,
|
||||
#[serde(deserialize_with = "crate::chrome::statusline")]
|
||||
statusline: ChromeLine,
|
||||
#[serde(deserialize_with = "crate::chrome::top")]
|
||||
top_bar: ChromeLine,
|
||||
#[serde(deserialize_with = "crate::chrome::bottom")]
|
||||
bottom_bar: ChromeLine,
|
||||
#[serde(deserialize_with = "crate::chrome::labels")]
|
||||
labels: BTreeMap<String, String>,
|
||||
#[serde(deserialize_with = "diff_context_lines")]
|
||||
@@ -1181,9 +1181,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","tabline":{},"statusline":{},"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","tabline":{},"statusline":{},"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","tabline":{},"statusline":{},"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":{},"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}"#,
|
||||
] {
|
||||
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
||||
}
|
||||
@@ -1196,69 +1196,91 @@ end
|
||||
"config.rb",
|
||||
r#"
|
||||
Vamp.configure do |config|
|
||||
config.tabline = {left: [:tabs], center: ["workspace"], right: [:battery],
|
||||
config.bar.top = {left: [:tabs], center: ["workspace"], right: [:battery],
|
||||
tabs: {title: "T{index}:{path}{modified}", padding: 0, first_leading_edge: ""},
|
||||
battery: {leading_edge: "界", trailing_edge: ""}}
|
||||
config.statusline = {"left" => [], center: [:workspace]}
|
||||
config.bar.bottom = {"left" => [], center: [:workspace]}
|
||||
config.labels = {battery: "bat 38%", "workspace" => "界 e\u0301 👩🚀\t\n\e"}
|
||||
end
|
||||
"#,
|
||||
);
|
||||
let loaded = load_config(&path).unwrap();
|
||||
assert_eq!(loaded.tabline.left, ["tabs"]);
|
||||
assert_eq!(loaded.tabline.center, ["workspace"]);
|
||||
assert_eq!(loaded.tabline.right, ["battery"]);
|
||||
assert_eq!(loaded.top_bar.left, ["tabs"]);
|
||||
assert_eq!(loaded.top_bar.center, ["workspace"]);
|
||||
assert_eq!(loaded.top_bar.right, ["battery"]);
|
||||
assert_eq!(
|
||||
loaded.tabline.segment("tabs").title,
|
||||
loaded.top_bar.segment("tabs").title,
|
||||
"T{index}:{path}{modified}"
|
||||
);
|
||||
assert_eq!(loaded.tabline.segment("tabs").padding, 0);
|
||||
assert_eq!(loaded.tabline.segment("tabs").trailing_edge, "▓▒░");
|
||||
assert_eq!(loaded.tabline.segment("battery").leading_edge, "界");
|
||||
assert_eq!(loaded.top_bar.segment("tabs").padding, 0);
|
||||
assert_eq!(loaded.top_bar.segment("tabs").trailing_edge, "▓▒░");
|
||||
assert_eq!(loaded.top_bar.segment("battery").leading_edge, "界");
|
||||
assert_eq!(
|
||||
loaded.tabline.segment("workspace"),
|
||||
loaded.top_bar.segment("workspace"),
|
||||
&crate::chrome::SegmentConfig::default()
|
||||
);
|
||||
assert!(loaded.statusline.left.is_empty());
|
||||
assert_eq!(loaded.statusline.right, ["position"]);
|
||||
assert!(loaded.bottom_bar.left.is_empty());
|
||||
assert_eq!(loaded.bottom_bar.right, ["position"]);
|
||||
assert_eq!(loaded.labels["battery"], "bat 38%");
|
||||
assert_eq!(loaded.labels["workspace"], "界 e\u{301} 👩🚀\t\n\u{1b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bar_namespace_is_stable_and_accepts_swapped_builtins() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
r#"
|
||||
Vamp.configure do |config|
|
||||
bar = config.bar
|
||||
raise "bar namespace must be stable" unless bar.equal?(config.bar)
|
||||
bar.top = {left: [:mode, :language], right: [], mode: {padding: 0}}
|
||||
config.bar.bottom = {left: [:tabs], right: [], tabs: {title: "{name}{modified}"}}
|
||||
end
|
||||
"#,
|
||||
);
|
||||
let loaded = load_config(&path).unwrap();
|
||||
assert_eq!(loaded.top_bar.left, ["mode", "language"]);
|
||||
assert_eq!(loaded.top_bar.segment("mode").padding, 0);
|
||||
assert_eq!(loaded.top_bar.segment("language").padding, 1);
|
||||
assert_eq!(loaded.bottom_bar.left, ["tabs"]);
|
||||
assert_eq!(loaded.bottom_bar.segment("tabs").title, "{name}{modified}");
|
||||
|
||||
for setter in ["bar", "tabline", "statusline"] {
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
&format!("Vamp.configure {{ |config| config.{setter} = {{}} }}"),
|
||||
);
|
||||
let error = load_config(&path).unwrap_err();
|
||||
assert!(error.contains("undefined method"), "{setter}: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chrome_configuration_rejects_invalid_types_names_and_templates() {
|
||||
let fixture = Fixture::new();
|
||||
for (setting, value, expected) in [
|
||||
("tabline", "[]", "must be a Hash"),
|
||||
("tabline", "{left: :tabs}", "must be an Array"),
|
||||
("tabline", "{left: [7]}", "must be a String"),
|
||||
("tabline", "{left: [:bad_name!]}", "must match"),
|
||||
("bar.top", "[]", "must be a Hash"),
|
||||
("bar.bottom", "[]", "must be a Hash"),
|
||||
("bar.top", "{left: :tabs}", "must be an Array"),
|
||||
("bar.top", "{left: [7]}", "must be a String"),
|
||||
("bar.top", "{left: [:bad_name!]}", "must match"),
|
||||
(
|
||||
"tabline",
|
||||
"{left: [:mode]}",
|
||||
"only available in the statusline",
|
||||
),
|
||||
(
|
||||
"statusline",
|
||||
"{right: [:tabs]}",
|
||||
"only available in the tabline",
|
||||
),
|
||||
(
|
||||
"tabline",
|
||||
"bar.bottom",
|
||||
"{tabs: {title: '{unknown}'}}",
|
||||
"unknown title placeholder",
|
||||
),
|
||||
("tabline", r#"{tabs: {leading_edge: "\n"}}"#, "single-line"),
|
||||
("tabline", "{tabs: {padding: 65}}", "between 0 and 64"),
|
||||
("tabline", "{tabs: {padding: '1'}}", "must be an Integer"),
|
||||
("tabline", "{tabs: {title: nil}}", "must be a String"),
|
||||
("bar.top", r#"{mode: {leading_edge: "\n"}}"#, "single-line"),
|
||||
("bar.top", "{language: {padding: 65}}", "between 0 and 64"),
|
||||
("bar.bottom", "{tabs: {padding: '1'}}", "must be an Integer"),
|
||||
("bar.top", "{mode: {title: nil}}", "must be a String"),
|
||||
(
|
||||
"tabline",
|
||||
"bar.bottom",
|
||||
"{tabs: {color: 'red'}}",
|
||||
"unknown chrome style option",
|
||||
),
|
||||
(
|
||||
"tabline",
|
||||
"bar.top",
|
||||
"{left: [], 'left' => []}",
|
||||
"duplicate chrome key",
|
||||
),
|
||||
@@ -1310,7 +1332,7 @@ end
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"tabline": {}, "statusline": {}, "labels": {},
|
||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7
|
||||
});
|
||||
for setting in ["diff_context_lines", "diff_expand_lines"] {
|
||||
@@ -1723,7 +1745,7 @@ end"#,
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"tabline": {}, "statusline": {}, "labels": {},
|
||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15
|
||||
});
|
||||
let Response::Config { lsp, .. } =
|
||||
@@ -1806,18 +1828,18 @@ end"#,
|
||||
for mutation in [
|
||||
"return '['",
|
||||
"frame.delete('lsp')",
|
||||
"frame.delete('tabline')",
|
||||
"frame.delete('statusline')",
|
||||
"frame.delete('top_bar')",
|
||||
"frame.delete('bottom_bar')",
|
||||
"frame.delete('labels')",
|
||||
"frame['tabline'] = 7",
|
||||
"frame['statusline'] = []",
|
||||
"frame['top_bar'] = 7",
|
||||
"frame['bottom_bar'] = []",
|
||||
"frame['labels'] = {'battery' => 7}",
|
||||
"frame['tabline'] = {'tabs' => {'title' => '{unknown}'}}",
|
||||
"frame['tabline'] = {'tabs' => {'leading_edge' => \"\\n\"}}",
|
||||
"frame['tabline'] = {'left' => nil}",
|
||||
"frame['tabline'] = {'left' => ['bad-name']}",
|
||||
"frame['tabline'] = {'tabs' => {'padding' => '1'}}",
|
||||
"frame['tabline'] = {'tabs' => {'color' => 'red'}}",
|
||||
"frame['bottom_bar'] = {'tabs' => {'title' => '{unknown}'}}",
|
||||
"frame['top_bar'] = {'mode' => {'leading_edge' => \"\\n\"}}",
|
||||
"frame['top_bar'] = {'left' => nil}",
|
||||
"frame['bottom_bar'] = {'left' => ['bad-name']}",
|
||||
"frame['bottom_bar'] = {'tabs' => {'padding' => '1'}}",
|
||||
"frame['top_bar'] = {'language' => {'color' => 'red'}}",
|
||||
"frame['labels'] = {'bad-name' => ''}",
|
||||
"frame['leader'] = 7",
|
||||
"frame['unexpected'] = true",
|
||||
|
||||
+69
-56
@@ -12,11 +12,77 @@ module Vamp
|
||||
name
|
||||
end
|
||||
|
||||
# @param value [Hash{String, Symbol => Object}]
|
||||
# @return [Hash{String => Object}]
|
||||
def self.chrome_hash!(value)
|
||||
raise TypeError, "chrome options must be a Hash" unless value.is_a?(Hash)
|
||||
value.each_with_object({}) do |(key, item), result|
|
||||
key = key.to_s if key.is_a?(Symbol)
|
||||
key = string!(key, "chrome key")
|
||||
raise ArgumentError, "duplicate chrome key: #{key}" if result.key?(key)
|
||||
result[key] = item
|
||||
end
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => Object}]
|
||||
# @return [Hash{String => Object}]
|
||||
def self.chrome_line!(options)
|
||||
chrome_hash!(options).to_h do |name, value|
|
||||
if %w[left center right].include?(name)
|
||||
raise TypeError, "#{name} must be an Array" unless value.is_a?(Array)
|
||||
[name, value.map { |item| chrome_name!(item) }]
|
||||
else
|
||||
chrome_name!(name)
|
||||
style = chrome_hash!(value).to_h do |option, item|
|
||||
case option
|
||||
when "padding"
|
||||
unless item.is_a?(Integer) && item.between?(0, 64)
|
||||
raise ArgumentError, "padding must be an Integer between 0 and 64"
|
||||
end
|
||||
when "title", "leading_edge", "trailing_edge", "first_leading_edge"
|
||||
item = string!(item, option)
|
||||
else
|
||||
raise ArgumentError, "unknown chrome style option: #{option}"
|
||||
end
|
||||
[option, item]
|
||||
end
|
||||
[name, style]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Collects options for the independently configured top and bottom bars.
|
||||
class BarConfig
|
||||
# @return [Hash{String => Object}]
|
||||
attr_reader :top, :bottom
|
||||
|
||||
# @return [void]
|
||||
def initialize
|
||||
@top = {}
|
||||
@bottom = {}
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => Object}]
|
||||
# @return [Hash{String => Object}]
|
||||
def top=(options)
|
||||
@top = Vamp.chrome_line!(options)
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => Object}]
|
||||
# @return [Hash{String => Object}]
|
||||
def bottom=(options)
|
||||
@bottom = Vamp.chrome_line!(options)
|
||||
end
|
||||
end
|
||||
|
||||
# Collects and validates the Ruby startup configuration DSL.
|
||||
class Config
|
||||
# @return [Hash{String => Boolean, Hash{String, Symbol => Object}}] language presets/options
|
||||
attr_reader :lsp
|
||||
|
||||
# @return [Vamp::BarConfig]
|
||||
attr_reader :bar
|
||||
|
||||
# @return [void]
|
||||
def initialize
|
||||
@plugins = []
|
||||
@@ -27,8 +93,7 @@ module Vamp
|
||||
@file_picker_view = "tree"
|
||||
@file_explorer_view = "tree"
|
||||
@clipboard_mode = "system"
|
||||
@tabline = {}
|
||||
@statusline = {}
|
||||
@bar = BarConfig.new
|
||||
@labels = {}
|
||||
@diff_context_lines = 10
|
||||
@diff_expand_lines = 15
|
||||
@@ -76,22 +141,10 @@ module Vamp
|
||||
@clipboard_mode = nonempty_string!(mode, "clipboard mode")
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => Object}]
|
||||
# @return [Hash]
|
||||
def tabline=(options)
|
||||
@tabline = chrome_line!(options)
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => Object}]
|
||||
# @return [Hash]
|
||||
def statusline=(options)
|
||||
@statusline = chrome_line!(options)
|
||||
end
|
||||
|
||||
# @param values [Hash{String, Symbol => String}]
|
||||
# @return [Hash]
|
||||
def labels=(values)
|
||||
@labels = chrome_hash!(values).to_h do |name, text|
|
||||
@labels = Vamp.chrome_hash!(values).to_h do |name, text|
|
||||
[Vamp.chrome_name!(name), Vamp.string!(text, "label text")]
|
||||
end
|
||||
end
|
||||
@@ -139,51 +192,11 @@ module Vamp
|
||||
{ "version" => 1, "type" => "config", "plugins" => @plugins, "lsp" => lsp_options, "bindings" => @bindings,
|
||||
"leader" => @leader, "file_explorer_side" => @file_explorer_side, "file_picker_view" => @file_picker_view,
|
||||
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
||||
"tabline" => @tabline, "statusline" => @statusline, "labels" => @labels,
|
||||
"top_bar" => @bar.top, "bottom_bar" => @bar.bottom, "labels" => @labels,
|
||||
"diff_context_lines" => @diff_context_lines, "diff_expand_lines" => @diff_expand_lines }
|
||||
end
|
||||
|
||||
private
|
||||
# @param value [Hash{String, Symbol => Object}]
|
||||
# @return [Hash{String => Object}]
|
||||
def chrome_hash!(value)
|
||||
raise TypeError, "chrome options must be a Hash" unless value.is_a?(Hash)
|
||||
value.each_with_object({}) do |(key, item), result|
|
||||
key = key.to_s if key.is_a?(Symbol)
|
||||
key = Vamp.string!(key, "chrome key")
|
||||
raise ArgumentError, "duplicate chrome key: #{key}" if result.key?(key)
|
||||
result[key] = item
|
||||
end
|
||||
end
|
||||
|
||||
# @param options [Hash{String, Symbol => Object}]
|
||||
# @return [Hash{String => Object}]
|
||||
def chrome_line!(options)
|
||||
chrome_hash!(options).to_h do |name, value|
|
||||
if %w[left center right].include?(name)
|
||||
raise TypeError, "#{name} must be an Array" unless value.is_a?(Array)
|
||||
[name, value.map { |item| Vamp.chrome_name!(item) }]
|
||||
else
|
||||
Vamp.chrome_name!(name)
|
||||
style = chrome_hash!(value).to_h do |option, item|
|
||||
case option
|
||||
when "padding"
|
||||
unless item.is_a?(Integer) && item.between?(0, 64)
|
||||
raise ArgumentError, "padding must be an Integer between 0 and 64"
|
||||
end
|
||||
when "title", "leading_edge", "trailing_edge", "first_leading_edge"
|
||||
item = Vamp.string!(item, option)
|
||||
else
|
||||
raise ArgumentError, "unknown chrome style option: #{option}"
|
||||
end
|
||||
[option, item]
|
||||
end
|
||||
[name, style]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# @param value [Integer]
|
||||
# @param label [String]
|
||||
# @param minimum [Integer]
|
||||
|
||||
+7
-5
@@ -221,7 +221,7 @@ impl EditorSession {
|
||||
}
|
||||
editor.set_diff_context_options(config.diff_context_lines, config.diff_expand_lines);
|
||||
if let Err(error) =
|
||||
editor.set_chrome_config(config.tabline, config.statusline, config.labels)
|
||||
editor.set_chrome_config(config.top_bar, config.bottom_bar, config.labels)
|
||||
{
|
||||
notices.push(format!("chrome: {error}"));
|
||||
}
|
||||
@@ -3016,16 +3016,18 @@ end
|
||||
let session = fixture.open(
|
||||
r#"
|
||||
Vamp.configure do |config|
|
||||
config.tabline = {right: [:battery]}
|
||||
config.statusline = {center: [:workspace]}
|
||||
config.bar.top = {left: [:mode, :language], right: [:battery]}
|
||||
config.bar.bottom = {left: [:tabs], center: [:workspace]}
|
||||
config.labels = {battery: "bat 38%", workspace: "tools\n"}
|
||||
end
|
||||
"#,
|
||||
);
|
||||
assert_eq!(session.editor.label("battery"), "bat 38%");
|
||||
assert_eq!(session.editor.label("workspace"), "tools�");
|
||||
assert_eq!(session.editor.tabline_config().right, ["battery"]);
|
||||
assert_eq!(session.editor.statusline_config().center, ["workspace"]);
|
||||
assert_eq!(session.editor.top_bar_config().left, ["mode", "language"]);
|
||||
assert_eq!(session.editor.top_bar_config().right, ["battery"]);
|
||||
assert_eq!(session.editor.bottom_bar_config().left, ["tabs"]);
|
||||
assert_eq!(session.editor.bottom_bar_config().center, ["workspace"]);
|
||||
assert_eq!(session.editor.lines(), ["source"]);
|
||||
assert_eq!(session.editor.text_revision(), 0);
|
||||
assert!(!session.editor.dirty());
|
||||
|
||||
+478
-135
@@ -406,15 +406,9 @@ impl BufferViewports {
|
||||
self.parked
|
||||
.retain(|id, _| editor.pane_buffer_id(*id).is_some());
|
||||
self.syntax.retain(|id, _| editor.has_buffer(*id));
|
||||
let area = frame.area();
|
||||
let area = draw_tab_bar(frame, editor, area);
|
||||
let syntax = self.syntax.entry(editor.buffer_id()).or_default();
|
||||
let [full_workspace, prompt] = draw_bars(frame, editor, frame.area(), syntax);
|
||||
let multiple = editor.pane_layout().len() > 1;
|
||||
let chrome = if !multiple && editor.diff().is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(document_layout(area, editor))
|
||||
};
|
||||
let full_workspace = chrome.map_or(area, |areas| areas[0]);
|
||||
let diff_header = editor
|
||||
.diff()
|
||||
.filter(|diff| !multiple && diff.phase == Phase::View)
|
||||
@@ -452,6 +446,10 @@ impl BufferViewports {
|
||||
.visit_rects(pane_rect(workspace), |id, area| {
|
||||
self.rects.push((id, terminal_rect(area)));
|
||||
});
|
||||
// An active prompt still needs its focused render pass when no document row fits.
|
||||
if self.rects.is_empty() && prompt.height > 0 {
|
||||
self.rects.push((focused, workspace));
|
||||
}
|
||||
// Paint focused overlays last, so an adjacent leaf cannot erase them.
|
||||
for active_pass in [false, true] {
|
||||
for index in 0..self.rects.len() {
|
||||
@@ -474,12 +472,11 @@ impl BufferViewports {
|
||||
frame.render_widget(block, leaf);
|
||||
leaf = inner;
|
||||
}
|
||||
let status = chrome
|
||||
.filter(|_| id == focused)
|
||||
.map_or(Rect::default(), |a| a[1]);
|
||||
let prompt = chrome
|
||||
.filter(|_| id == focused)
|
||||
.map_or(Rect::default(), |a| a[2]);
|
||||
let prompt = if id == focused {
|
||||
prompt
|
||||
} else {
|
||||
Rect::default()
|
||||
};
|
||||
editor.with_render_pane(id, |view| {
|
||||
self.activate_document(view.buffer_id());
|
||||
self.documents.retain(|id, _| view.has_buffer(*id));
|
||||
@@ -492,8 +489,8 @@ impl BufferViewports {
|
||||
draw_document(
|
||||
frame,
|
||||
view,
|
||||
[leaf, status, prompt],
|
||||
area,
|
||||
[leaf, prompt],
|
||||
full_workspace,
|
||||
top,
|
||||
left,
|
||||
syntax,
|
||||
@@ -544,14 +541,20 @@ impl BufferViewports {
|
||||
draw_diff(
|
||||
frame,
|
||||
diff,
|
||||
area,
|
||||
full_workspace,
|
||||
editor.selection_navigation_hint(),
|
||||
editor.file_navigation_hint(),
|
||||
&mut self.panels,
|
||||
Some(editor),
|
||||
);
|
||||
} else {
|
||||
draw_overlays(frame, editor, area, self.panels.caret, &mut self.panels);
|
||||
draw_overlays(
|
||||
frame,
|
||||
editor,
|
||||
full_workspace,
|
||||
self.panels.caret,
|
||||
&mut self.panels,
|
||||
);
|
||||
}
|
||||
self.diff_visible = self
|
||||
.panels
|
||||
@@ -1071,7 +1074,7 @@ fn chrome_items<'a>(
|
||||
editor: &'a Editor,
|
||||
line: &'a ChromeLine,
|
||||
names: &[String],
|
||||
syntax: Option<&SyntaxCache>,
|
||||
syntax: &SyntaxCache,
|
||||
) -> Vec<ChromeItem<'a>> {
|
||||
let mut items = Vec::new();
|
||||
for name in names {
|
||||
@@ -1100,31 +1103,28 @@ fn chrome_items<'a>(
|
||||
continue;
|
||||
}
|
||||
let mut text = match name.as_str() {
|
||||
"mode" if syntax.is_some() => Cow::Owned(if editor.visual_linewise() {
|
||||
"mode" => Cow::Owned(if editor.visual_linewise() {
|
||||
"VISUAL LINE".into()
|
||||
} else {
|
||||
editor.mode().to_string()
|
||||
}),
|
||||
"path" if syntax.is_some() => Cow::Owned(chrome_title(
|
||||
"path" => Cow::Owned(chrome_title(
|
||||
"{path}{modified}",
|
||||
editor.tab_index() + 1,
|
||||
editor.path(),
|
||||
editor.dirty(),
|
||||
)),
|
||||
"position" if syntax.is_some() => Cow::Owned(format!(
|
||||
"position" => Cow::Owned(format!(
|
||||
"{}:{}",
|
||||
editor.cursor().row + 1,
|
||||
editor.cursor().column + 1
|
||||
)),
|
||||
"language" if syntax.is_some() => {
|
||||
let syntax = syntax.unwrap();
|
||||
Cow::Owned(if let Some(error) = syntax.error() {
|
||||
format!("syntax: {error}")
|
||||
} else {
|
||||
syntax.language().unwrap_or_default().to_owned()
|
||||
})
|
||||
}
|
||||
"diagnostics" if syntax.is_some() => {
|
||||
"language" => Cow::Owned(if let Some(error) = syntax.error() {
|
||||
format!("syntax: {error}")
|
||||
} else {
|
||||
syntax.language().unwrap_or_default().to_owned()
|
||||
}),
|
||||
"diagnostics" => {
|
||||
let mut counts = [0; 4];
|
||||
for diagnostic in editor.diagnostics() {
|
||||
counts[diagnostic_severity(diagnostic.severity).0] += 1;
|
||||
@@ -1140,18 +1140,18 @@ fn chrome_items<'a>(
|
||||
}
|
||||
Cow::Owned(text)
|
||||
}
|
||||
"selections" if syntax.is_some() => Cow::Owned(if editor.selection_count() > 1 {
|
||||
"selections" => Cow::Owned(if editor.selection_count() > 1 {
|
||||
format!("{} selections", editor.selection_count())
|
||||
} else {
|
||||
String::new()
|
||||
}),
|
||||
"count" if syntax.is_some() => Cow::Owned(
|
||||
"count" => Cow::Owned(
|
||||
editor
|
||||
.count()
|
||||
.map(|count| format!("count: {count}"))
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
"pending" if syntax.is_some() => {
|
||||
"pending" => {
|
||||
let mut text = editor.sequence_pending().unwrap_or_default().to_owned();
|
||||
for pending in [
|
||||
editor.g_pending().then_some('g'),
|
||||
@@ -1168,7 +1168,7 @@ fn chrome_items<'a>(
|
||||
}
|
||||
Cow::Owned(text)
|
||||
}
|
||||
"buffer" if syntax.is_some() => Cow::Owned(if editor.buffer_count() > 1 {
|
||||
"buffer" => Cow::Owned(if editor.buffer_count() > 1 {
|
||||
format!("[buffer {}/{}]", editor.buffer_id(), editor.buffer_count())
|
||||
} else {
|
||||
String::new()
|
||||
@@ -1348,47 +1348,69 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec<ChromeItem<'_>>; 3], area: Re
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
|
||||
if area.height == 0 {
|
||||
return area;
|
||||
}
|
||||
let line = editor.tabline_config();
|
||||
let anchors = [&line.left, &line.center, &line.right];
|
||||
if !anchors.iter().flat_map(|names| names.iter()).any(|name| {
|
||||
if name == "tabs" {
|
||||
editor.tab_count() > 1
|
||||
} else {
|
||||
!editor.label(name).is_empty()
|
||||
}
|
||||
}) {
|
||||
return area;
|
||||
}
|
||||
let groups = anchors.map(|names| chrome_items(editor, line, names, None));
|
||||
if !groups.iter().flatten().any(|item| item.tab.is_none())
|
||||
&& !(editor.tab_count() > 1 && groups.iter().any(|group| !group.is_empty()))
|
||||
{
|
||||
return area;
|
||||
}
|
||||
draw_chrome(frame, groups, Rect::new(area.x, area.y, area.width, 1));
|
||||
Rect::new(
|
||||
area.x,
|
||||
area.y.saturating_add(1),
|
||||
area.width,
|
||||
area.height - 1,
|
||||
)
|
||||
fn chrome_groups<'a>(
|
||||
editor: &'a Editor,
|
||||
line: &'a ChromeLine,
|
||||
syntax: &SyntaxCache,
|
||||
) -> [Vec<ChromeItem<'a>>; 3] {
|
||||
[&line.left, &line.center, &line.right].map(|names| chrome_items(editor, line, names, syntax))
|
||||
}
|
||||
|
||||
fn document_layout(area: Rect, editor: &Editor) -> [Rect; 3] {
|
||||
fn chrome_visible(editor: &Editor, groups: &[Vec<ChromeItem<'_>>; 3]) -> bool {
|
||||
groups.iter().flatten().any(|item| item.tab.is_none())
|
||||
|| (editor.tab_count() > 1 && groups.iter().any(|group| !group.is_empty()))
|
||||
}
|
||||
|
||||
fn document_layout(area: Rect, editor: &Editor, visible: [bool; 2]) -> [Rect; 4] {
|
||||
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
|
||||
let dock_focused = editor
|
||||
.file_picker()
|
||||
.is_some_and(|picker| picker.focused && picker.presentation == FilePresentation::Docked);
|
||||
Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(u16::from(area.height >= 3)),
|
||||
Constraint::Length(u16::from(area.height >= 2 || (prompting && !dock_focused))),
|
||||
])
|
||||
.areas(area)
|
||||
let prompt =
|
||||
u16::from(editor.diff().is_none() && (area.height >= 2 || (prompting && !dock_focused)))
|
||||
.min(area.height);
|
||||
// Both bars yield to the separate prompt and the last document row.
|
||||
let available = area.height.saturating_sub(prompt + 1);
|
||||
let top = u16::from(visible[0]).min(available);
|
||||
let bottom = u16::from(visible[1]).min(available - top);
|
||||
let workspace = Rect::new(
|
||||
area.x,
|
||||
area.y.saturating_add(top),
|
||||
area.width,
|
||||
area.height - top - bottom - prompt,
|
||||
);
|
||||
[
|
||||
Rect::new(area.x, area.y, area.width, top),
|
||||
workspace,
|
||||
Rect::new(area.x, workspace.bottom(), area.width, bottom),
|
||||
Rect::new(area.x, area.bottom() - prompt, area.width, prompt),
|
||||
]
|
||||
}
|
||||
|
||||
fn draw_bars(
|
||||
frame: &mut Frame,
|
||||
editor: &Editor,
|
||||
area: Rect,
|
||||
syntax: &mut SyntaxCache,
|
||||
) -> [Rect; 2] {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
let [_, workspace, _, prompt] = document_layout(area, editor, [false; 2]);
|
||||
return [workspace, prompt];
|
||||
}
|
||||
// Resolve the active buffer's language before computing either bar's visibility.
|
||||
syntax.update_visible(editor.lines(), editor.path(), editor.text_revision(), &[]);
|
||||
let groups = [
|
||||
chrome_groups(editor, editor.top_bar_config(), syntax),
|
||||
chrome_groups(editor, editor.bottom_bar_config(), syntax),
|
||||
];
|
||||
let visible = groups
|
||||
.each_ref()
|
||||
.map(|groups| chrome_visible(editor, groups));
|
||||
let [top, workspace, bottom, prompt] = document_layout(area, editor, visible);
|
||||
for (groups, area) in groups.into_iter().zip([top, bottom]) {
|
||||
draw_chrome(frame, groups, area);
|
||||
}
|
||||
[workspace, prompt]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -1402,14 +1424,9 @@ fn draw(
|
||||
panels: &mut WheelPanels,
|
||||
) {
|
||||
let area = frame.area();
|
||||
let area = draw_tab_bar(frame, editor, area);
|
||||
let areas = if editor.diff().is_some() {
|
||||
[area, Rect::default(), Rect::default()]
|
||||
} else {
|
||||
document_layout(area, editor)
|
||||
};
|
||||
let areas = draw_bars(frame, editor, area, syntax);
|
||||
draw_document(
|
||||
frame, editor, areas, area, top, left, syntax, document, panels,
|
||||
frame, editor, areas, areas[0], top, left, syntax, document, panels,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1417,7 +1434,7 @@ fn draw(
|
||||
fn draw_document(
|
||||
frame: &mut Frame,
|
||||
editor: &Editor,
|
||||
[workspace, status_area, prompt_area]: [Rect; 3],
|
||||
[workspace, prompt_area]: [Rect; 2],
|
||||
overlay_area: Rect,
|
||||
top: &mut usize,
|
||||
left: &mut usize,
|
||||
@@ -1430,7 +1447,6 @@ fn draw_document(
|
||||
let area = workspace;
|
||||
let focused = editor.render_pane_focused();
|
||||
if let Some(diff) = editor.diff() {
|
||||
draw_status(frame, editor, syntax, status_area);
|
||||
if panels.workspace_managed && diff.phase != Phase::View {
|
||||
return;
|
||||
}
|
||||
@@ -1446,7 +1462,7 @@ fn draw_document(
|
||||
return;
|
||||
}
|
||||
panels.diff_syntax = None;
|
||||
if overlay_area.width == 0 || overlay_area.height == 0 {
|
||||
if overlay_area.width == 0 || (overlay_area.height == 0 && prompt_area.height == 0) {
|
||||
return;
|
||||
}
|
||||
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
|
||||
@@ -1759,7 +1775,6 @@ fn draw_document(
|
||||
);
|
||||
}
|
||||
}
|
||||
draw_status(frame, editor, syntax, status_area);
|
||||
if prompting && prompt_area.height > 0 && prompt_area.width > 0 {
|
||||
let prefix = if editor.mode() == Mode::Search {
|
||||
'/'
|
||||
@@ -1895,13 +1910,6 @@ fn draw_overlays(
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_status(frame: &mut Frame, editor: &Editor, syntax: &SyntaxCache, status_area: Rect) {
|
||||
let line = editor.statusline_config();
|
||||
let groups = [&line.left, &line.center, &line.right]
|
||||
.map(|names| chrome_items(editor, line, names, Some(syntax)));
|
||||
draw_chrome(frame, groups, status_area);
|
||||
}
|
||||
|
||||
struct DiffGeometry {
|
||||
regions: [(Rect, Focus); 3],
|
||||
viewport: Option<(u64, usize, usize, usize, usize)>,
|
||||
@@ -5447,8 +5455,8 @@ mod tests {
|
||||
editor: &mut Editor,
|
||||
update: impl FnOnce(&mut ChromeLine, &mut ChromeLine),
|
||||
) {
|
||||
let mut top = editor.tabline_config().clone();
|
||||
let mut bottom = editor.statusline_config().clone();
|
||||
let mut top = editor.top_bar_config().clone();
|
||||
let mut bottom = editor.bottom_bar_config().clone();
|
||||
update(&mut top, &mut bottom);
|
||||
editor
|
||||
.set_chrome_config(top, bottom, BTreeMap::new())
|
||||
@@ -5490,7 +5498,11 @@ mod tests {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 3)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_tab_bar(frame, &editor, frame.area());
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||
frame.area(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
||||
@@ -5508,7 +5520,11 @@ mod tests {
|
||||
});
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_tab_bar(frame, &editor, frame.area());
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||
frame.area(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
|
||||
@@ -5554,8 +5570,16 @@ mod tests {
|
||||
let syntax = SyntaxCache::new();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_tab_bar(frame, &editor, frame.area());
|
||||
draw_status(frame, &editor, &syntax, Rect::new(3, 2, 45, 1));
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||
frame.area(),
|
||||
);
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
|
||||
Rect::new(3, 2, 45, 1),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
@@ -5570,8 +5594,16 @@ mod tests {
|
||||
editor.set_label("left".into(), "L".repeat(30)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_tab_bar(frame, &editor, frame.area());
|
||||
draw_status(frame, &editor, &syntax, Rect::new(3, 2, 45, 1));
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||
frame.area(),
|
||||
);
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
|
||||
Rect::new(3, 2, 45, 1),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
for (y, x, width) in [(0, 0, 51), (2, 3, 45)] {
|
||||
@@ -5585,7 +5617,7 @@ mod tests {
|
||||
fn active_tabs_keep_their_anchor_and_text_when_optional_groups_collide() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.execute("tab.new");
|
||||
configure_chrome(&mut editor, |top, _| {
|
||||
configure_chrome(&mut editor, |top, bottom| {
|
||||
top.left = vec!["prefix".into()];
|
||||
top.center = vec!["tabs".into()];
|
||||
top.right = vec!["suffix".into()];
|
||||
@@ -5594,6 +5626,7 @@ mod tests {
|
||||
tabs.padding = 64;
|
||||
tabs.leading_edge = "界".repeat(8);
|
||||
tabs.trailing_edge = "界".repeat(8);
|
||||
*bottom = top.clone();
|
||||
});
|
||||
editor
|
||||
.set_label("prefix".into(), "LEFT".repeat(32))
|
||||
@@ -5605,29 +5638,42 @@ mod tests {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 2)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_tab_bar(frame, &editor, frame.area());
|
||||
for (y, line) in [editor.top_bar_config(), editor.bottom_bar_config()]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, line, &SyntaxCache::new()),
|
||||
Rect::new(0, y as u16, width, 1),
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
if width > 0 {
|
||||
let row = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
||||
for y in 0..2 {
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
let row = pane_screen(&terminal, Rect::new(0, y, width, 1));
|
||||
assert!(
|
||||
row.trim()
|
||||
.starts_with(if width == 1 { "2" } else { "2:active" }),
|
||||
"{row}"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[((width - width.min(8)) / 2, 0)].symbol(),
|
||||
terminal.backend().buffer()[((width - width.min(8)) / 2, y)].symbol(),
|
||||
"2"
|
||||
);
|
||||
assert!(!row.contains("LEFT") && !row.contains("RIGHT"), "{row}");
|
||||
assert!(!row.contains('界'), "{row}");
|
||||
}
|
||||
}
|
||||
configure_chrome(&mut editor, |top, _| {
|
||||
configure_chrome(&mut editor, |top, bottom| {
|
||||
let tabs = top.segments.get_mut("tabs").unwrap();
|
||||
tabs.padding = 0;
|
||||
tabs.leading_edge.clear();
|
||||
tabs.trailing_edge.clear();
|
||||
*bottom = top.clone();
|
||||
});
|
||||
editor
|
||||
.set_label("prefix".into(), "LEFT".repeat(32))
|
||||
@@ -5638,11 +5684,22 @@ mod tests {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 2)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_tab_bar(frame, &editor, frame.area());
|
||||
for (y, line) in [editor.top_bar_config(), editor.bottom_bar_config()]
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, line, &SyntaxCache::new()),
|
||||
Rect::new(0, y as u16, 40, 1),
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let row = pane_screen(&terminal, Rect::new(0, 0, 40, 1));
|
||||
assert_eq!(&row[16..24], "2:active");
|
||||
for y in 0..2 {
|
||||
let row = pane_screen(&terminal, Rect::new(0, y, 40, 1));
|
||||
assert_eq!(&row[16..24], "2:active");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5683,8 +5740,16 @@ mod tests {
|
||||
let syntax = SyntaxCache::new();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_tab_bar(frame, &editor, frame.area());
|
||||
draw_status(frame, &editor, &syntax, Rect::new(0, 2, 40, 1));
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||
frame.area(),
|
||||
);
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
|
||||
Rect::new(0, 2, 40, 1),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
@@ -5726,7 +5791,11 @@ mod tests {
|
||||
editor.execute("tab.previous");
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_tab_bar(frame, &editor, frame.area());
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||
frame.area(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
@@ -5738,7 +5807,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_configured_chrome_does_not_allocate_a_tab_row() {
|
||||
fn empty_bar_content_reclaims_both_rows_without_hiding_the_prompt() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.execute("tab.new");
|
||||
configure_chrome(&mut editor, |top, bottom| {
|
||||
@@ -5755,28 +5824,292 @@ mod tests {
|
||||
first_leading_edge: None,
|
||||
},
|
||||
);
|
||||
bottom.left.clear();
|
||||
bottom.center.clear();
|
||||
bottom.right.clear();
|
||||
*bottom = top.clone();
|
||||
});
|
||||
let mut syntax = SyntaxCache::new();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
assert_eq!(draw_tab_bar(frame, &editor, area), area);
|
||||
assert_eq!(document_layout(area, &editor)[1].height, 1);
|
||||
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
|
||||
assert_eq!(workspace, Rect::new(0, 0, 40, 4));
|
||||
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
|
||||
})
|
||||
.unwrap();
|
||||
editor.set_label("empty".into(), "present".into()).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
assert_eq!(draw_tab_bar(frame, &editor, area).y, 1);
|
||||
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
|
||||
assert_eq!(workspace, Rect::new(0, 1, 40, 2));
|
||||
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
|
||||
})
|
||||
.unwrap();
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, 40, 1));
|
||||
assert!(title.contains("Not visible without a value"), "{title}");
|
||||
assert!(!title.contains('▓'), "{title}");
|
||||
for y in [0, 3] {
|
||||
let title = pane_screen(&terminal, Rect::new(0, y, 40, 1));
|
||||
assert!(title.contains("Not visible without a value"), "{title}");
|
||||
assert!(!title.contains('▓'), "{title}");
|
||||
}
|
||||
editor.handle(Input::Char(':'));
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
|
||||
assert_eq!(workspace.height, 2);
|
||||
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_bars_yield_to_document_and_prompt_identically_in_either_position() {
|
||||
let directory = Directory::new();
|
||||
let path = directory.file("source.txt", "DOCUMENT");
|
||||
for segment in ["tabs", "mode"] {
|
||||
for at_top in [false, true] {
|
||||
for prompting in [false, true] {
|
||||
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||
editor.execute("tab.new");
|
||||
configure_chrome(&mut editor, |top, bottom| {
|
||||
for line in [&mut *top, &mut *bottom] {
|
||||
line.left.clear();
|
||||
line.center.clear();
|
||||
line.right.clear();
|
||||
}
|
||||
let line = if at_top { top } else { bottom };
|
||||
line.left = vec![segment.into()];
|
||||
});
|
||||
if prompting {
|
||||
editor.handle(Input::Char(':'));
|
||||
}
|
||||
for height in 1..=4 {
|
||||
let area = Rect::new(3, 2, 80, height);
|
||||
let mut terminal = Terminal::with_options(
|
||||
ratatui::backend::TestBackend::new(100, 10),
|
||||
ratatui::TerminalOptions {
|
||||
viewport: ratatui::Viewport::Fixed(area),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let bar_text = match segment {
|
||||
"tabs" => "2 source.txt",
|
||||
_ if prompting => "COMMAND",
|
||||
_ => "NORMAL",
|
||||
};
|
||||
let visible = height >= 3;
|
||||
assert_eq!(screen_text(&terminal).contains(bar_text), visible);
|
||||
let cursor = terminal.get_cursor_position().unwrap();
|
||||
assert!(
|
||||
area.contains(cursor),
|
||||
"{segment} {at_top} prompting={prompting} {area:?}: {cursor:?}"
|
||||
);
|
||||
if prompting {
|
||||
assert_eq!(cursor.y, area.bottom() - 1);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(area.x, cursor.y)].symbol(),
|
||||
":",
|
||||
);
|
||||
}
|
||||
if !prompting || height > 1 {
|
||||
let viewport = views.document.geometry.unwrap().1;
|
||||
assert!(viewport.height > 0);
|
||||
assert!(pane_screen(&terminal, viewport).contains("DOCUMENT"));
|
||||
}
|
||||
if visible {
|
||||
let y = if at_top { area.y } else { area.bottom() - 2 };
|
||||
assert!(
|
||||
pane_screen(&terminal, Rect::new(area.x, y, area.width, 1))
|
||||
.contains(bar_text)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabs_only_singletons_hide_in_either_bar_and_reappear_with_another_tab() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
configure_chrome(&mut editor, |top, bottom| *bottom = top.clone());
|
||||
let mut views = BufferViewports::default();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(views.rects[0].1, Rect::new(0, 0, 80, 11));
|
||||
assert!(views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, 0),));
|
||||
|
||||
editor.execute("tab.new");
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(views.rects[0].1, Rect::new(0, 1, 80, 9));
|
||||
for y in [0, 10] {
|
||||
assert!(
|
||||
!views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, y),)
|
||||
);
|
||||
}
|
||||
let caret = terminal.get_cursor_position().unwrap();
|
||||
assert!(caret.y >= 1 && caret.y < 10);
|
||||
for y in [0, 10] {
|
||||
let bar = pane_screen(&terminal, Rect::new(0, y, 80, 1));
|
||||
assert!(
|
||||
bar.contains("1 [No Name]") && bar.contains("2 [No Name]"),
|
||||
"{bar}"
|
||||
);
|
||||
}
|
||||
editor.execute("tab.close");
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(views.rects[0].1, Rect::new(0, 0, 80, 11));
|
||||
assert!(!screen_text(&terminal).contains("[No Name]"));
|
||||
for y in [0, 10] {
|
||||
assert!(
|
||||
views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, y),)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swapped_bars_render_active_metadata_and_language_on_the_first_paint() {
|
||||
let directory = Directory::new();
|
||||
let path = directory.file("active.rs", "fn main() {}");
|
||||
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
|
||||
editor.execute("tab.new");
|
||||
editor.handle(Input::Char('i'));
|
||||
editor.handle(Input::Paste("// changed\n".into()));
|
||||
editor.handle(Input::Escape);
|
||||
configure_chrome(&mut editor, std::mem::swap);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(240, 12)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let top = pane_screen(&terminal, Rect::new(0, 0, 240, 1));
|
||||
assert!(top.contains("NORMAL"), "{top}");
|
||||
assert!(top.contains(&path.display().to_string()), "{top}");
|
||||
assert!(top.contains("[+]") && top.contains("Rust"), "{top}");
|
||||
let cursor = editor.cursor();
|
||||
assert!(
|
||||
top.contains(&format!("{}:{}", cursor.row + 1, cursor.column + 1)),
|
||||
"{top}"
|
||||
);
|
||||
let bottom = pane_screen(&terminal, Rect::new(0, 10, 240, 1));
|
||||
assert!(
|
||||
bottom.contains("1 active.rs [+]") && bottom.contains("2 active.rs [+]"),
|
||||
"{bottom}"
|
||||
);
|
||||
assert_eq!(views.document.geometry.unwrap().1.y, 1);
|
||||
let before = views.rects[0].1;
|
||||
editor.handle(Input::Char(':'));
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(views.rects[0].1, before);
|
||||
assert!(pane_screen(&terminal, Rect::new(0, 11, 240, 1)).starts_with(':'));
|
||||
assert_eq!(terminal.get_cursor_position().unwrap().y, 11);
|
||||
assert_eq!(pane_screen(&terminal, Rect::new(0, 10, 240, 1)), bottom);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_bars_bound_palettes_diff_selectors_docks_and_diff_prompts() {
|
||||
let directory = Directory::new();
|
||||
let mut editor =
|
||||
Editor::open(Some(directory.file("source.rs", "fn main() {}")), vec![]).unwrap();
|
||||
editor.execute("tab.new");
|
||||
configure_chrome(&mut editor, |top, bottom| {
|
||||
std::mem::swap(top, bottom);
|
||||
top.left = vec!["mode".into(), "language".into()];
|
||||
top.center = vec!["workspace".into()];
|
||||
top.right = vec!["position".into()];
|
||||
bottom.right = vec!["battery".into()];
|
||||
});
|
||||
editor
|
||||
.set_label("workspace".into(), "WORKSPACE".into())
|
||||
.unwrap();
|
||||
editor.set_label("battery".into(), "BOTTOM".into()).unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
let assert_bars = |terminal: &Terminal<ratatui::backend::TestBackend>,
|
||||
views: &BufferViewports| {
|
||||
assert!(pane_screen(terminal, Rect::new(0, 0, 120, 1)).contains("WORKSPACE"));
|
||||
let bottom_y = if views.panels.diff.is_some() { 29 } else { 28 };
|
||||
assert!(pane_screen(terminal, Rect::new(0, bottom_y, 120, 1)).contains("BOTTOM"));
|
||||
for (_, rect) in &views.rects {
|
||||
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
|
||||
}
|
||||
if let Some(geometry) = &views.panels.diff {
|
||||
for (rect, _) in geometry.regions {
|
||||
if rect.height > 0 {
|
||||
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((rect, _, _)) = views.panels.files {
|
||||
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
|
||||
}
|
||||
};
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)).contains("Rust"));
|
||||
assert_bars(&terminal, &views);
|
||||
editor.handle(Input::Ctrl('p'));
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains("Commands"));
|
||||
assert_bars(&terminal, &views);
|
||||
editor.handle(Input::Escape);
|
||||
|
||||
editor.execute("diff");
|
||||
let (id, _) = editor.take_diff_request().unwrap();
|
||||
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains("Choose Before"));
|
||||
assert!(views.document.geometry.is_none());
|
||||
assert_bars(&terminal, &views);
|
||||
editor.handle(Input::Enter);
|
||||
editor.handle(Input::Enter);
|
||||
let (id, _) = editor.take_diff_request().unwrap();
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
root: directory.0.clone(),
|
||||
editable_paths: Default::default(),
|
||||
before: "baseline".into(),
|
||||
after: "Working checkout".into(),
|
||||
files: vec![deletion_file()],
|
||||
})),
|
||||
);
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(views.panels.files.is_some());
|
||||
assert_bars(&terminal, &views);
|
||||
editor.handle(Input::Char(':'));
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let cursor = terminal.get_cursor_position().unwrap();
|
||||
assert!(Rect::new(0, 1, 120, 28).contains(cursor), "{cursor:?}");
|
||||
assert_bars(&terminal, &views);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char(' '));
|
||||
editor.handle(Input::Char('o'));
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
editor.file_picker().unwrap().presentation,
|
||||
FilePresentation::Floating
|
||||
);
|
||||
assert_bars(&terminal, &views);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5802,11 +6135,16 @@ mod tests {
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
if width > 0 && height > 0 {
|
||||
if width > 0 && height >= 3 {
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
||||
assert!(title.trim_start().starts_with('3'), "{title}");
|
||||
assert!(!title.contains("status"), "{title}");
|
||||
}
|
||||
if width > 0 && height == 1 {
|
||||
let (_, viewport) = views.document.geometry.unwrap();
|
||||
assert!(viewport.contains(terminal.get_cursor_position().unwrap()));
|
||||
assert!(pane_screen(&terminal, viewport).contains("ACTIVE"));
|
||||
}
|
||||
}
|
||||
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
@@ -5890,7 +6228,11 @@ mod tests {
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
assert_eq!(draw_tab_bar(frame, &editor, area).y, 1);
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||
area,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
||||
@@ -5908,7 +6250,11 @@ mod tests {
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
draw_tab_bar(frame, &editor, area);
|
||||
draw_chrome(
|
||||
frame,
|
||||
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
|
||||
area,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, 80, 1));
|
||||
@@ -5979,10 +6325,6 @@ mod tests {
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
views.panels.files.unwrap().0,
|
||||
floating_panel(Rect::new(0, 1, 120, 29))
|
||||
);
|
||||
assert_eq!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)), title);
|
||||
}
|
||||
|
||||
@@ -6185,10 +6527,10 @@ mod tests {
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
views.panels.files.unwrap().0,
|
||||
floating_panel(Rect::new(0, 0, 120, 30))
|
||||
);
|
||||
let panel = views.panels.files.unwrap().0;
|
||||
assert!(panel.x < 60 && panel.right() > 60);
|
||||
assert!(panel.y >= views.rects[0].1.y);
|
||||
assert!(panel.bottom() <= views.rects[0].1.bottom());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -6764,7 +7106,6 @@ mod tests {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert_eq!(terminal.get_cursor_position().unwrap().y, 13);
|
||||
editor.handle(Input::Enter);
|
||||
let searched = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert!(before.contains(searched));
|
||||
@@ -7071,7 +7412,6 @@ mod tests {
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(terminal.get_cursor_position().unwrap().y, 13);
|
||||
assert!(screen_text(&terminal).contains(":diff.close"));
|
||||
editor.handle(Input::Enter);
|
||||
assert!(editor.diff().is_none());
|
||||
@@ -9989,6 +10329,7 @@ end
|
||||
let before = (top, left);
|
||||
editor.handle(Input::Char(' '));
|
||||
editor.handle(Input::Char('o'));
|
||||
let mut panels = WheelPanels::default();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw(
|
||||
@@ -9998,11 +10339,13 @@ end
|
||||
&mut left,
|
||||
&mut syntax,
|
||||
&mut DocumentViewport::default(),
|
||||
&mut WheelPanels::default(),
|
||||
&mut panels,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
let panel = Rect::new(area.x + 5, area.y + 1, 100, 24);
|
||||
let panel = panels.files.unwrap().0;
|
||||
assert_eq!(panel.x - area.x, area.right() - panel.right());
|
||||
assert!(panel.y >= area.y && panel.bottom() <= area.bottom() - 2);
|
||||
assert_eq!((top, left), before);
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
|
||||
Reference in New Issue
Block a user