feat: configure chrome segments and anchor groups

This commit is contained in:
Valdemar Lorenzen
2026-10-12 11:07:40 +02:00
parent d55b7b65d2
commit 4cfd068f1c
14 changed files with 1731 additions and 371 deletions
+178 -28
View File
@@ -220,7 +220,11 @@ impl EditorSession {
));
}
editor.set_diff_context_options(config.diff_context_lines, config.diff_expand_lines);
editor.set_tabline_label(config.tabline_label);
if let Err(error) =
editor.set_chrome_config(config.tabline, config.statusline, config.labels)
{
notices.push(format!("chrome: {error}"));
}
let mut plugins = BTreeMap::new();
for configured in config.plugins {
let path = configured.path;
@@ -658,7 +662,7 @@ impl EditorSession {
&& self.editor.startup_diagnostic().is_none()
}
fn apply_update(&mut self, owner: &str, update: PluginUpdate) -> Result<bool, String> {
fn apply_update(&mut self, owner: &str, mut update: PluginUpdate) -> Result<bool, String> {
if owner == "core"
&& let Some(id) = update.request_id
&& self
@@ -668,7 +672,9 @@ impl EditorSession {
.map(|latest| latest.0)
!= Some(id)
{
return Ok(false);
update
.effects
.retain(|effect| matches!(effect, UpdateEffect::Label { .. }));
}
for effect in &update.effects {
if let UpdateEffect::Completion {
@@ -697,6 +703,7 @@ impl EditorSession {
}
// Validate the whole applicable annotation batch before changing any buffer.
let validation = (|| -> Result<(), String> {
let mut label_bytes = 0usize;
for effect in &update.effects {
match effect {
UpdateEffect::Diagnostics {
@@ -757,9 +764,14 @@ impl EditorSession {
}
UpdateEffect::Completion { .. } => {}
UpdateEffect::Message { .. } => {}
UpdateEffect::TablineLabel { text } => {
if text.len() > 4 * 1024 * 1024 {
return Err("Tabline label exceeds 4 MiB".into());
UpdateEffect::Label { name, text } => {
crate::editor::validate_label(name, text)?;
label_bytes = label_bytes
.checked_add(name.len())
.and_then(|bytes| bytes.checked_add(text.len()))
.ok_or("Labels exceed 4 MiB")?;
if label_bytes > 4 * 1024 * 1024 {
return Err("Labels exceed 4 MiB".into());
}
}
}
@@ -857,8 +869,8 @@ impl EditorSession {
changed |= self.editor.message() != text;
self.editor.set_message(text);
}
UpdateEffect::TablineLabel { text } => {
changed |= self.editor.set_tabline_label(text);
UpdateEffect::Label { name, text } => {
changed |= self.editor.set_label(name, text)?;
}
}
}
@@ -870,7 +882,7 @@ impl EditorSession {
&mut self,
owner: &str,
id: u64,
effects: Vec<Effect>,
mut effects: Vec<Effect>,
) -> Result<bool, String> {
let source = self
.requests
@@ -879,16 +891,18 @@ impl EditorSession {
let Some(source) = source else {
return Err("plugin response has an unknown invocation ID".into());
};
if !self.source_is_current(source)
|| self.editor.buffer_id() != source.buffer_id
|| self.editor.pane_id() != source.pane_id
{
return Ok(false);
// Document effects retain source freshness; labels belong to the whole session.
let current = self.source_is_current(source)
&& self.editor.buffer_id() == source.buffer_id
&& self.editor.pane_id() == source.pane_id;
if !current {
effects.retain(|effect| matches!(effect, Effect::Label { .. }));
}
if effects
.iter()
.all(|effect| matches!(effect, Effect::Message(_) | Effect::TablineLabel(_)))
.all(|effect| matches!(effect, Effect::Message(_) | Effect::Label { .. }))
{
crate::editor::validate_effects(&effects)?;
let mut changed = false;
for effect in effects {
match effect {
@@ -896,8 +910,8 @@ impl EditorSession {
changed |= self.editor.message() != text;
self.editor.set_message(text);
}
Effect::TablineLabel(text) => {
changed |= self.editor.set_tabline_label(text);
Effect::Label { name, text } => {
changed |= self.editor.set_label(name, text)?;
}
_ => unreachable!("presentation-only effects checked above"),
}
@@ -2996,35 +3010,129 @@ end
}
#[test]
fn tabline_updates_survive_tab_switches_and_remain_atomic_in_readonly_views() {
fn startup_chrome_installs_named_values_without_editing_the_document() {
let fixture = Fixture::new();
fixture.file("buffer.txt", "source");
let session = fixture.open(
r#"
Vamp.configure do |config|
config.tabline = {right: [:battery]}
config.statusline = {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.lines(), ["source"]);
assert_eq!(session.editor.text_revision(), 0);
assert!(!session.editor.dirty());
}
#[test]
fn asynchronous_result_labels_outlive_the_source_without_applying_stale_edits() {
let fixture = Fixture::new();
let target = fixture.file("other.txt", "");
let mut session = EditorSession::open(None, vec![], None).unwrap();
let source = InvocationSource {
buffer_id: session.editor.buffer_id(),
pane_id: session.editor.pane_id(),
revision: session.editor.text_revision(),
};
session.editor.execute("tab.new");
session
.editor
.execute(&format!("edit {}", target.display()));
assert_ne!(session.editor.buffer_id(), source.buffer_id);
session.editor.handle(Input::Char('i'));
let message = session.editor.message().to_owned();
for (id, effects) in [
(
1,
vec![
Effect::Insert("stale".into()),
Effect::Message("stale message".into()),
Effect::Label {
name: "battery".into(),
text: "bat 38%".into(),
},
],
),
(
2,
vec![
Effect::Label {
name: "battery".into(),
text: "must not leak".into(),
},
Effect::Label {
name: "bad-name".into(),
text: "invalid".into(),
},
],
),
] {
session
.requests
.entry("fixture".into())
.or_default()
.pending
.insert(id, source);
let result = session.apply_response("fixture", id, effects);
if id == 1 {
assert!(result.unwrap());
} else {
assert!(result.is_err());
}
assert_eq!(session.editor.label("battery"), "bat 38%");
assert_eq!(session.editor.lines(), [""]);
assert_eq!(session.editor.message(), message);
assert_eq!(session.editor.mode(), Mode::Insert);
assert_eq!(session.editor.text_revision(), 0);
assert!(!session.editor.dirty());
}
session.editor.handle(Input::Char('x'));
session.editor.handle(Input::Escape);
session.editor.handle(Input::Char('u'));
assert_eq!(session.editor.lines(), [""]);
assert_eq!(session.editor.label("battery"), "bat 38%");
}
#[test]
fn named_labels_survive_tab_switches_and_remain_atomic_in_readonly_views() {
let fixture = Fixture::new();
let mut session = fixture.asynchronous(None);
publish(
&mut session,
vec![serde_json::json!({
"type": "tabline_label", "text": "bat 37%"
"type": "label", "name": "battery", "text": "bat 37%"
})],
);
drain_until(&mut session, |session| {
session.editor().tabline_label() == "bat 37%"
session.editor().label("battery") == "bat 37%"
});
let gate = fixture.0.join("label.gate");
hold(
&mut session,
&gate,
vec![serde_json::json!({
"type": "tabline_label", "text": "bat 38%"
"type": "label", "name": "battery", "text": "bat 38%"
})],
);
barrier(&mut session, "label request received");
session.execute("tab.new");
release(&mut session, &gate);
assert_eq!(session.editor().tabline_label(), "bat 38%");
assert_eq!(session.editor().label("battery"), "bat 38%");
session.execute("tab.previous");
assert_eq!(session.editor().tabline_label(), "bat 38%");
assert_eq!(session.editor().label("battery"), "bat 38%");
let update = |text: &str| PluginUpdate {
request_id: None,
effects: vec![UpdateEffect::TablineLabel { text: text.into() }],
effects: vec![UpdateEffect::Label {
name: "battery".into(),
text: text.into(),
}],
};
assert!(
!session
@@ -3034,7 +3142,8 @@ end
let rejected = PluginUpdate {
request_id: None,
effects: vec![
UpdateEffect::TablineLabel {
UpdateEffect::Label {
name: "battery".into(),
text: "must not leak".into(),
},
UpdateEffect::SemanticTokens {
@@ -3045,7 +3154,48 @@ end
],
};
assert!(session.apply_update("generic_fixture", rejected).is_err());
assert_eq!(session.editor().tabline_label(), "bat 38%");
assert_eq!(session.editor().label("battery"), "bat 38%");
let message = session.editor.message().to_owned();
for effects in [
vec![
UpdateEffect::Message {
text: "must not leak".into(),
},
UpdateEffect::Label {
name: "battery".into(),
text: "changed".into(),
},
UpdateEffect::Label {
name: "bad-name".into(),
text: "invalid".into(),
},
],
vec![
UpdateEffect::Label {
name: "battery".into(),
text: "x".repeat(2 * 1024 * 1024),
},
UpdateEffect::Label {
name: "workspace".into(),
text: "x".repeat(2 * 1024 * 1024),
},
],
] {
assert!(
session
.apply_update(
"generic_fixture",
PluginUpdate {
request_id: None,
effects
}
)
.is_err()
);
assert_eq!(session.editor.label("battery"), "bat 38%");
assert_eq!(session.editor.label("workspace"), "");
assert_eq!(session.editor.message(), message);
}
session.editor.execute("diff");
assert!(session.editor.mutation_blocked());
assert!(
@@ -3053,10 +3203,10 @@ end
.apply_update("generic_fixture", update("bat\t39%\n"))
.unwrap()
);
assert_eq!(session.editor().tabline_label(), "bat�39%�");
assert_eq!(session.editor().label("battery"), "bat�39%�");
assert_eq!(session.editor().lines(), [""]);
assert!(session.apply_update("generic_fixture", update("")).unwrap());
assert_eq!(session.editor().tabline_label(), "");
assert_eq!(session.editor().label("battery"), "");
}
#[test]