Skip to content
Merged
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
91 changes: 91 additions & 0 deletions crates/neomacs-gui-tests/fixtures/commit-editing.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
;;; commit-editing.el --- Commit-buffer input and separator regressions -*- lexical-binding: t; -*-

(defvar neo-commit-old-map-count 0)
(defconst neo-commit-async-probe
(member (getenv "NEOMACS_COMMIT_PROBE") '("async-map" "async-map-delayed"))
"Non-nil when this fixture checks asynchronous command dispatch.")
(defconst neo-commit-overlay-probe
(equal (getenv "NEOMACS_COMMIT_PROBE") "overlay-separator")
"Non-nil when the small newline belongs to an overlay string.")

(defun neo-commit-probe-finish ()
"Check the completed input sequence or the redisplayed separator geometry."
(interactive)
(condition-case err
(progn
(if neo-commit-async-probe
(progn
(unless (= neo-commit-old-map-count 0)
(error "First commit key used the previous buffer's map"))
(unless (equal (with-current-buffer "*commit-message*" (buffer-string)) "aa")
(error "Commit buffer did not receive both characters")))
(redisplay t)
(let* ((summary-y (cdr (posn-x-y (posn-at-point 1))))
(separator-y (cdr (posn-x-y (posn-at-point 11))))
(body-y (cdr (posn-x-y (posn-at-point 12))))
(normal-height
(if neo-commit-overlay-probe
(- (cdr (posn-x-y (posn-at-point 17))) body-y)
(- separator-y summary-y)))
(separator-height
(if neo-commit-overlay-probe
(- body-y summary-y (* 2 normal-height))
(- body-y separator-y))))
(unless (and (> separator-height 0)
(< separator-height (/ normal-height 2.0)))
(error "Separator is not short: normal=%S separator=%S"
normal-height separator-height))))
(princ "COMMIT-EDITING-PASS\n" 'external-debugging-output)
(kill-emacs 0))
(error
(princ (format "COMMIT-EDITING-FAIL %S\n" err) 'external-debugging-output)
(kill-emacs 1))))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

;; Install the watchdog before setup can enter any input wait.
(run-at-time 10 nil
(lambda ()
(princ "COMMIT-EDITING-FAIL watchdog expired\n" 'external-debugging-output)
(kill-emacs 1)))

(if neo-commit-async-probe
(progn
;; This sentinel is dispatched only after both queued characters, even
;; when slow startup makes multiple timers due in one service pass.
(global-set-key [neo-commit-probe-finish] #'neo-commit-probe-finish)
(switch-to-buffer (get-buffer-create "*commit-status*"))
(use-local-map (make-sparse-keymap))
(local-set-key "a" (lambda () (interactive)
(setq neo-commit-old-map-count (1+ neo-commit-old-map-count))))
(with-current-buffer (get-buffer-create "*commit-message*")
(fundamental-mode))
;; Like a with-editor process filter: display the new buffer, but
;; restore the old current buffer while the command loop awaits input.
(run-at-time 0.5 nil
(lambda ()
(save-current-buffer (switch-to-buffer "*commit-message*"))
(setq unread-command-events
(list ?a ?a 'neo-commit-probe-finish)))))
(require 'log-edit)
(switch-to-buffer (get-buffer-create "*commit-separator*"))
(if neo-commit-overlay-probe
(progn
(fundamental-mode)
(insert "Summary: \n\nbody\nnext\n")
(overlay-put (make-overlay 11 11) 'before-string
(propertize "\n" 'face
'(:height 0.1 :inverse-video t :extend t))))
;; Checkout-local settings must not prompt during an isolated GUI probe.
(let ((enable-dir-local-variables nil))
(log-edit-mode))
(insert "Summary: \n\nbody\n")
(font-lock-ensure))
(goto-char 10))

(unless neo-commit-async-probe
(run-at-time 2 nil #'neo-commit-probe-finish))

;; Reproduce startup slow enough to expire both timers in the old fixture,
;; before the command loop could dispatch either queued character.
(when (equal (getenv "NEOMACS_COMMIT_PROBE") "async-map-delayed")
(let ((deadline (+ (float-time) 3)))
(while (< (float-time) deadline))))
106 changes: 106 additions & 0 deletions crates/neomacs-gui-tests/tests/commit_editing.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Real command-input and small-font newline regressions, including macOS.
#![cfg(any(target_os = "linux", target_os = "macos"))]

use neomacs_gui_tests::{
DisplayHarness, GuiBackend, GuiRunOptions, GuiScenario, GuiTestPlan, ProcessGuiCommandRunner,
};
use std::{fs, path::PathBuf, time::Duration};

/// Honor explicit compositor selection, defaulting to the host's native backend.
fn commit_probe_backend(configured: Option<&str>) -> GuiBackend {
match configured {
Some("wayland" | "linux-wayland") => GuiBackend::LinuxWayland,
Some("x11" | "linux-x11") => GuiBackend::LinuxX11,
Some("macos") => GuiBackend::Macos,
None if cfg!(target_os = "macos") => GuiBackend::Macos,
None => GuiBackend::LinuxX11,
Some(other) => panic!("unsupported NEOMACS_GUI_TEST_BACKEND={other:?}"),
}
}

/// Run a native commit-editing fixture and require its explicit success marker.
fn run_commit_probe(probe: &str) {
let root = neomacs_infra::crate_root!().join("../..");
let configured = std::env::var("NEOMACS_GUI_TEST_BACKEND").ok();
let backend = commit_probe_backend(configured.as_deref());
let artifacts = root
.join("target/neomacs-gui-tests")
.join(format!("commit-{probe}"));
let session = DisplayHarness::for_backend(backend)
.start_session(&artifacts)
.unwrap();
let program = 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(
probe,
root.join("crates/neomacs-gui-tests/fixtures/commit-editing.el"),
),
)
.with_program(program)
.with_env("NEOMACS_COMMIT_PROBE", probe)
.with_env("RUST_LOG", "warn");
for (key, value) in session.env() {
plan = plan.with_env(key, value);
}
let result = plan
.run_with(
&mut ProcessGuiCommandRunner,
GuiRunOptions::with_timeout(Duration::from_secs(20)),
)
.unwrap();
let stderr = fs::read_to_string(&result.artifacts.stderr).unwrap_or_default();
assert!(!result.timed_out, "{result:#?}\n{stderr}");
assert_eq!(result.exit_code, Some(0), "{result:#?}\n{stderr}");
assert!(stderr.contains("COMMIT-EDITING-PASS"), "{stderr}");
}

/// Backend aliases and platform defaults must not require a live compositor.
#[test]
fn commit_probe_respects_requested_backend() {
for name in ["wayland", "linux-wayland"] {
assert_eq!(commit_probe_backend(Some(name)), GuiBackend::LinuxWayland);
}
for name in ["x11", "linux-x11"] {
assert_eq!(commit_probe_backend(Some(name)), GuiBackend::LinuxX11);
}
assert_eq!(commit_probe_backend(Some("macos")), GuiBackend::Macos);
assert_eq!(
commit_probe_backend(None),
if cfg!(target_os = "macos") {
GuiBackend::Macos
} else {
GuiBackend::LinuxX11
}
);
}

#[test]
#[should_panic(expected = "unsupported NEOMACS_GUI_TEST_BACKEND")]
fn commit_probe_rejects_unknown_backend() {
commit_probe_backend(Some("unknown"));
}

#[test]
fn first_commit_character_uses_the_asynchronously_selected_buffer() {
run_commit_probe("async-map");
}

#[test]
fn delayed_timer_service_waits_for_commit_commands() {
run_commit_probe("async-map-delayed");
}

#[test]
fn overlay_separator_uses_its_small_font_height() {
run_commit_probe("overlay-separator");
}

#[test]
fn log_edit_separator_uses_its_small_font_height() {
run_commit_probe("separator");
}
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 @@ -31,6 +31,9 @@ mod coding_save;
#[path = "colrv1_emoji.rs"]
mod colrv1_emoji;

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,8 @@ impl BufferSourceOutputSetup {
}

#[allow(clippy::too_many_arguments)]
/// Render a window body using eligible retained-row replay or source layout,
/// returning the resulting installation or retry outcome.
pub(crate) fn render_body_attempt<
'a,
'surface,
Expand Down Expand Up @@ -945,7 +947,9 @@ impl BufferSourceOutputSetup {
cursor_row_pixel_y,
face_height,
face_ascent,
cursor_height,
// Fresh capture clamps against the
// whole row, including spacing.
row.height_px,
cursor_ascent,
geometry.char_height,
row.ends_at_zv,
Expand Down
23 changes: 15 additions & 8 deletions crates/neomacs-layout-engine/src/buffer_source/row_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1941,6 +1941,8 @@ impl<'a> BufferSourceLineBreakRenderRequest<'a> {
self
}

/// Resolve source-newline font geometry and line-end effects before
/// advancing the row transition, including intercepted display newlines.
pub(crate) fn render_and_apply<B: LayoutBufferView, FS: crate::neovm_bridge::FaceServices>(
self,
source_walk: &mut BufferSourceWalk<'_, B>,
Expand All @@ -1961,21 +1963,26 @@ impl<'a> BufferSourceLineBreakRenderRequest<'a> {
let mut source_render = source_render;
let context = self.context;

let resolved_height = if self.line_height
!= crate::display_item::DisplayLineHeightPolicy::Default
&& source_render
.measurement_mode()
.uses_concrete_font_geometry()
{
let defaults = context.row_geometry_defaults;
// The frame default is a fallback, not a minimum for a small font.
// Share this decision with worker-prepared rows; ordinary default-font
// rows keep their accumulated geometry without scanning glyphs.
let defaults = context.row_geometry_defaults;
let face = context.active_face_state.metrics();
let resolved_height = if source_render
.measurement_mode()
.uses_concrete_font_geometry()
&& crate::display_row::metrics::newline_requires_height_resolution(
self.line_height,
(face.row_height(), face.ascent()),
(defaults.height, defaults.ascent),
) {
let fallback =
crate::display_row::metrics::DisplayRowFallbackMetrics::from_default_face_extents(
context.default_char_width,
defaults.height,
defaults.ascent,
);
let content = source_render.current_row_visible_content_metrics(face_ids, fallback);
let face = context.active_face_state.metrics();
let metrics = crate::display_row::metrics::resolve_line_height(
self.line_height,
(content.height_px(), content.ascent_px()),
Expand Down
Loading