neovm-core 0.0.2

Core runtime structures for NeoVM
//! Bootstrap-facing subset of GNU Emacs's `xfaces.c`.
//!
//! Face-related builtins are still mostly implemented in `face.rs` and
//! `font.rs`, but GNU startup also relies on a small set of C-level
//! variables from `xfaces.c` being bound before Lisp runs. Keep those
//! defaults here so Rust startup matches the same ownership boundary.

use crate::emacs_core::error::{EvalResult, signal};
use crate::emacs_core::intern::resolve_sym;
use crate::emacs_core::symbol::Obarray;
use crate::emacs_core::value::{HashKey, HashTableTest, Value, ValueKind, list_to_vec};
use crate::face::Face as RuntimeFace;

const FACE_ATTRIBUTES_VECTOR_LEN: usize = 20;

/// Register bootstrap variables owned by the face subsystem.
pub fn register_bootstrap_vars(obarray: &mut Obarray) {
    obarray.set_symbol_value("face-filters-always-match", Value::NIL);
    obarray.set_symbol_value(
        "face--new-frame-defaults",
        bootstrap_face_new_frame_defaults_table(),
    );
    obarray.set_symbol_value("face-default-stipple", Value::string("gray3"));
    obarray.set_symbol_value("tty-defined-color-alist", Value::NIL);
    obarray.set_symbol_value("scalable-fonts-allowed", Value::NIL);
    obarray.set_symbol_value("face-ignored-fonts", Value::NIL);
    obarray.set_symbol_value("face-remapping-alist", Value::NIL);
    obarray.set_symbol_value("face-font-rescale-alist", Value::NIL);
    obarray.set_symbol_value("face-near-same-color-threshold", Value::fixnum(30_000));
    obarray.set_symbol_value("face-font-lax-matched-attributes", Value::T);
}

/// Backfill xfaces-owned bootstrap variables after loading a dump or partial
/// source bootstrap. GNU owns these in xfaces.c, so load/bootstrap glue should
/// delegate here instead of duplicating the values itself.
pub(crate) fn ensure_startup_compat_variables(eval: &mut crate::emacs_core::eval::Context) {
    let defaults = [
        ("face-filters-always-match", Value::NIL),
        (
            "face--new-frame-defaults",
            bootstrap_face_new_frame_defaults_table(),
        ),
        ("face-default-stipple", Value::string("gray3")),
        ("scalable-fonts-allowed", Value::NIL),
        ("face-ignored-fonts", Value::NIL),
        ("face-remapping-alist", Value::NIL),
        ("face-font-rescale-alist", Value::NIL),
        ("face-near-same-color-threshold", Value::fixnum(30_000)),
        ("face-font-lax-matched-attributes", Value::T),
    ];
    for (name, value) in defaults {
        if eval.obarray().symbol_value(name).is_none() {
            eval.set_variable(name, value);
        }
    }
}

pub(crate) fn builtin_frame_face_hash_table(
    eval: &mut crate::emacs_core::eval::Context,
    args: Vec<Value>,
) -> EvalResult {
    crate::emacs_core::display::expect_range_args("frame--face-hash-table", &args, 0, 1)?;
    let frame_id = crate::emacs_core::window_cmds::resolve_frame_id_in_state(
        &mut eval.frames,
        &mut eval.buffers,
        args.first(),
        "frame-live-p",
    )?;

    Ok(eval
        .frames
        .get(frame_id)
        .map(|frame| frame.face_hash_table())
        .unwrap_or(Value::hash_table(HashTableTest::Eq)))
}

fn unspecified_face_attributes_vector() -> Value {
    Value::vector(vec![
        Value::symbol("unspecified");
        FACE_ATTRIBUTES_VECTOR_LEN
    ])
}

fn face_attributes_vector_slot(attr_name: &str) -> Option<usize> {
    match attr_name {
        ":family" => Some(1),
        ":foundry" => Some(2),
        ":width" => Some(3),
        ":height" => Some(4),
        ":weight" => Some(5),
        ":slant" => Some(6),
        ":underline" => Some(7),
        ":inverse-video" => Some(8),
        ":foreground" => Some(9),
        ":background" => Some(10),
        ":stipple" => Some(11),
        ":overline" => Some(12),
        ":strike-through" => Some(13),
        ":box" => Some(14),
        ":font" => Some(15),
        ":inherit" => Some(16),
        ":fontset" => Some(17),
        ":distant-foreground" => Some(18),
        ":extend" => Some(19),
        _ => None,
    }
}

fn face_attr_key_name(value: &Value) -> Option<&str> {
    match value.kind() {
        ValueKind::Symbol(id) => Some(resolve_sym(id)),
        _ => None,
    }
}

pub(crate) fn builtin_face_attributes_as_vector(args: Vec<Value>) -> EvalResult {
    crate::emacs_core::display::expect_args("face-attributes-as-vector", &args, 1)?;

    let mut attrs = vec![Value::symbol("unspecified"); FACE_ATTRIBUTES_VECTOR_LEN];
    let Some(plist) = list_to_vec(&args[0]) else {
        return Ok(Value::vector(attrs));
    };

    let mut i = 0;
    while i + 1 < plist.len() {
        let Some(attr_name) = face_attr_key_name(&plist[i]) else {
            i += 2;
            continue;
        };
        let Some(slot) = face_attributes_vector_slot(attr_name) else {
            i += 2;
            continue;
        };

        let value = plist[i + 1];
        match attr_name {
            ":foreground" | ":background" | ":distant-foreground" if value.is_nil() => {}
            ":stipple" | ":font" | ":inherit" | ":fontset" => {}
            ":box" if value.is_t() => attrs[slot] = Value::fixnum(1),
            _ => attrs[slot] = value,
        }

        i += 2;
    }

    Ok(Value::vector(attrs))
}

pub(crate) fn mirror_runtime_face_into_frame(
    frame: &mut crate::window::Frame,
    face_name: &str,
    face: &RuntimeFace,
) {
    let face_symbol = Value::symbol(face_name);
    frame.set_realized_face(face_symbol, face.clone());
    upsert_frame_face_hash_entry(
        frame.face_hash_table(),
        face_symbol,
        crate::emacs_core::font::runtime_face_to_lisp_vector(face),
    );
}

pub(crate) fn seed_face_new_frame_defaults_table(table: Value) {
    let face_names = crate::emacs_core::font::all_defined_face_names_sorted_by_id_desc();
    let face_entries: Vec<(Value, Value)> = face_names
        .into_iter()
        .filter_map(|face_name| {
            let face_id = crate::emacs_core::font::face_id_for_name(&face_name)?;
            Some((
                Value::symbol(face_name.as_str()),
                Value::cons(
                    Value::fixnum(face_id),
                    crate::emacs_core::font::make_lisp_face_vector(),
                ),
            ))
        })
        .collect();

    for (key, value) in face_entries {
        upsert_frame_face_hash_entry(table, key, value);
    }
}

fn bootstrap_face_new_frame_defaults_table() -> Value {
    let table = Value::hash_table(HashTableTest::Eq);
    seed_face_new_frame_defaults_table(table);
    table
}

pub(crate) fn ensure_face_new_frame_defaults_entry(
    eval: &mut crate::emacs_core::eval::Context,
    face_name: &str,
) -> Option<Value> {
    let table = eval
        .obarray()
        .symbol_value("face--new-frame-defaults")
        .copied()?;
    seed_face_new_frame_defaults_table(table);
    let face_id = crate::emacs_core::font::face_id_for_name(face_name)?;
    upsert_frame_face_hash_entry(
        table,
        Value::symbol(face_name),
        Value::cons(
            Value::fixnum(face_id),
            crate::emacs_core::font::make_lisp_face_vector(),
        ),
    );
    Some(table)
}

fn upsert_frame_face_hash_entry(table: Value, key: Value, value: Value) {
    if !table.is_hash_table() {
        unreachable!("frame face hash table must be a hash table");
    };
    let _ = table.with_hash_table_mut(|hash_table| {
        let hash_key = match key.kind() {
            ValueKind::Symbol(id) => HashKey::Symbol(id),
            _ => unreachable!("face hash keys are symbols"),
        };
        if !hash_table.data.contains_key(&hash_key) {
            hash_table.insertion_order.push(hash_key.clone());
        }
        hash_table.key_snapshots.insert(hash_key.clone(), key);
        hash_table.data.insert(hash_key, value);
    });
}

#[cfg(test)]
#[path = "xfaces_test.rs"]
mod tests;