Merge branch 'main' into fix/3532-config-error-recovery

This commit is contained in:
Alexsander Falcucci 2026-08-29 01:51:26 +02:00 committed by GitHub
commit 986fcb8f64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 161 additions and 32 deletions

View file

@ -430,18 +430,19 @@ vim.api.nvim_create_autocmd("OptionSet", {
notify_intro_state()
pcall(vim.api.nvim_create_autocmd, 'Progress', {
group = vim.api.nvim_create_augroup('NeovideProgressBar', { clear = true }),
desc = 'Forward progress events to Neovide',
pcall(vim.api.nvim_create_autocmd, "Progress", {
group = vim.api.nvim_create_augroup("NeovideProgressBar", { clear = true }),
desc = "Forward progress events to Neovide",
callback = function(ev)
if ev.data and ev.data.status == 'running' then
if ev.data then
progress_bar({
percent = ev.data.percent or 0,
id = ev.data.id,
source = ev.data.source,
status = ev.data.status,
percent = ev.data.percent,
title = ev.data.title or "",
message = ev.data.message or "",
})
else
progress_bar({ percent = 100 })
end
end,
})

View file

@ -12,7 +12,7 @@ use strum::AsRefStr;
use super::RestartDetails;
use crate::{
editor::{Colors, CursorMode, CursorShape, Style, UnderlineStyle},
window::UserEvent,
window::{ProgressBarUpdate, UserEvent},
};
#[derive(Clone, Debug)]
@ -1144,15 +1144,40 @@ pub fn parse_redraw_event(event_value: Value) -> Result<Vec<RedrawEvent>> {
Ok(parsed_events)
}
fn parse_map_field<'a>(map: &'a [(Value, Value)], key: &str) -> Option<&'a Value> {
map.iter().find_map(|(name, value)| (name.as_str() == Some(key)).then_some(value))
}
fn parse_progress_id(value: &Value) -> Option<String> {
value
.as_str()
.map(ToOwned::to_owned)
.or_else(|| value.as_i64().map(|value| value.to_string()))
.or_else(|| value.as_u64().map(|value| value.to_string()))
}
fn parse_progress_string(value: &Value) -> Option<String> {
value.as_str().map(ToOwned::to_owned)
}
fn parse_progress_percent(value: &Value) -> Option<f32> {
value
.as_f64()
.map(|value| value as f32)
.or_else(|| value.as_i64().map(|value| value as f32))
.or_else(|| value.as_u64().map(|value| value as f32))
}
pub fn parse_progress_bar_event(value: Option<&Value>) -> Option<UserEvent> {
let map = value.filter(|v| matches!(v, Value::Map(_)))?.as_map()?;
let percent = map
.iter()
.find(|(key, _)| key.as_str() == Some("percent"))
.and_then(|(_, value)| value.as_f64())
.unwrap_or(0.0) as f32;
let update = ProgressBarUpdate {
id: parse_map_field(map, "id").and_then(parse_progress_id),
source: parse_map_field(map, "source").and_then(parse_progress_string),
status: parse_map_field(map, "status").and_then(parse_progress_string),
percent: parse_map_field(map, "percent").and_then(parse_progress_percent),
};
Some(UserEvent::ShowProgressBar { percent })
Some(UserEvent::ShowProgressBar { update })
}
#[cfg(test)]

View file

@ -488,7 +488,7 @@ impl Renderer {
let progress_bar_settings = self.settings.get::<ProgressBarSettings>();
self.progress_bar.animate(&progress_bar_settings, dt);
animating |= self.progress_bar.is_animating();
animating |= self.progress_bar.is_active();
animating
}

View file

@ -4,6 +4,9 @@ use crate::{renderer::GridRenderer, settings::ParseFromValue};
use neovide_derive::SettingGroup;
use skia_safe::{Canvas, Color4f, Paint, Rect};
/// Minimum active time required before the progress earns to be shown.
const REVEAL_DELAY: f32 = 0.1;
#[derive(Clone, SettingGroup)]
#[setting_prefix = "progress_bar"]
pub struct ProgressBarSettings {
@ -21,36 +24,105 @@ impl Default for ProgressBarSettings {
enum ProgressBarState {
Idle,
/// Progress has started, but the bar has not been shown yet.
///
/// it could happen that a producer has a very short progress or just sent
/// only a final completion update showing updates that could take a 0-100
/// animation bar after its work has been finished or by an unknown behavior,
/// adding actually only noise without giving useful user progress feedback.
///
/// we keep the bar hidden until progress remains active based on a
/// REVEAL_DELAY making it much more meaninful to show progress that
/// has been earned to be shown instead.
Pending {
elapsed: f32,
},
Animating,
Completing { completion_time: Instant },
Completing {
completion_time: Instant,
},
}
pub struct ProgressBar {
current_percent: f32,
target_percent: f32,
id: Option<String>,
state: ProgressBarState,
}
impl ProgressBar {
pub fn new() -> Self {
Self { current_percent: 0.0, target_percent: 0.0, state: ProgressBarState::Idle }
Self { current_percent: 0.0, target_percent: 0.0, id: None, state: ProgressBarState::Idle }
}
pub fn is_animating(&self) -> bool {
pub fn is_active(&self) -> bool {
!matches!(self.state, ProgressBarState::Idle)
}
pub fn start(&mut self, percent: f32) {
fn is_visible(&self) -> bool {
matches!(self.state, ProgressBarState::Animating | ProgressBarState::Completing { .. })
}
fn reset(&mut self) {
self.current_percent = 0.0;
self.target_percent = 0.0;
self.id = None;
self.state = ProgressBarState::Idle;
}
fn set_target_percent(&mut self, percent: f32) {
self.target_percent = percent.clamp(0.0, 100.0);
if self.target_percent < self.current_percent {
self.current_percent = self.target_percent;
}
}
pub fn start(&mut self, id: Option<&str>, percent: f32) {
if percent >= 100.0 {
self.finish(id);
return;
}
let is_idle = matches!(self.state, ProgressBarState::Idle);
let is_completing = matches!(self.state, ProgressBarState::Completing { .. });
self.id = id.map(ToOwned::to_owned);
self.set_target_percent(percent);
if is_idle {
self.state = ProgressBarState::Pending { elapsed: 0.0 };
} else if is_completing {
self.state = ProgressBarState::Animating;
}
}
pub fn finish(&mut self, id: Option<&str>) {
let current_id = self.id.as_deref();
let id_mismatch = id.zip(current_id).is_some_and(|(id, current_id)| id != current_id);
if !self.is_active() || id_mismatch {
return;
}
if matches!(&self.state, ProgressBarState::Pending { .. }) {
self.reset();
return;
}
self.id = id.or(current_id).map(ToOwned::to_owned);
self.set_target_percent(100.0);
self.state = ProgressBarState::Animating;
}
pub fn animate(&mut self, settings: &ProgressBarSettings, dt: f32) {
match &self.state {
match &mut self.state {
ProgressBarState::Idle => {}
ProgressBarState::Pending { elapsed } => {
*elapsed += dt;
if *elapsed >= REVEAL_DELAY {
self.state = ProgressBarState::Animating;
}
}
ProgressBarState::Animating => {
if self.current_percent < self.target_percent {
self.current_percent += settings.animation_speed * dt;
@ -63,10 +135,7 @@ impl ProgressBar {
}
ProgressBarState::Completing { completion_time } => {
if completion_time.elapsed().as_secs_f32() > settings.hide_delay {
self.state = ProgressBarState::Idle;
// Reset percents for next time
self.current_percent = 0.0;
self.target_percent = 0.0;
self.reset();
}
}
}
@ -79,7 +148,7 @@ impl ProgressBar {
grid_renderer: &GridRenderer,
grid_size: crate::units::GridSize<u32>,
) {
if !self.is_animating() || !settings.enabled {
if !self.is_visible() || !settings.enabled {
return;
}

View file

@ -499,7 +499,7 @@ impl Application {
fn process_buffered_draw_commands(&mut self, window_id: WindowId) {
let pending_batches = match self.render_states.get_mut(&window_id) {
Some(state) => state.pending_draw_commands.drain(..).collect::<Vec<_>>(),
Some(state) => std::mem::take(&mut state.pending_draw_commands),
None => return,
};
if !pending_batches.is_empty() {

View file

@ -74,6 +74,29 @@ pub struct Pressure {
stage: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ProgressBarUpdate {
pub id: Option<String>,
pub source: Option<String>,
pub status: Option<String>,
pub percent: Option<f32>,
}
impl ProgressBarUpdate {
/// neovim stable and nightly encode the same save event differently.
/// for example stable uses id = "bufwrite" with percent = 0 while nightly uses
/// id = "nvim.bufwrite [file]" without a percent field.
/// A determinate percentage on either shape is not treated as built-in save progress.
pub fn is_neovim_bufwrite(&self) -> bool {
self.source.as_deref() == Some("nvim")
&& match self.id.as_deref() {
Some("bufwrite") => self.percent == Some(0.0), // stable
Some(id) if id.starts_with("nvim.bufwrite ") => self.percent.is_none(), // nightly
_ => false,
}
}
}
#[cfg(target_os = "macos")]
#[derive(Clone, Debug, PartialEq)]
pub enum ForceClickKind {
@ -158,7 +181,7 @@ pub enum UserEvent {
},
NeovimRestart(RestartDetails),
ShowProgressBar {
percent: f32,
update: ProgressBarUpdate,
},
#[cfg(target_os = "macos")]
CreateWindow,

View file

@ -23,7 +23,8 @@ use approx::AbsDiffEq;
use super::settings::CornerPreference;
use super::{
EventPayload, EventTarget, KeyboardManager, MessageSelectionEvent, MouseManager, OverlayEvent,
RouteId, UserEvent, WindowCommand, WindowSettings, WindowSettingsChanged, WindowSize,
ProgressBarUpdate, RouteId, UserEvent, WindowCommand, WindowSettings, WindowSettingsChanged,
WindowSize,
};
#[cfg(target_os = "macos")]
@ -1243,8 +1244,8 @@ impl WinitWindowWrapper {
UserEvent::MacShortcut(command) => {
self.handle_mac_shortcut(command);
}
UserEvent::ShowProgressBar { percent, .. } => {
self.handle_progress_bar(target, percent);
UserEvent::ShowProgressBar { update } => {
self.handle_progress_bar(target, update);
}
_ => {}
}
@ -2246,13 +2247,23 @@ impl WinitWindowWrapper {
}
}
fn handle_progress_bar(&mut self, target: EventTarget, percent: f32) {
fn handle_progress_bar(&mut self, target: EventTarget, update: ProgressBarUpdate) {
tracy_zone!("handle_progress_bar");
if update.is_neovim_bufwrite() {
return;
}
let window_ids = self.window_ids_for_target(target);
for window_id in window_ids {
if let Some(route) = self.routes.get(&window_id) {
let mut renderer = route.window.renderer.borrow_mut();
renderer.progress_bar.start(percent);
match update.percent {
Some(percent) if percent >= 100.0 => {
renderer.progress_bar.finish(update.id.as_deref());
}
Some(percent) => renderer.progress_bar.start(update.id.as_deref(), percent),
None => renderer.progress_bar.finish(update.id.as_deref()),
}
}
}
}