feat: redesign status chrome and floating prompts

This commit is contained in:
Valdemar Lorenzen
2026-10-12 12:21:31 +02:00
parent 8c6a050171
commit 52915ffacf
12 changed files with 1333 additions and 373 deletions
+289 -17
View File
@@ -1351,11 +1351,14 @@ pub struct Editor {
owned_system_clipboard: Option<(String, Vec<String>, bool)>,
last_edit: Option<EditRecipe>,
message: String,
notification_visible: bool,
top_bar: ChromeLine,
bottom_bar: ChromeLine,
labels: BTreeMap<String, String>,
battery_charge: Option<u8>,
battery_label: String,
lsp_names: Vec<String>,
lsp_label: String,
quit: bool,
capabilities: Vec<String>,
features: HashMap<String, Feature>,
@@ -1441,11 +1444,14 @@ impl Editor {
owned_system_clipboard: None,
last_edit: None,
message: String::new(),
notification_visible: true,
top_bar: ChromeLine::top(),
bottom_bar: ChromeLine::bottom(),
labels: BTreeMap::new(),
battery_charge: None,
battery_label: String::new(),
lsp_names: Vec::new(),
lsp_label: String::new(),
quit: false,
capabilities,
features: HashMap::new(),
@@ -1773,6 +1779,7 @@ impl Editor {
self.line_pending.take(),
self.seek_pending.take(),
std::mem::take(&mut self.message),
std::mem::replace(&mut self.notification_visible, true),
)
});
self.select_tab(target_tab);
@@ -1794,6 +1801,7 @@ impl Editor {
self.line_pending,
self.seek_pending,
self.message,
self.notification_visible,
) = interaction;
}
match result {
@@ -3199,7 +3207,6 @@ impl Editor {
popup: Completion { items, selected: 1 },
original,
});
self.message.clear();
Ok(())
}
@@ -3835,6 +3842,9 @@ impl Editor {
pub fn message(&self) -> &str {
&self.message
}
pub fn notification_visible(&self) -> bool {
self.notification_visible
}
pub fn top_bar_config(&self) -> &ChromeLine {
&self.top_bar
}
@@ -3847,6 +3857,12 @@ impl Editor {
pub fn battery_label(&self) -> &str {
&self.battery_label
}
pub fn lsp_names(&self) -> &[String] {
&self.lsp_names
}
pub fn lsp_label(&self) -> &str {
&self.lsp_label
}
pub fn path(&self) -> Option<&Path> {
self.active.path.as_deref()
}
@@ -3884,6 +3900,7 @@ impl Editor {
} else {
message
};
self.notification_visible = true;
}
pub fn set_chrome_config(
@@ -3928,6 +3945,20 @@ impl Editor {
Ok(true)
}
pub(crate) fn set_lsp_names(&mut self, mut names: Vec<String>) -> bool {
for name in &mut names {
if name.chars().any(char::is_control) {
*name = sanitize_label(std::mem::take(name));
}
}
if self.lsp_names == names {
return false;
}
self.lsp_label = names.join(" + ");
self.lsp_names = names;
true
}
pub(crate) fn set_battery_charge(&mut self, charge: Option<u8>) -> bool {
let charge = charge.filter(|charge| *charge <= 100);
if self.battery_charge == charge {
@@ -4775,6 +4806,14 @@ impl Editor {
if input == Input::Escape && self.count.take().is_some() {
return None;
}
if input == Input::Escape
&& self.active.view.mode == Mode::Normal
&& self.notification_visible
&& (!self.message.is_empty() || self.diagnostic_at_cursor().is_some())
{
self.notification_visible = false;
return None;
}
if matches!(self.active.view.mode, Mode::Normal | Mode::Visual)
&& let Input::Char(ch @ '0'..='9') = input
{
@@ -5124,7 +5163,6 @@ impl Editor {
self.active.view.linewise = linewise;
self.active.view.mode = Mode::Visual;
self.normalize_targets();
self.message.clear();
}
}
Action::SelectLines => self.split_rows(false, false),
@@ -5255,7 +5293,6 @@ impl Editor {
}
self.active.view.visual_motions = None;
self.normalize_targets();
self.message.clear();
self.record_jump(source);
}
@@ -5360,21 +5397,12 @@ impl Editor {
self.parked.insert(previous.id, previous);
self.load_view();
self.reveal_caret();
self.show_buffer();
}
fn show_buffer(&mut self) {
let path = self
.path()
.map_or_else(|| Cow::Borrowed("[No Name]"), Path::to_string_lossy);
self.set_message(format!("Buffer {}: {path}", self.active.id));
}
fn switch_buffer(&mut self, id: usize) {
if id == self.active.id {
self.reveal_caret();
self.park_source();
self.show_buffer();
} else if let Some(buffer) = self.parked.remove(&id) {
self.activate(buffer);
} else {
@@ -6424,7 +6452,6 @@ impl Editor {
}
self.active.view.visual_motions = None;
self.normalize_targets();
self.message.clear();
}
fn line_selections(&mut self, count: usize) -> Vec<Selection> {
@@ -6489,7 +6516,6 @@ impl Editor {
},
);
}
self.message.clear();
self.reveal_caret();
}
@@ -6518,7 +6544,6 @@ impl Editor {
if changing {
self.start_insert(before.unwrap(), recipe.unwrap().start);
self.edit_ranges(action, &ranges);
self.message.clear();
return;
}
self.edit_ranges(action, &ranges);
@@ -6770,7 +6795,6 @@ impl Editor {
});
}
self.record(before);
self.message.clear();
}
if !system {
self.clipboard = fragments;
@@ -7079,7 +7103,6 @@ impl Editor {
}
self.active.view.visual_motions = None;
self.record(before);
self.message.clear();
}
Err(error) => {
if !restored {
@@ -8612,6 +8635,206 @@ mod tests {
}
}
#[test]
fn notification_escape_preserves_carets_then_ordinary_escape_collapses_them() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("abcd\nabcd".into())])
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
assert!(editor.notification_visible());
editor.set_message("Written");
editor.handle(Input::Right);
let targets = editor.active.view.targets.clone();
let revision = editor.text_revision();
let history = (editor.active.undo.len(), editor.active.redo.len());
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.message(), "Written");
assert_eq!(editor.active.view.targets, targets);
assert_eq!(editor.active.view.primary, 1);
assert_eq!(editor.text_revision(), revision);
assert_eq!(
(editor.active.undo.len(), editor.active.redo.len()),
history
);
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
editor.set_message("Written");
assert!(editor.notification_visible());
type_text(&mut editor, "iX");
editor.handle(Input::Escape);
assert!(editor.notification_visible());
assert_eq!(editor.message(), "Written");
editor.handle(Input::Char('v'));
editor.handle(Input::Escape);
assert!(editor.notification_visible());
for prompt in [':', '/'] {
editor.handle(Input::Char(prompt));
editor.handle(Input::Char('x'));
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Normal);
assert!(editor.notification_visible());
}
}
#[test]
fn notification_escape_waits_for_prefixes_and_focused_overlays() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("abcd\nabcd".into())])
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
editor.set_message("Written");
let targets = editor.active.view.targets.clone();
for prefix in ["f", "3g", "2d", "4", " b"] {
type_text(&mut editor, prefix);
editor.handle(Input::Escape);
assert!(editor.notification_visible(), "{prefix}");
assert_eq!(editor.active.view.targets, targets, "{prefix}");
assert_eq!(editor.count(), None);
}
editor.show_startup_diagnostic("startup".into());
editor.handle(Input::Escape);
assert!(editor.startup_diagnostic().is_none());
assert!(editor.notification_visible());
editor.show_hover("hover".into()).unwrap();
editor.handle(Input::Escape);
assert!(editor.hover().is_none());
assert!(editor.notification_visible());
editor.handle(Input::Ctrl('p'));
editor.handle(Input::Escape);
assert!(editor.palette().is_none());
assert!(editor.notification_visible());
editor.open_file_picker(FilePresentation::Docked);
editor.handle(Input::Char('i'));
assert!(editor.file_picker().unwrap().searching);
editor.handle(Input::Escape);
assert!(editor.file_picker().unwrap().focused);
assert!(editor.notification_visible());
editor.handle(Input::Escape);
assert!(!editor.file_picker().unwrap().focused);
assert!(editor.notification_visible());
assert_eq!(editor.active.view.targets, targets);
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.active.view.targets, targets);
}
#[test]
fn empty_notification_does_not_swallow_escape_but_cursor_diagnostic_does() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("abcd\nabcd".into())])
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
assert!(editor.notification_visible());
editor
.replace_diagnostics(
"fixture",
editor.buffer_id(),
editor.text_revision(),
vec![annotation((1, 0), (1, 3), "diagnostic")],
)
.unwrap();
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.selection_count(), 2);
assert_eq!(editor.diagnostic_at_cursor().unwrap().message, "diagnostic");
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
}
#[test]
fn hidden_pane_interactions_cannot_reopen_or_dismiss_focused_notifications() {
let mut editor = editor();
let hidden = editor.pane_id();
editor.execute("pane.split_vertical");
editor.set_message("focused");
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
editor.with_pane(hidden, |pane| pane.set_message("background"));
assert_eq!(editor.message(), "focused");
assert!(!editor.notification_visible());
editor.with_render_pane(hidden, |pane| {
assert!(pane.message().is_empty());
});
assert!(!editor.notification_visible());
editor.set_message("focused");
editor.with_pane(hidden, |pane| {
pane.set_message("background");
pane.handle(Input::Escape);
});
assert!(editor.notification_visible());
assert_eq!(editor.message(), "focused");
editor.execute("tab.new");
editor.set_message("new tab");
editor.handle(Input::Escape);
editor.with_pane(hidden, |pane| pane.set_message("hidden tab"));
assert_eq!(editor.message(), "new tab");
assert!(!editor.notification_visible());
}
#[test]
fn buffer_activation_preserves_last_operation_and_dismissal() {
let directory = Directory::new();
let path = directory.path("opened.txt");
fs::write(&path, "text").unwrap();
let mut editor = editor();
editor.set_message("Written");
editor.execute(&format!("edit {}", path.display()));
assert_eq!(editor.message(), "Written");
editor.handle(Input::Escape);
for command in ["buffer 1", "bnext", "bprev", "buffer 1"] {
editor.execute(command);
assert_eq!(editor.message(), "Written");
assert!(!editor.notification_visible());
}
editor.execute("buffer 999");
assert!(editor.message().contains("No buffer"));
assert!(editor.notification_visible());
}
#[test]
fn native_lsp_cache_is_independent_presentation_metadata() {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("document".into())])
.unwrap();
type_text(&mut editor, "vl");
editor.set_message("Written");
editor.notification_visible = false;
editor
.set_label("lsp".into(), "configured label".into())
.unwrap();
let targets = editor.active.view.targets.clone();
let revision = editor.text_revision();
let history = (editor.active.undo.len(), editor.active.redo.len());
assert!(editor.lsp_names().is_empty());
assert_eq!(editor.lsp_label(), "");
assert!(editor.set_lsp_names(vec!["rust-analyzer".into(), "界 server".into()]));
assert_eq!(editor.lsp_names(), ["rust-analyzer", "界 server"]);
assert_eq!(editor.lsp_label(), "rust-analyzer + 界 server");
assert!(!editor.set_lsp_names(vec!["rust-analyzer".into(), "界 server".into()]));
assert!(editor.set_lsp_names(vec!["unsafe\nname".into()]));
assert_eq!(editor.lsp_label(), "unsafe�name");
assert!(editor.set_lsp_names(Vec::new()));
assert_eq!(editor.lsp_label(), "");
assert_eq!(editor.label("lsp"), "configured label");
assert_eq!(editor.message(), "Written");
assert!(!editor.notification_visible());
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.active.view.targets, targets);
assert_eq!(editor.text_revision(), revision);
assert_eq!(
(editor.active.undo.len(), editor.active.redo.len()),
history
);
}
#[test]
fn pane_focus_remaps_do_not_steal_printable_picker_filter_input() {
let directory = Directory::new();
@@ -9571,6 +9794,38 @@ mod tests {
assert!(editor.diff_interacting());
}
#[test]
fn diff_documents_dismiss_notifications_but_files_and_selectors_keep_escape() {
let directory = Directory::new();
let mut editor = editor();
historical_diff(
&mut editor,
&directory.0,
vec!["before".into(), "second".into()],
vec!["after".into(), "second".into()],
);
set_carets(&mut editor, &[(0, 1), (1, 1)], 1);
editor.set_message("operation");
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.selection_count(), 2);
assert!(editor.diff_interacting());
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
editor.set_message("operation");
editor.diff.as_mut().unwrap().focus = Focus::Files;
editor.handle(Input::Escape);
assert!(editor.notification_visible());
assert_ne!(editor.diff().unwrap().focus, Focus::Files);
editor.execute("diff.close");
editor.execute("diff");
editor.handle(Input::Char('q'));
assert_eq!(editor.diff().unwrap().query, "q");
editor.handle(Input::Escape);
assert_eq!(editor.diff().unwrap().query, "");
assert!(editor.notification_visible());
}
#[test]
fn readonly_native_panes_allow_selection_yank_search_and_multicarets() {
for focus in [Focus::Before, Focus::After] {
@@ -11451,6 +11706,7 @@ mod tests {
);
editor.handle(Input::Escape);
assert!(editor.hover().is_none());
assert!(editor.notification_visible());
assert_eq!(editor.active.view.targets, targets);
assert_eq!(editor.active.view.primary, primary);
assert_eq!(editor.context().text, text);
@@ -11461,6 +11717,9 @@ mod tests {
);
assert_eq!(editor.mode(), Mode::Normal);
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.active.view.targets, targets);
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
editor.show_hover("documentation".into()).unwrap();
editor.handle(Input::Char('i'));
@@ -14211,6 +14470,11 @@ mod tests {
assert_eq!(editor.count(), None);
assert_eq!(editor.sequence_pending(), None);
editor.handle(Input::Escape);
if !visual {
assert!(!editor.notification_visible());
assert_eq!(editor.selection_count(), 2);
editor.handle(Input::Escape);
}
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(editor.selection_count(), if visual { 2 } else { 1 });
}
@@ -16901,6 +17165,10 @@ mod tests {
assert_eq!(editor.context().text, "ab\nxyzq");
editor.handle(Input::Char('u'));
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
assert_eq!(editor.selection_count(), 2);
editor.handle(Input::Escape);
assert_eq!(editor.selection_count(), 1);
let before = editor.snapshot();
let undo = editor.active.undo.len();
let redo = editor.active.redo.len();
@@ -17979,6 +18247,8 @@ mod tests {
fn plugin_effects_validate_before_any_mutation_and_group_undo() {
let mut editor = editor();
editor.set_message("unchanged");
editor.handle(Input::Escape);
assert!(!editor.notification_visible());
editor
.set_label("battery".into(), "unchanged".into())
.unwrap();
@@ -17996,6 +18266,7 @@ mod tests {
);
assert_eq!(editor.context().text, "");
assert_eq!(editor.message(), "unchanged");
assert!(!editor.notification_visible());
assert_eq!(editor.label("battery"), "unchanged");
assert!(!editor.dirty());
assert!(
@@ -18024,6 +18295,7 @@ mod tests {
.unwrap();
assert_eq!(editor.context().text, "one\ntwo");
assert_eq!(editor.message(), "ok");
assert!(editor.notification_visible());
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "");
assert_eq!(editor.label("battery"), "updated");