Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions crates/neomacs-gui-tests/fixtures/point-query-geometry/init.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
;;; Point-dependent geometry must be queried before redisplay. -*- lexical-binding: t; -*-
(require 'json)

(defun point-query-geometry-probe ()
(switch-to-buffer (get-buffer-create "*point-query-geometry*"))
(erase-buffer)
(dotimes (_ 200) (insert "a\n"))
(let ((out nil))
(push (cons "native-engine"
(if (fboundp 'neomacs--write-frame-snapshot) t :json-false)) out)
(dolist (variable '(header-line-format tab-line-format mode-line-format))
(setq-local header-line-format nil tab-line-format nil mode-line-format nil)
(setq-local display-line-numbers nil)
(goto-char 1)
(set-window-start (selected-window) 1)
(set (make-local-variable variable)
'((:eval (propertize "H" 'face
(if (= (point) 1) '(:height 1.0) '(:height 4.0))))))
(redisplay t)
(let ((target 1))
(while (and (< (+ target 2) (point-max))
(pos-visible-in-window-p (+ target 2) nil nil))
(setq target (+ target 2)))
(let ((before (posn-at-point target)))
(goto-char 3)
;; Deliberately query before redisplay: point changes the header's
;; height without changing the text or the chrome format's identity.
(push (cons (symbol-name variable)
(vector (if before t :json-false)
(if (posn-at-point target) t :json-false))) out))))
(setq-local header-line-format nil tab-line-format nil mode-line-format nil)
(setq-local display-line-numbers t)
(set-face-attribute 'line-number-current-line nil :inherit 'line-number :height 2.0)
(goto-char 1)
(set-window-start (selected-window) 1)
(redisplay t)
(let ((before (posn-x-y (posn-at-point 3))))
(goto-char 3)
(let ((after (posn-x-y (posn-at-point 3))))
(push (cons "current-line-face"
(vector (car before) (cdr before) (car after) (cdr after))) out)))
(with-temp-file (getenv "NEOMACS_GUI_STATE_JSON")
(insert (json-encode (nreverse out)))))
(when (fboundp 'neomacs--write-frame-snapshot)
(neomacs--write-frame-snapshot
(concat (getenv "NEOMACS_GUI_STATE_JSON") ".frame.json") nil 'json))
(kill-emacs 0))

(run-with-timer 1 nil
(lambda ()
(condition-case err (point-query-geometry-probe)
(error
(with-temp-file (getenv "NEOMACS_GUI_STATE_JSON")
(insert (json-encode (list (cons "error" (format "%S" err))))))
(kill-emacs 1)))))
3 changes: 3 additions & 0 deletions crates/neomacs-gui-tests/tests/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ mod org_silent_selection;
#[path = "pixel_measurement.rs"]
mod pixel_measurement;

#[path = "point_query_geometry.rs"]
mod point_query_geometry;

#[path = "prefix_help.rs"]
mod prefix_help;

Expand Down
93 changes: 93 additions & 0 deletions crates/neomacs-gui-tests/tests/point_query_geometry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//! Live position queries must see point-dependent chrome and gutter metrics.
#![cfg(target_os = "linux")]
use neomacs_gui_tests::{
CommandSpec, DisplayHarness, GuiArtifactSet, GuiBackend, GuiCommandRunner, GuiRunOptions,
GuiRunStatus, GuiScenario, GuiTestPlan, ProcessGuiCommandRunner,
};
use std::{fs, path::PathBuf, time::Duration};

fn assert_geometry(state: &serde_json::Value) {
for variable in ["header-line-format", "tab-line-format", "mode-line-format"] {
assert_eq!(
state[variable],
serde_json::json!([true, false]),
"{variable}: {state}"
);
}
let face = &state["current-line-face"];
assert!(
face[1].as_i64().unwrap() > face[3].as_i64().unwrap(),
"moving the taller current-line face to row two must shorten row one: {state}"
);
}

#[test]
fn point_dependent_position_queries_match_live_gnu_before_redisplay() {
let root = neomacs_infra::workspace_root();
let artifacts = root.join(format!("tmp/point-query-geometry-{}", std::process::id()));
let fixture = root.join("crates/neomacs-gui-tests/fixtures/point-query-geometry/init.el");
let backend = match std::env::var("NEOMACS_GUI_TEST_BACKEND").as_deref() {
Ok("wayland") => GuiBackend::LinuxWayland,
_ => GuiBackend::LinuxX11,
};
let gnu_session = DisplayHarness::for_backend(GuiBackend::LinuxX11)
.start_session(&artifacts.join("gnu"))
.unwrap();
let gnu_state = artifacts.join("gnu-state.json");
let mut command = CommandSpec {
program: std::env::var_os("NEOMACS_GNU_EMACS_BINARY")
.map(PathBuf::from)
.unwrap_or_else(|| "emacs".into()),
args: vec!["-Q".into(), "--load".into(), fixture.display().to_string()],
env: vec![
("GDK_BACKEND".into(), "x11".into()),
("GSETTINGS_BACKEND".into(), "memory".into()),
(
"NEOMACS_GUI_STATE_JSON".into(),
gnu_state.display().to_string(),
),
],
};
command.env.extend(gnu_session.env().iter().cloned());
let output = ProcessGuiCommandRunner
.run(
&command,
&GuiArtifactSet::new(&artifacts, GuiBackend::LinuxX11, "gnu"),
&GuiRunOptions::with_timeout(Duration::from_secs(30)),
)
.unwrap();
assert!(!output.timed_out, "GNU: {output:#?}");
assert_eq!(output.exit_code, Some(0), "GNU: {output:#?}");
let gnu: serde_json::Value = serde_json::from_slice(&fs::read(gnu_state).unwrap()).unwrap();
assert_eq!(gnu["native-engine"], false);
assert_geometry(&gnu);

let session = DisplayHarness::for_backend(backend)
.start_session(&artifacts.join("native"))
.unwrap();
let binary = std::env::var_os("NEOMACS_GUI_TEST_BINARY")
.map(PathBuf::from)
.unwrap_or_else(|| root.join("target/release/neomacs"));
let mut plan = GuiTestPlan::new(
backend,
&root,
&artifacts,
GuiScenario::new("point-query-geometry", &fixture),
)
.with_program(binary)
.with_env("NEOMACS_DEBUG_SURFACE_READBACK", "1");
for (key, value) in session.env() {
plan = plan.with_env(key.clone(), value.clone());
}
let result = plan
.run_with(
&mut ProcessGuiCommandRunner,
GuiRunOptions::with_timeout(Duration::from_secs(30)),
)
.unwrap();
assert_eq!(result.status, GuiRunStatus::Passed, "{result:#?}");
let native: serde_json::Value =
serde_json::from_slice(&fs::read(&result.artifacts.gui_state).unwrap()).unwrap();
assert_eq!(native["native-engine"], true);
assert_geometry(&native);
}
48 changes: 41 additions & 7 deletions crates/neomacs-layout-engine/src/buffer_source/render_attempt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ pub(crate) struct BufferSourceRenderAttemptContext<
window_snapshots: &'a mut Vec<WindowPresentationSnapshot>,
}

/// A Position query expands live chrome once. Only the immediately following
/// physical chrome-height retry may reuse its measured native dimensions.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum PositionQueryChrome {
#[default]
Measure,
Measured,
}

/// Which live window state may be published by a completed row walk.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum WindowPositionPublication {
Expand Down Expand Up @@ -74,6 +83,11 @@ pub(crate) enum WindowPositionPublication {
/// start/end nor a window-scroll-functions effect to the live minibuffer.
InactiveEchoArea,
SynchronousQueryEnd,
/// GNU pos_visible_p remeasures live chrome before answering visibility.
/// Like window-end, this probe cannot publish a start/end or redisplay.
SynchronousPositionQuery {
chrome: PositionQueryChrome,
},
}

impl WindowPositionPublication {
Expand All @@ -84,15 +98,31 @@ impl WindowPositionPublication {
pub(crate) const fn uses_exact_window_start(self) -> bool {
matches!(
self,
Self::Snapshot | Self::InactiveEchoArea | Self::SynchronousQueryEnd
Self::Snapshot
| Self::InactiveEchoArea
| Self::SynchronousQueryEnd
| Self::SynchronousPositionQuery { .. }
)
}

/// A synchronous GNU `window-end` query walks only the text area using
/// the chrome dimensions accepted by the last redisplay. It must not
/// evaluate mode/header/tab-line Lisp while answering the query.
pub(crate) const fn is_synchronous_query(self) -> bool {
matches!(self, Self::SynchronousQueryEnd)
matches!(
self,
Self::SynchronousQueryEnd | Self::SynchronousPositionQuery { .. }
)
}

/// Preserve either the accepted window-end partition or the native heights
/// just measured for this position request. Initial position visibility
/// measures live mode/header/tab lines, as GNU pos_visible_p does.
pub(crate) const fn preserves_reserved_chrome(self) -> bool {
matches!(
self,
Self::SynchronousQueryEnd
| Self::SynchronousPositionQuery {
chrome: PositionQueryChrome::Measured,
}
)
}

pub(crate) const fn keeps_complete_minibuffer_measurement_start(self) -> bool {
Expand Down Expand Up @@ -133,7 +163,10 @@ impl WindowPositionPublication {
window_start_lisp,
);
}
Self::Snapshot | Self::InactiveEchoArea | Self::SynchronousQueryEnd => {}
Self::Snapshot
| Self::InactiveEchoArea
| Self::SynchronousQueryEnd
| Self::SynchronousPositionQuery { .. } => {}
}
None
}
Expand Down Expand Up @@ -440,7 +473,8 @@ impl BufferSourceRedisplayPublishRequest {
// discarded attempt.
WindowPositionPublication::Snapshot
| WindowPositionPublication::InactiveEchoArea
| WindowPositionPublication::SynchronousQueryEnd => {}
| WindowPositionPublication::SynchronousQueryEnd
| WindowPositionPublication::SynchronousPositionQuery { .. } => {}
}
}

Expand Down
11 changes: 5 additions & 6 deletions crates/neomacs-layout-engine/src/buffer_source/render_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1852,12 +1852,11 @@ impl BufferSourceOutputSetup {
// *measured* chrome heights so the window snapshot reports the real
// (possibly taller, e.g. doom-modeline's bar) height rather than the
// face-only estimate the text-area geometry was reserved from.
let chrome_source = if self.position_publication.is_synchronous_query() {
// GNU `Fwindow_end` builds a stack-local display iterator for the
// text area. Reuse the already accepted partition instead of
// evaluating status-line Lisp as a side effect of a geometry
// query. This result is discarded rather than presented, so no
// chrome rows need to be emitted.
let chrome_source = if self.position_publication.preserves_reserved_chrome() {
// GNU `Fwindow_end` preserves the accepted text partition. A
// position query instead preserves its freshly measured native
// heights across this one physical convergence retry. Neither
// case needs chrome rows or another status-line Lisp evaluation.
WindowChromeRowSource::PreserveReservedMetrics(WindowChromeMetrics::from_params(params))
} else {
WindowChromeRowSource::Recompute
Expand Down
39 changes: 39 additions & 0 deletions crates/neomacs-layout-engine/src/display_status_line.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,45 @@ thread_local! {
std::cell::RefCell::new(HashMap::new());
}

/// A position probe may shape live chrome, but cannot replace records owned
/// by the enclosing redisplay. All stored state is native; no Lisp output is
/// retained across a physical query retry.
#[must_use]
pub(crate) struct ChromeQueryRecordScope {
generation: HashMap<i64, ChromeGenerationRecord>,
mode_count: u32,
header_count: u32,
tab_count: u32,
_fingerprints: crate::incremental_layout::chrome_memo::ChromeFingerprintRecordScope,
_columns: neovm_core::emacs_core::xdisp::ColumnSpecConsumptionScope,
}

impl ChromeQueryRecordScope {
pub(crate) fn isolate() -> Self {
Self {
generation: CHROME_GENERATION_RECORD
.with(|record| std::mem::take(&mut *record.borrow_mut())),
mode_count: MODE_LINE_EVAL_COUNT.with(|count| count.replace(0)),
header_count: HEADER_LINE_EVAL_COUNT.with(|count| count.replace(0)),
tab_count: TAB_LINE_EVAL_COUNT.with(|count| count.replace(0)),
_fingerprints:
crate::incremental_layout::chrome_memo::ChromeFingerprintRecordScope::isolate(),
_columns: neovm_core::emacs_core::xdisp::ColumnSpecConsumptionScope::isolate(),
}
}
}

impl Drop for ChromeQueryRecordScope {
fn drop(&mut self) {
CHROME_GENERATION_RECORD.with(|record| {
std::mem::swap(&mut *record.borrow_mut(), &mut self.generation);
});
MODE_LINE_EVAL_COUNT.with(|count| count.set(self.mode_count));
HEADER_LINE_EVAL_COUNT.with(|count| count.set(self.header_count));
TAB_LINE_EVAL_COUNT.with(|count| count.set(self.tab_count));
}
}

/// Reset the per-redisplay chrome generation record. Called with
/// [`reset_mode_line_eval_count`] at the start of `layout_frame_rust`.
pub(crate) fn reset_chrome_generation_record() {
Expand Down
Loading
Loading