Named face storage and lookup with cached inheritance resolution.
The registry holds all defined faces and caches their resolved
(fully-inherited) forms. When a face is looked up, the registry
returns the resolved version with no nil fields.
Building a registry
A registry is built from a theme's syntax map using from_theme/1,
which converts every capture_name => style entry into a Face
struct with proper inheritance wired by dotted-name convention.
Resolution caching
resolve_all/1 pre-computes every face's resolved form and stores
it alongside the raw faces. Subsequent resolve/2 calls return the
cached result in O(1).
Buffer-local overrides
with_overrides/2 merges buffer-local face overrides (e.g., from
face remapping) on top of the base registry. The overrides are
merged attribute-by-attribute, and the resolution cache is rebuilt.
Summary
Functions
Builds a registry from a theme's syntax map.
Builds a registry from a full MingaEditor.UI.Theme.t() struct.
Looks up a face by name. Returns the raw (unresolved) face or nil.
Returns all face names in the registry.
Creates an empty registry with only the default face.
Adds or replaces a face in the registry.
Resolves a face by name, returning the fully-inherited version.
Pre-resolves all faces and populates the resolution cache.
Resolves a face by name and returns a Face.t().
Resolves a face with modifier composition.
Adds default LSP semantic token faces to the registry.
Merges buffer-local face overrides on top of the base registry.
Types
@type face_map() :: %{required(String.t()) => Minga.Core.Face.t()}
Map of face name to face struct.
Functions
@spec from_syntax(MingaEditor.UI.Theme.syntax()) :: t()
Builds a registry from a theme's syntax map.
Converts each capture_name => style_keyword_list entry into a Face
struct. Inheritance is inferred from dotted names: "keyword.function"
inherits from "keyword", which inherits from "default".
The registry is pre-resolved after building, so all lookups return fully resolved faces.
Examples
iex> syntax = %{"keyword" => [fg: 0xC678DD, bold: true], "keyword.function" => [fg: 0xC678DD]}
iex> reg = Registry.from_syntax(syntax)
iex> face = Registry.resolve(reg, "keyword.function")
iex> face.bold
true
@spec from_theme(MingaEditor.UI.Theme.t()) :: t()
Builds a registry from a full MingaEditor.UI.Theme.t() struct.
Uses the theme's syntax map and sets the default face's fg/bg from the theme's editor colors.
@spec get(t(), String.t()) :: Minga.Core.Face.t() | nil
Looks up a face by name. Returns the raw (unresolved) face or nil.
Returns all face names in the registry.
@spec new() :: t()
Creates an empty registry with only the default face.
@spec put(t(), Minga.Core.Face.t()) :: t()
Adds or replaces a face in the registry.
The resolution cache is invalidated for any face that could be
affected (the face itself and all descendants). Call resolve_all/1
after batch updates to rebuild the cache.
@spec resolve(t(), String.t()) :: Minga.Core.Face.t()
Resolves a face by name, returning the fully-inherited version.
If the face is in the resolution cache, returns it directly.
If not found by exact name, falls back through the dotted name
hierarchy (e.g., "keyword.function.builtin" tries
"keyword.function", then "keyword", then "default").
Always returns a face (falls back to default).
Pre-resolves all faces and populates the resolution cache.
@spec style_for(t(), String.t()) :: Minga.Core.Face.t()
Resolves a face by name and returns a Face.t().
This is the main entry point for the render pipeline, replacing
Theme.style_for_capture/2. Returns a fully resolved face struct.
For composite capture names (e.g., "@lsp.type.variable+deprecated"),
modifier attributes are composed on top of the base face.
Always returns a face (falls back to default).
@spec style_for_with_modifiers(t(), String.t(), [String.t()]) :: Minga.Core.Face.t()
Resolves a face with modifier composition.
Takes a base capture name and a list of modifier names, resolves each,
and merges modifier attributes on top of the base face. This is how
@lsp.mod.deprecated adds strikethrough to whatever the type's color
is, rather than replacing it.
Returns a Face.t() with the base face's colors and the modifier
face's decorative attributes composed.
Adds default LSP semantic token faces to the registry.
Maps @lsp.type.* captures to their closest tree-sitter equivalents
so semantic tokens render with sensible colors even without explicit
theme support. Themes can override these by defining faces for
@lsp.type.variable, @lsp.mod.deprecated, etc.
Also adds modifier faces:
@lsp.mod.deprecatedgets strikethrough@lsp.mod.readonlyinherits from the type face (no visual change by default)
Merges buffer-local face overrides on top of the base registry.
Each override is a {face_name, attribute_overrides} pair where
attribute_overrides is a keyword list of face fields to override.
Returns a new registry with the overrides merged and the resolution
cache rebuilt.
Examples
iex> reg = Registry.from_syntax(%{"keyword" => [fg: 0xC678DD, bold: true]})
iex> reg = Registry.with_overrides(reg, %{"keyword" => [fg: 0xFF0000]})
iex> face = Registry.resolve(reg, "keyword")
iex> face.fg
0xFF0000
iex> face.bold
true