Skip to main content

PluginLoader

Struct PluginLoader 

Source
pub struct PluginLoader { /* private fields */ }
Expand description

The plugin loader subsystem: owns the runtime, the loaded-plugin set, and the discovery + load orchestration. Stood up at boot by [install], which captures the editor environment and registers the loader as a PluginLoaderHandle service so the user surface reaches it generically.

Implementations§

Source§

impl PluginLoader

Source

pub fn new(host: Arc<PluginHost>) -> Self

Construct a loader over host with no editor environment — the minimal constructor for tests exercising only the lifecycle spine. [install] uses with_env to wire the real seams.

Source

pub fn with_services(host: Arc<PluginHost>, services: LoaderServices) -> Self

Construct a loader wired with the editor environment (LoaderServices) — the boot path ([install]) and headless harnesses / tests. The seams a plugin declares are driven against the wired handles; an absent handle makes that seam a logged skip.

Source

pub fn loaded_count(&self) -> usize

The number of currently-loaded plugins. The spine proof + the PL8.H manager view read it.

Source

pub fn wired_seams(&self) -> WiredSeams

Which drain-required services the loader captured from the boot context ([install]). A boot-ordering regression (installing the loader before a service it depends on registers) silently leaves a field false, turning that seam’s drain into a NotWired skip — so the boot pin asserts every flag is set after Editor::boot. Test/introspection affordance.

Source

pub fn is_loaded(&self, name: &str) -> bool

Whether a plugin with manifest id name is currently loaded (the :plugin-unload <name> / :plugin-reload <name> resolution, PL8.C).

Source

pub async fn discover_and_load(&self, dir: &Path, tier: TrustTier) -> usize

Discover every plugin under dir and load each, logging + skipping any that fails (never aborting the others). Returns the count loaded. Runs on the caller (the multi-thread runtime), off the editor actor.

Source

pub async fn load_discovered( &self, plugin: &DiscoveredPlugin, tier: TrustTier, ) -> Result<PluginId, PluginLoaderError>

Load one already-discovered plugin: compile, then either drive the lifecycle spine (empty provides) or drain each declared seam, and record its provenance. Returns the host-issued PluginId.

Source

pub fn request_mode_enablement(&self, mode: &str)

PM.7/PM.8 follow-up: honour a require’s enable-mode sugar.

Publishes the same ModeEnablementRequested the manifest default_mode gate publishes — one mechanism, two ways of asking for it (a plugin declaring its own default, or a user’s init.rs asking for it at the call site).

The host never learns the mode-id statically: it arrives in the spec and is forwarded as an opaque string, so the mode stays the plugin’s own surface (feedback_mode_owns_its_surface).

A missing bus is a silent skip — the same degradation every other event publisher here uses when the editor is not fully wired (tests, headless harnesses).

Source

pub fn plugin_status(&self) -> Vec<PluginStatus>

PL8.H.1: a read-only snapshot of every loaded plugin — identity, trust tier, capabilities granted/denied, and health — for the :plugins manager view (PL8.H.2/.3). Cloned out under the loaded-set lock, so the view renders a stable frame while loads/unloads proceed.

Source

pub fn failed_loads(&self) -> Vec<FailedLoad>

WT.4: the plugins that tried to load this session and could not.

Name-sorted like plugin_status, for the same reason: the view is read down a column, and discovery order is not something a user can predict or reproduce.

Source

pub fn mark_quarantined(&self, plugin: u32, func: String, kind: String)

PL8.H.1: mark the plugin plugin quarantined (its instance trapped) — the body of the Event::PluginCrashed subscription (subscribe_health), exposed directly so a test can drive the health flip without a live bus. A crash id matching no loaded plugin is ignored (it may have been unloaded between the trap and the drain) — never a panic.

Source

pub fn subscribe_health(self: &Arc<Self>)

PL8.H.1: subscribe to Event::PluginCrashed so a trapped plugin’s health flips to Quarantined in the manager view. Filtered by kind (indexed dispatch); events drain on the shared runtime via a Channel sink, OFF the keystroke path (the bus calls the sink lock-dropped). Holds a Weak<Self> so the drain task never keeps the loader alive — the loop ends when the loader drops. Called once by [install]; a no-op if no bus/runtime was wired (the minimal test constructor).

Source

pub fn subscribe_mode_gates(self: &Arc<Self>)

PM.3: react to <id>.enabled changes — the config gate for a plugin’s default mode. On a :set <id>.enabled=<bool> (an OptionChanged), map the option back to the loaded plugin’s default_mode and request the mode’s enablement to match, so the toggle activates / deactivates it live. Mirrors Self::subscribe_health; a no-op when no bus/runtime was wired.

Source

pub fn register_ex_commands(self: &Arc<Self>)

Self-register the :plugin-load / :plugin-unload / :plugin-reload ex-commands into the runtime-mutable command registry (option A — the loader owns its full command surface; zero host code). Plain command names resolve directly via id_by_name (no expand_alias host entry), exactly like plugin-contributed ex-commands. Called once by [install] after the loader is constructed; a no-op (logged) if no command registry was wired.

The apply closures capture a Weak<Self>. The command registry holds them and the loader holds the registry, so a strong capture was a cycle that kept the loader — and its plugin host’s engine and threads — alive after the editor that booted it had been dropped.

Source

pub async fn run_bulk( &self, op: BulkOp, on_leg: &(dyn Fn(usize, usize, &str) + Send + Sync), ) -> BulkReport

Run op over every loaded plugin, reporting each leg as it starts.

Sequential, deliberately. The obvious reading is that N plugins should run concurrently, and it is wrong three times over: cargo already saturates the machine, so N of them contend rather than parallelise (and can exhaust the disk — a full build tree is tens of gigabytes); every leg finishes by reloading, which mutates the shared registries by copy-on-write RCU, so overlapping legs race to publish; and a user watching the view wants to read which plugin is building now, not six rows all claiming to be.

A leg’s failure never stops the next one — the same rule install_all follows at boot, for the same reason: one broken plugin should cost you that plugin, not the rest.

on_leg(done, total, name) fires BEFORE each leg runs, which is what lets the :plugins view say which plugin it is on rather than only what it finished. A caller with nothing to show passes a no-op.

Source

pub async fn rebuild_all(&self) -> BulkReport

Rebuild every loaded plugin from the source it already has, then reload each. See Self::run_bulk.

Source

pub async fn update_all(&self) -> BulkReport

Update every loaded plugin: bring each source up to date, rebuild, reload. Pinned plugins are skipped. See Self::run_bulk.

Source

pub async fn reload_all(&self) -> BulkReport

Re-instantiate every loaded plugin from the artifact already on disk — no build, no network. See Self::run_bulk.

Source

pub fn removable_plugin_dirs(&self) -> Vec<(String, PathBuf)>

Staged plugin directories that nothing this session claims — what clean would remove.

A directory is removable only when all of these hold, and each clause is here because dropping it deletes something a user wanted:

  1. Not loaded. The obvious one.
  2. Not a load FAILURE this session. A plugin that tried and broke is still a plugin the user asked for; cleaning it would turn “my plugin is failing” into “my plugin is gone” and hide the error the view was showing.
  3. Not init. That is the user’s own configuration, not a plugin, and it is never in the loaded set under that name.
  4. Carries a .source marker. Provenance is what makes removal recoverable — with it the directory can be re-resolved and rebuilt, without it the bytes are the only copy. A hand-staged directory has no marker, and is exactly the case where deleting is unrecoverable.

Returns (name, path) pairs in name order. Reading only — the caller decides whether to act, which is what lets :plugin-clean show the list and :plugin-clean! act on it.

Source

pub fn clean(&self, names: &[String]) -> BulkReport

Remove the named staged plugin directories.

Takes names rather than re-deriving the list, so the thing the user confirmed is the thing that gets deleted — Effect::Confirm carries the payload for this reason (effect.rs, IX.1). Re-deriving after the prompt would let a reload land in between and change the answer.

Each name is re-checked against Self::removable_plugin_dirs before its directory goes: a confirmation the user left sitting while a plugin loaded must not delete the plugin that just arrived.

Source

pub async fn load_path( &self, dir: &Path, tier: TrustTier, ) -> Result<PluginId, PluginLoaderError>

Load a single plugin from an explicit directory — the :plugin-load <path> entry point (PL8.C). Unlike discover_and_load (a tree scan that silently skips non-plugin dirs), a direct request surfaces a bad path as a PluginLoaderError::Discovery the user sees.

Source

pub fn builds_in_flight(&self) -> usize

PM.8b: how many builds are running right now.

The :plugins headerline reads this, per the async-buffer-status-in-headerline rule — a build takes seconds to minutes and the user needs to see it is happening somewhere other than a status line that the next echo will overwrite.

Source

pub async fn rebuild(&self, name: &str) -> Result<(), String>

PM.8b: force a fresh build of name from its recorded source, then reload it.

“Force” is the difference from an ordinary load: the build service short-circuits on a matching stamp, which is exactly what you do NOT want when a user pressed rebuild. The stamp is removed first so the build is unconditional — the user asked, not the staleness check.

Returns the error when the rebuild could not happen or did not succeed, having left the plugin as it was. A failed rebuild never unloads a working plugin: PM.5’s StaleKept keeps the old artifact, and this reloads from it.

Blocking work runs on spawn_blocking; only the reload is awaited.

Source

pub async fn update(&self, name: &str) -> Result<(), String>

Bring name up to date with its upstream, then rebuild and reload it.

The difference from Self::rebuild is one argument — the RefreshPolicy the resolver runs under — but it is the whole verb: rebuild compiles the source you already have, update goes and gets a newer one first.

What “newer” means is the source’s to answer, and three of the four kinds answer it without any work here:

sourceupdate
Git { rev: None }fetch, move to the tracked head, rebuild
Git { rev: Some(_) }declines — a pin is the answer already
Local(_)rebuild; the directory IS the source, so it is always current
Prebuilt { url }re-download (the resolver fetches unconditionally)

The pinned arm declines rather than silently rebuilding, because those are different outcomes and a user who pinned a plugin and then pressed update is owed the reason nothing moved.

Source

pub fn take_required(&self) -> Vec<RequiredSpec>

PM.7b: take the plugins declared via require so far, leaving the queue empty.

Drained exactly once per boot by the install task. Draining rather than reading is what stops a second call from resolving, building and loading the same set twice.

Source

pub fn unload(&self, target: &str) -> Option<TeardownReport>

Unload the plugin named target (its manifest id, or its numeric plugin id): abort its actor tasks and reverse every registry contribution via PluginTeardown. Returns the TeardownReport (what each surface removed), or None if no loaded plugin matched. Synchronous — teardown and JoinHandle::abort don’t await — so an ex-command apply closure can call it directly. Idempotent per the teardown contract.

Source

pub async fn reload( &self, target: &str, tier: TrustTier, ) -> Result<PluginId, PluginLoaderError>

Reload the plugin named target: unload it, then re-load_path from its recorded source directory — minting a fresh Store with a fresh, untripped Quarantine (the reload contract, teardown.rs §“Why no reload method”). Errors if target names no loaded plugin (NotLoaded) or it has no on-disk source (NotReloadable).

Source

pub async fn sync_init( &self, init_dir: &Path, tier: TrustTier, ) -> Result<PluginId, PluginLoaderError>

Load the init config if it isn’t loaded, or reload it if it is — the idempotent “make init reflect what’s on disk” the auto-reload watcher (PL8.D.4) fires on every change to <config>/lattice/init/. First good build loads; a rebuild reloads (unbinding the old keymaps / commands and re-applying); a broken rebuild leaves init unloaded (reload unloads before it fails to re-load), which the next good build heals — is_loaded is then false, so this loads rather than reloads. Errors propagate for the caller to log; never panics.

Source

pub async fn reload_config( &self, ) -> Result<ReloadConfigReport, PluginLoaderError>

Rebuild the user’s init.rs in place, then (re)load it — the source → artifact step :reload-config and the plugins view’s rebuild-of-init need and that the bare artifact reload (reload / load_path) cannot do.

This mirrors the boot path (install::build_init + sync_init). Before it existed, :reload-config re-instantiated the stale on-disk init.wasm, so an edited option (e.g. tabstop) took effect only after a full restart — the one path that recompiled. init’s SourceRecord is Unknown (it is discovered directly, never “installed”, so it has no .source marker) and it compiles in place rather than staging into the plugin cache, so it cannot go through the generic rebuild_with pipeline — hence this dedicated seam.

Loads at the Bundled tier the boot path uses — the user’s own config is trusted, not a third-party plugin.

A build failure with a previous artifact present keeps the last good config running and reports ConfigBuildStatus::BuildFailed with the compiler error (an Ok whose applied_new_config is false); a build failure with no previous artifact surfaces as Err — there is nothing to load.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
§

impl<T> Pointee for T

§

type Pointer = u32

§

fn debug( pointer: <T as Pointee>::Pointer, f: &mut Formatter<'_>, ) -> Result<(), Error>

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more