feat: configure chrome segments and anchor groups
This commit is contained in:
+230
-67
@@ -1,3 +1,4 @@
|
||||
use crate::chrome::{ChromeLine, validate_name};
|
||||
use crate::clipboard::{ClipboardMode, SystemClipboard};
|
||||
use crate::diff::{DiffState, Focus, Operation, Outcome, Phase, RevisionTarget};
|
||||
use crate::files::{DockSide, FilePicker, FilePresentation, FileView};
|
||||
@@ -179,7 +180,7 @@ pub enum Effect {
|
||||
Insert(String),
|
||||
Replace(String),
|
||||
Message(String),
|
||||
TablineLabel(String),
|
||||
Label { name: String, text: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
@@ -1350,7 +1351,9 @@ pub struct Editor {
|
||||
owned_system_clipboard: Option<(String, Vec<String>, bool)>,
|
||||
last_edit: Option<EditRecipe>,
|
||||
message: String,
|
||||
tabline_label: String,
|
||||
tabline: ChromeLine,
|
||||
statusline: ChromeLine,
|
||||
labels: BTreeMap<String, String>,
|
||||
quit: bool,
|
||||
capabilities: Vec<String>,
|
||||
features: HashMap<String, Feature>,
|
||||
@@ -1436,7 +1439,9 @@ impl Editor {
|
||||
owned_system_clipboard: None,
|
||||
last_edit: None,
|
||||
message: String::new(),
|
||||
tabline_label: String::new(),
|
||||
tabline: ChromeLine::tabline(),
|
||||
statusline: ChromeLine::statusline(),
|
||||
labels: BTreeMap::new(),
|
||||
quit: false,
|
||||
capabilities,
|
||||
features: HashMap::new(),
|
||||
@@ -3826,8 +3831,14 @@ impl Editor {
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
pub fn tabline_label(&self) -> &str {
|
||||
&self.tabline_label
|
||||
pub fn tabline_config(&self) -> &ChromeLine {
|
||||
&self.tabline
|
||||
}
|
||||
pub fn statusline_config(&self) -> &ChromeLine {
|
||||
&self.statusline
|
||||
}
|
||||
pub fn label(&self, name: &str) -> &str {
|
||||
self.labels.get(name).map_or("", String::as_str)
|
||||
}
|
||||
pub fn path(&self) -> Option<&Path> {
|
||||
self.active.path.as_deref()
|
||||
@@ -3868,19 +3879,46 @@ impl Editor {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn set_tabline_label(&mut self, text: String) -> bool {
|
||||
let text = if text.chars().any(char::is_control) {
|
||||
text.chars()
|
||||
.map(|ch| if ch.is_control() { '\u{fffd}' } else { ch })
|
||||
.collect()
|
||||
} else {
|
||||
text
|
||||
};
|
||||
if self.tabline_label == text {
|
||||
return false;
|
||||
pub fn set_chrome_config(
|
||||
&mut self,
|
||||
mut tabline: ChromeLine,
|
||||
mut statusline: ChromeLine,
|
||||
labels: BTreeMap<String, String>,
|
||||
) -> Result<(), String> {
|
||||
tabline.validate(true)?;
|
||||
statusline.validate(false)?;
|
||||
let mut total = 0usize;
|
||||
for (name, text) in &labels {
|
||||
validate_label(name, text)?;
|
||||
total = total
|
||||
.checked_add(name.len())
|
||||
.and_then(|total| total.checked_add(text.len()))
|
||||
.ok_or("Labels exceed 4 MiB")?;
|
||||
if total > MAX_EDIT_BYTES {
|
||||
return Err("Labels exceed 4 MiB".into());
|
||||
}
|
||||
}
|
||||
self.tabline_label = text;
|
||||
true
|
||||
self.tabline = tabline;
|
||||
self.statusline = statusline;
|
||||
self.labels = labels
|
||||
.into_iter()
|
||||
.map(|(name, text)| (name, sanitize_label(text)))
|
||||
.collect();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_label(&mut self, name: String, text: String) -> Result<bool, String> {
|
||||
validate_label(&name, &text)?;
|
||||
let text = sanitize_label(text);
|
||||
if self.label(&name) == text {
|
||||
return Ok(false);
|
||||
}
|
||||
if text.is_empty() {
|
||||
self.labels.remove(&name);
|
||||
} else {
|
||||
self.labels.insert(name, text);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn register(&mut self, feature: Feature) -> Result<(), String> {
|
||||
@@ -5767,24 +5805,7 @@ impl Editor {
|
||||
{
|
||||
return Err("Diff view is read-only".into());
|
||||
}
|
||||
let mut total = 0usize;
|
||||
for effect in &effects {
|
||||
let text = match effect {
|
||||
Effect::Insert(text)
|
||||
| Effect::Replace(text)
|
||||
| Effect::Message(text)
|
||||
| Effect::TablineLabel(text) => text,
|
||||
};
|
||||
total = total
|
||||
.checked_add(text.len())
|
||||
.ok_or("Plugin effects exceed 4 MiB")?;
|
||||
if total > MAX_EDIT_BYTES {
|
||||
return Err("Plugin effects exceed 4 MiB".into());
|
||||
}
|
||||
if !matches!(effect, Effect::TablineLabel(_)) {
|
||||
validate_text(text)?;
|
||||
}
|
||||
}
|
||||
validate_effects(&effects)?;
|
||||
if !effects
|
||||
.iter()
|
||||
.any(|effect| matches!(effect, Effect::Insert(_) | Effect::Replace(_)))
|
||||
@@ -5792,8 +5813,8 @@ impl Editor {
|
||||
for effect in effects {
|
||||
match effect {
|
||||
Effect::Message(text) => self.set_message(text),
|
||||
Effect::TablineLabel(text) => {
|
||||
self.set_tabline_label(text);
|
||||
Effect::Label { name, text } => {
|
||||
self.set_label(name, text)?;
|
||||
}
|
||||
_ => unreachable!("presentation-only effects"),
|
||||
}
|
||||
@@ -5837,8 +5858,8 @@ impl Editor {
|
||||
self.normalize_targets();
|
||||
}
|
||||
Effect::Message(text) => self.set_message(text),
|
||||
Effect::TablineLabel(text) => {
|
||||
self.set_tabline_label(text);
|
||||
Effect::Label { name, text } => {
|
||||
self.set_label(name, text)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8296,6 +8317,54 @@ fn validate_paste_text(text: &str) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_effects(effects: &[Effect]) -> Result<(), String> {
|
||||
let mut total = 0usize;
|
||||
for effect in effects {
|
||||
let text = match effect {
|
||||
Effect::Insert(text) | Effect::Replace(text) | Effect::Message(text) => text,
|
||||
Effect::Label { name, text } => {
|
||||
validate_label(name, text)?;
|
||||
total = total
|
||||
.checked_add(name.len())
|
||||
.ok_or("Plugin effects exceed 4 MiB")?;
|
||||
text
|
||||
}
|
||||
};
|
||||
total = total
|
||||
.checked_add(text.len())
|
||||
.ok_or("Plugin effects exceed 4 MiB")?;
|
||||
if total > MAX_EDIT_BYTES {
|
||||
return Err("Plugin effects exceed 4 MiB".into());
|
||||
}
|
||||
if !matches!(effect, Effect::Label { .. }) {
|
||||
validate_text(text)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_label(name: &str, text: &str) -> Result<(), String> {
|
||||
validate_name(name)?;
|
||||
if name
|
||||
.len()
|
||||
.checked_add(text.len())
|
||||
.is_none_or(|bytes| bytes > MAX_EDIT_BYTES)
|
||||
{
|
||||
return Err("Label exceeds 4 MiB".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sanitize_label(text: String) -> String {
|
||||
if text.chars().any(char::is_control) {
|
||||
text.chars()
|
||||
.map(|ch| if ch.is_control() { '\u{fffd}' } else { ch })
|
||||
.collect()
|
||||
} else {
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_text(text: &str) -> Result<(), String> {
|
||||
if let Some(ch) = text
|
||||
.chars()
|
||||
@@ -16972,10 +17041,13 @@ mod tests {
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::Message("ok".into()),
|
||||
Effect::TablineLabel("ready".into()),
|
||||
Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: "ready".into(),
|
||||
},
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(editor.tabline_label(), "ready");
|
||||
assert_eq!(editor.label("battery"), "ready");
|
||||
assert_eq!(editor.selections(), ranges);
|
||||
assert_eq!(editor.mode(), Mode::Visual);
|
||||
editor.apply(vec![Effect::Insert("!".into())]).unwrap();
|
||||
@@ -17674,23 +17746,91 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabline_label_sanitizes_once_and_is_global_until_cleared() {
|
||||
fn chrome_installation_and_invalid_label_batches_are_atomic() {
|
||||
let mut editor = editor();
|
||||
assert_eq!(editor.tabline_label(), "");
|
||||
let text = String::from("界 e\u{301} 👩🚀");
|
||||
assert!(editor.set_tabline_label(text));
|
||||
assert!(!editor.set_tabline_label("界 e\u{301} 👩🚀".into()));
|
||||
assert!(editor.set_tabline_label("界\0\t\r\n\u{1b}\u{85}👩🚀".into()));
|
||||
assert_eq!(editor.tabline_label(), "界������👩🚀");
|
||||
editor.set_label("battery".into(), "ready".into()).unwrap();
|
||||
let mut top = ChromeLine::tabline();
|
||||
top.center.push("workspace".into());
|
||||
let bottom = ChromeLine::statusline();
|
||||
editor
|
||||
.set_chrome_config(
|
||||
top.clone(),
|
||||
bottom.clone(),
|
||||
BTreeMap::from([
|
||||
("battery".into(), "ready".into()),
|
||||
("workspace".into(), "tools\n".into()),
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(editor.label("workspace"), "tools�");
|
||||
assert_eq!(
|
||||
editor.tabline_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");
|
||||
assert!(
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::Message("must not leak".into()),
|
||||
Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: "changed".into()
|
||||
},
|
||||
Effect::Insert("must not edit".into()),
|
||||
Effect::Label {
|
||||
name: "bad-name".into(),
|
||||
text: "invalid".into()
|
||||
},
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(editor.label("battery"), "ready");
|
||||
assert_eq!(editor.message(), "");
|
||||
assert_eq!(editor.context().text, "");
|
||||
assert_eq!(editor.text_revision(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_labels_sanitize_and_survive_tabs_until_independently_cleared() {
|
||||
let mut editor = editor();
|
||||
assert_eq!(editor.label("battery"), "");
|
||||
assert!(
|
||||
editor
|
||||
.set_label("battery".into(), "界 e\u{301} 👩🚀".into())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
!editor
|
||||
.set_label("battery".into(), "界 e\u{301} 👩🚀".into())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
editor
|
||||
.set_label("battery".into(), "界\0\t\r\n\u{1b}\u{85}👩🚀".into())
|
||||
.unwrap()
|
||||
);
|
||||
editor
|
||||
.set_label("workspace".into(), "tools".into())
|
||||
.unwrap();
|
||||
assert_eq!(editor.label("battery"), "界������👩🚀");
|
||||
editor.set_message("transient");
|
||||
editor.execute("tab.new");
|
||||
assert_eq!(editor.tabline_label(), "界������👩🚀");
|
||||
assert_eq!(editor.label("battery"), "界������👩🚀");
|
||||
editor.execute("tab.previous");
|
||||
assert_eq!(editor.tabline_label(), "界������👩🚀");
|
||||
assert!(editor.set_tabline_label(String::new()));
|
||||
assert_eq!(editor.label("workspace"), "tools");
|
||||
assert!(editor.set_label("battery".into(), String::new()).unwrap());
|
||||
editor.execute("tab.next");
|
||||
assert_eq!(editor.tabline_label(), "");
|
||||
assert!(!editor.set_tabline_label(String::new()));
|
||||
assert_eq!(editor.label("battery"), "");
|
||||
assert!(!editor.set_label("battery".into(), String::new()).unwrap());
|
||||
assert_eq!(editor.label("workspace"), "tools");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -17710,11 +17850,14 @@ mod tests {
|
||||
let history = (editor.active.undo.len(), editor.active.redo.len());
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::TablineLabel("界\n\u{1b}".into()),
|
||||
Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: "界\n\u{1b}".into(),
|
||||
},
|
||||
Effect::Message("read only".into()),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(editor.tabline_label(), "界��");
|
||||
assert_eq!(editor.label("battery"), "界��");
|
||||
assert_eq!(editor.message(), "read only");
|
||||
assert_eq!(editor.mode(), Mode::Visual);
|
||||
assert_eq!(editor.selections(), selection);
|
||||
@@ -17727,27 +17870,38 @@ mod tests {
|
||||
assert!(
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::TablineLabel("must not leak".into()),
|
||||
Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: "must not leak".into()
|
||||
},
|
||||
Effect::Insert("rejected".into()),
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(editor.tabline_label(), "界��");
|
||||
assert_eq!(editor.label("battery"), "界��");
|
||||
editor
|
||||
.apply(vec![Effect::TablineLabel(String::new())])
|
||||
.apply(vec![Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: String::new(),
|
||||
}])
|
||||
.unwrap();
|
||||
assert_eq!(editor.tabline_label(), "");
|
||||
assert_eq!(editor.label("battery"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_effects_validate_before_any_mutation_and_group_undo() {
|
||||
let mut editor = editor();
|
||||
editor.set_message("unchanged");
|
||||
editor.set_tabline_label("unchanged".into());
|
||||
editor
|
||||
.set_label("battery".into(), "unchanged".into())
|
||||
.unwrap();
|
||||
assert!(
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::TablineLabel("must not leak".into()),
|
||||
Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: "must not leak".into()
|
||||
},
|
||||
Effect::Insert("good".into()),
|
||||
Effect::Message("bad\u{1b}".into())
|
||||
])
|
||||
@@ -17755,31 +17909,37 @@ mod tests {
|
||||
);
|
||||
assert_eq!(editor.context().text, "");
|
||||
assert_eq!(editor.message(), "unchanged");
|
||||
assert_eq!(editor.tabline_label(), "unchanged");
|
||||
assert_eq!(editor.label("battery"), "unchanged");
|
||||
assert!(!editor.dirty());
|
||||
assert!(
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::TablineLabel("x".into()),
|
||||
Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: "x".into()
|
||||
},
|
||||
Effect::Insert("a".repeat(MAX_EDIT_BYTES))
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(editor.context().text, "");
|
||||
assert_eq!(editor.tabline_label(), "unchanged");
|
||||
assert_eq!(editor.label("battery"), "unchanged");
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::Insert("one".into()),
|
||||
Effect::Insert("\ntwo".into()),
|
||||
Effect::Message("ok".into()),
|
||||
Effect::TablineLabel("updated".into()),
|
||||
Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: "updated".into(),
|
||||
},
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(editor.context().text, "one\ntwo");
|
||||
assert_eq!(editor.message(), "ok");
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "");
|
||||
assert_eq!(editor.tabline_label(), "updated");
|
||||
assert_eq!(editor.label("battery"), "updated");
|
||||
editor.handle(Input::Char('U'));
|
||||
assert_eq!(editor.context().text, "one\ntwo");
|
||||
editor
|
||||
@@ -17798,7 +17958,10 @@ mod tests {
|
||||
let undo_len = editor.active.undo.len();
|
||||
editor
|
||||
.apply(vec![
|
||||
Effect::TablineLabel("typing".into()),
|
||||
Effect::Label {
|
||||
name: "battery".into(),
|
||||
text: "typing".into(),
|
||||
},
|
||||
Effect::Message("notice".into()),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user