use std::collections::BTreeMap;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Arc;
use rich::console::ConsoleOptions;
use rich::r#box::Box as BoxStyle;
use rich::segment::Segment;
#[cfg(feature = "syntax")]
use rich::SyntectHighlighter;
use rich::{
CodeHighlighter, CodeHighlighting, Console, ConsoleCodeHighlighting, FenceRenderer,
Highlighter, Text, Theme,
};
use rich_plugin_api::{
is_valid_name, Capability, ComponentFactory, CustomAction, HighlighterFactory, Plugin,
PluginComponent, PluginError, PluginMetadata, PluginRegistrar, SourceRenderer, TextTransform,
PLUGIN_API_VERSION,
};
use crate::transform::Pipeline;
type LocalHighlighterFactory = Box<dyn Fn() -> Box<dyn Highlighter + Send>>;
const DIRECT: &str = "(direct)";
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RegisteredPlugin {
pub metadata: PluginMetadata,
pub capabilities: Vec<Capability>,
}
#[derive(Default)]
pub struct ExtensionRegistry {
highlighters: Vec<(String, LocalHighlighterFactory)>,
code_highlighters: BTreeMap<String, (String, Arc<dyn CodeHighlighter>)>,
themes: BTreeMap<String, (String, Theme)>,
box_styles: BTreeMap<String, (String, BoxStyle)>,
renderers: BTreeMap<String, (String, Arc<dyn SourceRenderer>)>,
fence_renderers: BTreeMap<String, (String, Arc<dyn FenceRenderer>)>,
transforms: BTreeMap<String, (String, Arc<dyn TextTransform>)>,
actions: BTreeMap<String, (String, Arc<dyn CustomAction>)>,
components: BTreeMap<String, (String, ComponentFactory)>,
plugins: Vec<RegisteredPlugin>,
default_code_highlighter: Option<(String, Option<String>)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HighlighterChoiceError {
UnknownHighlighter {
name: String,
available: Vec<String>,
},
UnknownTheme { highlighter: String, theme: String },
}
impl std::fmt::Display for HighlighterChoiceError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HighlighterChoiceError::UnknownHighlighter { name, available } => write!(
f,
"unknown code highlighter {name:?}; available: {}",
available.join(", ")
),
HighlighterChoiceError::UnknownTheme { highlighter, theme } => write!(
f,
"the {highlighter} code highlighter has no theme {theme:?}"
),
}
}
}
impl std::error::Error for HighlighterChoiceError {}
impl ExtensionRegistry {
pub fn new() -> Self {
ExtensionRegistry::default()
}
pub fn with_defaults() -> Self {
let mut registry = ExtensionRegistry::new();
registry
.add_plugin(&BuiltinPlugin)
.expect("the built-in plugin registers cleanly");
registry
}
pub fn with_linked_plugins() -> Result<Self, PluginError> {
let mut registry = ExtensionRegistry::with_defaults();
registry.add_linked_plugins()?;
Ok(registry)
}
pub fn add_linked_plugins(&mut self) -> Result<Vec<String>, PluginError> {
let plugins: Vec<Box<dyn Plugin>> = rich_plugin_api::linked_plugins()
.map(|linked| linked.plugin())
.collect();
self.add_plugin_set(plugins)
}
pub fn add_plugin_set(
&mut self,
plugins: impl IntoIterator<Item = Box<dyn Plugin>>,
) -> Result<Vec<String>, PluginError> {
let mut keyed = Vec::new();
for plugin in plugins {
let metadata =
catch_unwind(AssertUnwindSafe(|| plugin.metadata())).map_err(|panic| {
PluginError::Failed {
plugin: "(unknown)".to_string(),
message: format!("metadata panicked: {}", panic_message(&*panic)),
}
})?;
keyed.push((metadata.id, plugin));
}
keyed.sort_by(|a, b| a.0.cmp(&b.0));
if let Some(pair) = keyed.windows(2).find(|pair| pair[0].0 == pair[1].0) {
return Err(PluginError::DuplicatePlugin {
id: pair[0].0.clone(),
});
}
let mut added = Vec::with_capacity(keyed.len());
for (id, plugin) in keyed {
self.add_plugin(plugin.as_ref())?;
added.push(id);
}
Ok(added)
}
pub fn register_highlighter<F>(&mut self, factory: F) -> &mut Self
where
F: Fn() -> Box<dyn Highlighter + Send> + 'static,
{
self.highlighters
.push((DIRECT.to_string(), Box::new(factory)));
self
}
pub fn register_code_highlighter(
&mut self,
name: &str,
highlighter: Arc<dyn CodeHighlighter>,
) -> Result<(), PluginError> {
if !is_valid_name(name) {
return Err(PluginError::InvalidName {
plugin: DIRECT.to_string(),
name: name.to_string(),
});
}
let capability = Capability::CodeHighlighter(name.to_string());
if let Some(existing) = self.provider(&capability) {
return Err(PluginError::Conflict {
capability,
existing: existing.to_string(),
plugin: DIRECT.to_string(),
});
}
self.code_highlighters
.insert(name.to_string(), (DIRECT.to_string(), highlighter));
Ok(())
}
pub fn add_plugin(&mut self, plugin: &dyn Plugin) -> Result<(), PluginError> {
let metadata = catch_unwind(AssertUnwindSafe(|| plugin.metadata())).map_err(|panic| {
PluginError::Failed {
plugin: "(unknown)".to_string(),
message: format!("metadata panicked: {}", panic_message(&*panic)),
}
})?;
let id = metadata.id.clone();
if !is_valid_name(&id) {
return Err(PluginError::InvalidName {
plugin: id.clone(),
name: id,
});
}
if metadata.api_version != PLUGIN_API_VERSION {
return Err(PluginError::IncompatibleApi {
plugin: id,
built_for: metadata.api_version,
host: PLUGIN_API_VERSION,
});
}
if self.plugins.iter().any(|p| p.metadata.id == id) {
return Err(PluginError::DuplicatePlugin { id });
}
let mut staged = Staged::default();
match catch_unwind(AssertUnwindSafe(|| plugin.register(&mut staged))) {
Ok(result) => result.map_err(|error| PluginError::Failed {
plugin: id.clone(),
message: error.to_string(),
})?,
Err(panic) => {
return Err(PluginError::Failed {
plugin: id,
message: format!("register panicked: {}", panic_message(&*panic)),
})
}
}
if let Some(name) = staged.invalid_names.into_iter().next() {
return Err(PluginError::InvalidName { plugin: id, name });
}
let mut seen = Vec::new();
for capability in &staged.capabilities {
if let Some(existing) = self.provider(capability) {
return Err(PluginError::Conflict {
capability: capability.clone(),
existing: existing.to_string(),
plugin: id,
});
}
if *capability != Capability::Highlighter && seen.contains(&capability) {
return Err(PluginError::Conflict {
capability: capability.clone(),
existing: id.clone(),
plugin: id,
});
}
seen.push(capability);
}
for factory in staged.highlighters {
self.highlighters
.push((id.clone(), Box::new(move || factory())));
}
for (name, value) in staged.code_highlighters {
self.code_highlighters.insert(name, (id.clone(), value));
}
for (name, value) in staged.themes {
self.themes.insert(name, (id.clone(), value));
}
for (name, value) in staged.box_styles {
self.box_styles.insert(name, (id.clone(), value));
}
for (name, value) in staged.renderers {
self.renderers.insert(name, (id.clone(), value));
}
for (language, value) in staged.fence_renderers {
self.fence_renderers.insert(language, (id.clone(), value));
}
for (name, value) in staged.transforms {
self.transforms.insert(name, (id.clone(), value));
}
for (name, value) in staged.actions {
self.actions.insert(name, (id.clone(), value));
}
for (name, value) in staged.components {
self.components.insert(name, (id.clone(), value));
}
self.plugins.push(RegisteredPlugin {
metadata,
capabilities: staged.capabilities,
});
Ok(())
}
fn provider(&self, capability: &Capability) -> Option<&str> {
match capability {
Capability::CodeHighlighter(name) => self.code_highlighters.get(name).map(|e| &e.0),
Capability::Theme(name) => self.themes.get(name).map(|e| &e.0),
Capability::BoxStyle(name) => self.box_styles.get(name).map(|e| &e.0),
Capability::Renderer(name) => self.renderers.get(name).map(|e| &e.0),
Capability::FenceRenderer(language) => self.fence_renderers.get(language).map(|e| &e.0),
Capability::Transform(name) => self.transforms.get(name).map(|e| &e.0),
Capability::Action(name) => self.actions.get(name).map(|e| &e.0),
Capability::Component(name) => self.components.get(name).map(|e| &e.0),
_ => None,
}
.map(String::as_str)
}
pub fn plugins(&self) -> &[RegisteredPlugin] {
&self.plugins
}
pub fn code_highlighter(&self, name: &str) -> Option<Arc<dyn CodeHighlighter>> {
self.code_highlighters.get(name).map(|e| e.1.clone())
}
pub fn code_highlighter_names(&self) -> Vec<&str> {
self.code_highlighters.keys().map(String::as_str).collect()
}
pub fn theme(&self, name: &str) -> Option<&Theme> {
self.themes.get(name).map(|e| &e.1)
}
pub fn theme_names(&self) -> Vec<&str> {
self.themes.keys().map(String::as_str).collect()
}
pub fn box_style(&self, name: &str) -> Option<BoxStyle> {
self.box_styles.get(name).map(|e| e.1)
}
pub fn renderer(&self, name: &str) -> Option<Arc<dyn SourceRenderer>> {
self.renderers.get(name).map(|e| e.1.clone())
}
pub fn set_default_code_highlighter(
&mut self,
name: &str,
theme: Option<&str>,
) -> Result<(), HighlighterChoiceError> {
let highlighter = self.code_highlighter(name).ok_or_else(|| {
HighlighterChoiceError::UnknownHighlighter {
name: name.to_string(),
available: self
.code_highlighter_names()
.into_iter()
.map(str::to_string)
.collect(),
}
})?;
if let Some(theme) = theme {
if !highlighter.themes().iter().any(|t| t == theme) {
return Err(HighlighterChoiceError::UnknownTheme {
highlighter: name.to_string(),
theme: theme.to_string(),
});
}
}
self.default_code_highlighter = Some((name.to_string(), theme.map(str::to_string)));
Ok(())
}
pub fn default_code_highlighter(&self) -> Option<&str> {
self.default_code_highlighter
.as_ref()
.map(|(name, _)| name.as_str())
}
pub fn code_highlighting(&self) -> Option<CodeHighlighting> {
let (name, theme) = self.default_code_highlighter.as_ref()?;
Some(CodeHighlighting {
highlighter: self.code_highlighter(name)?,
theme: theme.clone(),
})
}
pub fn fence_renderer(&self, language: &str) -> Option<Arc<dyn FenceRenderer>> {
self.fence_renderers.get(language).map(|e| e.1.clone())
}
pub fn transform(&self, name: &str) -> Option<Arc<dyn TextTransform>> {
self.transforms.get(name).map(|e| e.1.clone())
}
pub fn actions(&self) -> Vec<(&str, &str, Arc<dyn CustomAction>)> {
self.actions
.iter()
.map(|(name, (plugin, action))| (name.as_str(), plugin.as_str(), action.clone()))
.collect()
}
pub fn component(&self, name: &str) -> Option<ComponentFactory> {
self.components.get(name).map(|e| e.1.clone())
}
pub fn create_component(&self, name: &str) -> Option<Box<dyn PluginComponent>> {
let factory = self.component(name)?;
catch_unwind(AssertUnwindSafe(|| factory())).ok()
}
pub fn component_names(&self) -> Vec<(&str, &str)> {
self.components
.iter()
.map(|(name, (plugin, _))| (name.as_str(), plugin.as_str()))
.collect()
}
pub fn transform_names(&self) -> Vec<&str> {
self.transforms.keys().map(String::as_str).collect()
}
pub fn text_pipeline<'n>(
&self,
names: impl IntoIterator<Item = &'n str>,
) -> Result<Pipeline<Text>, String> {
let mut pipeline = Pipeline::new();
for name in names {
let transform = self.transform(name).ok_or_else(|| name.to_string())?;
pipeline.push(name, Box::new(transform));
}
Ok(pipeline)
}
pub fn register_transform(
&mut self,
name: &str,
transform: Arc<dyn TextTransform>,
) -> Result<(), PluginError> {
if !is_valid_name(name) {
return Err(PluginError::InvalidName {
plugin: DIRECT.to_string(),
name: name.to_string(),
});
}
let capability = Capability::Transform(name.to_string());
if let Some(existing) = self.provider(&capability) {
return Err(PluginError::Conflict {
capability,
existing: existing.to_string(),
plugin: DIRECT.to_string(),
});
}
self.transforms
.insert(name.to_string(), (DIRECT.to_string(), transform));
Ok(())
}
pub fn fences(&self) -> Option<Arc<dyn FenceRenderer>> {
if self.fence_renderers.is_empty() {
return None;
}
let routes = self
.fence_renderers
.iter()
.map(|(language, (_, renderer))| (language.clone(), renderer.clone()))
.collect();
Some(Arc::new(FenceRoutes(routes)))
}
pub fn provided_by(&self, capability: &Capability) -> Option<&str> {
match capability {
Capability::Highlighter => self.highlighters.first().map(|(id, _)| id.as_str()),
other => self.provider(other),
}
}
pub fn install(&self, console: &mut Console) {
for (_, factory) in &self.highlighters {
console.add_highlighter(factory());
}
if let Some(highlighting) = self.code_highlighting() {
console.set_code_highlighting(Some(highlighting));
}
}
}
#[derive(Default)]
struct Staged {
capabilities: Vec<Capability>,
invalid_names: Vec<String>,
highlighters: Vec<HighlighterFactory>,
code_highlighters: Vec<(String, Arc<dyn CodeHighlighter>)>,
themes: Vec<(String, Theme)>,
box_styles: Vec<(String, BoxStyle)>,
renderers: Vec<(String, Arc<dyn SourceRenderer>)>,
fence_renderers: Vec<(String, Arc<dyn FenceRenderer>)>,
transforms: Vec<(String, Arc<dyn TextTransform>)>,
actions: Vec<(String, Arc<dyn CustomAction>)>,
components: Vec<(String, ComponentFactory)>,
}
struct FenceRoutes(BTreeMap<String, Arc<dyn FenceRenderer>>);
impl FenceRenderer for FenceRoutes {
fn render_fence(
&self,
language: &str,
code: &str,
console: &Console,
options: &ConsoleOptions,
) -> Option<Vec<Segment>> {
self.0
.get(language)?
.render_fence(language, code, console, options)
}
}
impl Staged {
fn check(&mut self, name: &str) {
if !is_valid_name(name) {
self.invalid_names.push(name.to_string());
}
}
}
impl PluginRegistrar for Staged {
fn highlighter(&mut self, factory: HighlighterFactory) {
self.capabilities.push(Capability::Highlighter);
self.highlighters.push(factory);
}
fn code_highlighter(&mut self, name: &str, highlighter: Arc<dyn CodeHighlighter>) {
self.check(name);
self.capabilities
.push(Capability::CodeHighlighter(name.to_string()));
self.code_highlighters.push((name.to_string(), highlighter));
}
fn theme(&mut self, name: &str, theme: Theme) {
self.check(name);
self.capabilities.push(Capability::Theme(name.to_string()));
self.themes.push((name.to_string(), theme));
}
fn box_style(&mut self, name: &str, style: BoxStyle) {
self.check(name);
self.capabilities
.push(Capability::BoxStyle(name.to_string()));
self.box_styles.push((name.to_string(), style));
}
fn renderer(&mut self, name: &str, renderer: Arc<dyn SourceRenderer>) {
self.check(name);
self.capabilities
.push(Capability::Renderer(name.to_string()));
self.renderers.push((name.to_string(), renderer));
}
fn fence_renderer(&mut self, language: &str, renderer: Arc<dyn FenceRenderer>) {
self.check(language);
self.capabilities
.push(Capability::FenceRenderer(language.to_string()));
self.fence_renderers.push((language.to_string(), renderer));
}
fn transform(&mut self, name: &str, transform: Arc<dyn TextTransform>) {
self.check(name);
self.capabilities
.push(Capability::Transform(name.to_string()));
self.transforms.push((name.to_string(), transform));
}
fn action(&mut self, name: &str, action: Arc<dyn CustomAction>) {
self.check(name);
self.capabilities.push(Capability::Action(name.to_string()));
self.actions.push((name.to_string(), action));
}
fn component(&mut self, name: &str, factory: ComponentFactory) {
self.check(name);
self.capabilities
.push(Capability::Component(name.to_string()));
self.components.push((name.to_string(), factory));
}
}
pub struct BuiltinPlugin;
impl Plugin for BuiltinPlugin {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new("rich-ext", "rich-ext built-ins", env!("CARGO_PKG_VERSION"))
.description(if cfg!(feature = "syntax") {
"number highlighting and the syntect code highlighter"
} else {
"number highlighting"
})
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.highlighter(Box::new(|| {
Box::new(crate::highlighter::NumberHighlighter::new())
}));
#[cfg(feature = "syntax")]
registrar.code_highlighter("syntect", SyntectHighlighter::shared());
Ok(())
}
}
pub fn install_defaults(console: &mut Console) {
ExtensionRegistry::with_defaults().install(console);
}
fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = panic.downcast_ref::<&str>() {
(*message).to_string()
} else if let Some(message) = panic.downcast_ref::<String>() {
message.clone()
} else {
"a panic with no message".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rich::ColorSystem;
#[test]
fn number_highlighter_installs_and_styles_digits() {
let mut console = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.build();
install_defaults(&mut console);
let out = console.render_str_to_string("n=42");
assert!(out.contains("\x1b[1;36m42\x1b[0m"), "got: {out:?}");
}
#[test]
fn defaults_come_from_the_builtin_plugin() {
let registry = ExtensionRegistry::with_defaults();
assert_eq!(registry.plugins().len(), 1);
assert_eq!(registry.plugins()[0].metadata.id, "rich-ext");
#[cfg(feature = "syntax")]
{
assert_eq!(registry.code_highlighter_names(), ["syntect"]);
assert_eq!(
registry.provided_by(&Capability::CodeHighlighter("syntect".into())),
Some("rich-ext")
);
}
#[cfg(not(feature = "syntax"))]
assert!(registry.code_highlighter_names().is_empty());
}
struct Test {
id: &'static str,
api: u32,
themes: Vec<&'static str>,
fail: bool,
}
impl Plugin for Test {
fn metadata(&self) -> PluginMetadata {
let mut meta = PluginMetadata::new(self.id, self.id, "0.0.0");
meta.api_version = self.api;
meta
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
for name in &self.themes {
registrar.theme(name, Theme::new());
}
if self.fail {
return Err(PluginError::Other("broken".into()));
}
Ok(())
}
}
#[test]
fn a_panicking_plugin_is_refused_and_leaves_the_registry_unchanged() {
struct Panics {
in_metadata: bool,
}
impl Plugin for Panics {
fn metadata(&self) -> PluginMetadata {
if self.in_metadata {
panic!("no metadata");
}
PluginMetadata::new("panics", "Panics", "0.0.0")
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.theme("half", Theme::new());
panic!("register blew up");
}
}
let mut registry = ExtensionRegistry::with_defaults();
for in_metadata in [false, true] {
let error = registry.add_plugin(&Panics { in_metadata }).unwrap_err();
assert!(matches!(error, PluginError::Failed { .. }), "{error:?}");
let expected = if in_metadata {
"no metadata"
} else {
"register blew up"
};
assert!(error.to_string().contains(expected), "{error}");
}
assert_eq!(registry.plugins().len(), 1);
assert_eq!(
registry.provided_by(&Capability::Theme("half".into())),
None
);
}
#[test]
fn provided_by_highlighter_names_who_registered_it() {
assert_eq!(
ExtensionRegistry::new().provided_by(&Capability::Highlighter),
None
);
assert_eq!(
ExtensionRegistry::with_defaults().provided_by(&Capability::Highlighter),
Some("rich-ext")
);
let mut direct = ExtensionRegistry::new();
direct.register_highlighter(|| Box::new(rich::highlighter::ReprHighlighter::new()));
assert_eq!(direct.provided_by(&Capability::Highlighter), Some(DIRECT));
}
fn plugin(id: &'static str, themes: &[&'static str]) -> Test {
Test {
id,
api: PLUGIN_API_VERSION,
themes: themes.to_vec(),
fail: false,
}
}
#[test]
fn plugins_register_and_are_listed_in_order() {
let mut registry = ExtensionRegistry::new();
registry.add_plugin(&plugin("a", &["dark"])).unwrap();
registry.add_plugin(&plugin("b", &["light"])).unwrap();
let ids: Vec<_> = registry
.plugins()
.iter()
.map(|p| p.metadata.id.as_str())
.collect();
assert_eq!(ids, ["a", "b"]);
assert!(registry.theme("dark").is_some() && registry.theme("light").is_some());
assert_eq!(registry.theme_names(), ["dark", "light"]);
}
#[test]
fn fence_renderers_route_by_language_and_conflict_by_language() {
struct Tag(&'static str);
impl FenceRenderer for Tag {
fn render_fence(
&self,
_language: &str,
_code: &str,
_console: &Console,
_options: &ConsoleOptions,
) -> Option<Vec<Segment>> {
Some(vec![Segment::new(self.0, None), Segment::line()])
}
}
struct Fences(&'static str, &'static str);
impl Plugin for Fences {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new(self.0, self.0, "0.0.0")
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.fence_renderer(self.1, Arc::new(Tag(self.1)));
Ok(())
}
}
let mut registry = ExtensionRegistry::with_defaults();
assert!(registry.fences().is_none());
registry.add_plugin(&Fences("diagrams", "mermaid")).unwrap();
registry.add_plugin(&Fences("charts", "vega")).unwrap();
let conflict = registry
.add_plugin(&Fences("other", "mermaid"))
.unwrap_err();
assert!(
matches!(conflict, PluginError::Conflict { .. }),
"{conflict}"
);
assert_eq!(
registry.provided_by(&Capability::FenceRenderer("mermaid".into())),
Some("diagrams")
);
#[cfg(feature = "markdown")]
{
let md = rich::markdown::Markdown::new(
"```mermaid\nx\n```\n\n```vega\ny\n```\n\n```rust\nz\n```",
)
.fence_renderer(registry.fences().unwrap());
let console = Console::builder().width(30).color_system(None).build();
let out = console.render_to_string(&md);
assert!(out.contains("mermaid") && out.contains("vega"), "{out}");
assert!(out.contains('z'), "the rust fence is still code: {out}");
}
}
#[cfg(feature = "syntax")]
#[test]
fn a_default_code_highlighter_is_chosen_by_name_and_installed() {
let mut registry = ExtensionRegistry::with_defaults();
assert!(registry.code_highlighting().is_none());
let error = registry
.set_default_code_highlighter("nope", None)
.unwrap_err();
assert_eq!(
error.to_string(),
"unknown code highlighter \"nope\"; available: syntect"
);
assert!(matches!(
registry.set_default_code_highlighter("syntect", Some("no-theme")),
Err(HighlighterChoiceError::UnknownTheme { .. })
));
registry
.set_default_code_highlighter("syntect", Some("ansi_dark"))
.unwrap();
assert_eq!(registry.default_code_highlighter(), Some("syntect"));
let mut console = Console::builder()
.width(30)
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.build();
registry.install(&mut console);
assert_eq!(
console.code_highlighting().and_then(|h| h.theme.as_deref()),
Some("ansi_dark")
);
let out = console.render_to_string(&rich::Syntax::new("def f(): pass", "python"));
assert!(out.contains("\x1b[94mdef"), "{out:?}");
assert!(!out.contains("38;2;"), "{out:?}");
}
#[cfg(feature = "syntax")]
#[test]
fn code_highlighters_register_directly() {
let mut registry = ExtensionRegistry::with_defaults();
registry
.register_code_highlighter("mine", SyntectHighlighter::shared())
.unwrap();
assert_eq!(registry.code_highlighter_names(), ["mine", "syntect"]);
assert_eq!(
registry.provided_by(&Capability::CodeHighlighter("mine".into())),
Some("(direct)")
);
assert!(matches!(
registry.register_code_highlighter("syntect", SyntectHighlighter::shared()),
Err(PluginError::Conflict { .. })
));
assert!(matches!(
registry.register_code_highlighter("Bad Name", SyntectHighlighter::shared()),
Err(PluginError::InvalidName { .. })
));
}
#[test]
fn plugins_contribute_text_transforms() {
struct Shout;
impl TextTransform for Shout {
fn transform(&self, text: Text) -> Result<Text, PluginError> {
Ok(Text::new(text.plain().to_uppercase()))
}
}
struct Transforms;
impl Plugin for Transforms {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new("transforms", "Transforms", "1.0.0")
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.transform("shout", Arc::new(Shout));
Ok(())
}
}
let mut registry = ExtensionRegistry::new();
registry.add_plugin(&Transforms).unwrap();
registry
.register_transform(
"keep",
Arc::new(crate::transform::KeepLines::new("a").unwrap()),
)
.unwrap();
assert_eq!(registry.transform_names(), ["keep", "shout"]);
assert_eq!(
registry.plugins()[0].capabilities,
[Capability::Transform("shout".into())]
);
assert_eq!(
registry.provided_by(&Capability::Transform("keep".into())),
Some("(direct)")
);
let pipeline = registry.text_pipeline(["keep", "shout"]).unwrap();
assert_eq!(pipeline.names(), ["keep", "shout"]);
let text = pipeline.apply(Text::new("abc\nxyz\n")).unwrap();
assert_eq!(text.plain(), "ABC\n");
assert_eq!(
registry.text_pipeline(["shout", "nope"]).unwrap_err(),
"nope"
);
assert!(matches!(
registry.register_transform("shout", Arc::new(Shout)),
Err(PluginError::Conflict { .. })
));
}
#[test]
fn an_incompatible_api_version_is_refused_with_both_versions() {
let mut registry = ExtensionRegistry::new();
let mut old = plugin("old", &["x"]);
old.api = PLUGIN_API_VERSION + 1;
let error = registry.add_plugin(&old).unwrap_err();
assert_eq!(
error,
PluginError::IncompatibleApi {
plugin: "old".into(),
built_for: PLUGIN_API_VERSION + 1,
host: PLUGIN_API_VERSION,
}
);
assert!(registry.plugins().is_empty() && registry.theme("x").is_none());
}
#[test]
fn duplicate_ids_and_conflicting_names_are_refused_and_nothing_is_kept() {
let mut registry = ExtensionRegistry::new();
registry.add_plugin(&plugin("a", &["dark"])).unwrap();
assert_eq!(
registry.add_plugin(&plugin("a", &["other"])).unwrap_err(),
PluginError::DuplicatePlugin { id: "a".into() }
);
let error = registry
.add_plugin(&plugin("b", &["fresh", "dark"]))
.unwrap_err();
assert_eq!(
error,
PluginError::Conflict {
capability: Capability::Theme("dark".into()),
existing: "a".into(),
plugin: "b".into(),
}
);
assert!(registry.theme("fresh").is_none());
assert_eq!(registry.plugins().len(), 1);
assert!(matches!(
registry.add_plugin(&plugin("c", &["twice", "twice"])),
Err(PluginError::Conflict { .. })
));
}
#[test]
fn invalid_names_and_failed_registration_are_refused() {
let mut registry = ExtensionRegistry::new();
assert!(matches!(
registry.add_plugin(&plugin("Bad Id", &[])),
Err(PluginError::InvalidName { .. })
));
assert!(matches!(
registry.add_plugin(&plugin("ok", &["\u{1b}]0;x"])),
Err(PluginError::InvalidName { .. })
));
let mut broken = plugin("broken", &["kept-not"]);
broken.fail = true;
let error = registry.add_plugin(&broken).unwrap_err();
assert_eq!(
error,
PluginError::Failed {
plugin: "broken".into(),
message: "broken".into(),
}
);
assert!(registry.theme("kept-not").is_none() && registry.plugins().is_empty());
}
#[test]
fn plugins_register_actions_for_interactive_views() {
struct Reveal;
impl CustomAction for Reveal {
fn label(&self) -> String {
"Reveal in file manager".into()
}
fn key(&self) -> Option<String> {
Some("ctrl+r".into())
}
fn applies(&self, kind: &str, _: &str) -> bool {
kind == "file"
}
}
struct Actions;
impl Plugin for Actions {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new("actions", "Actions", "0.0.0")
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.action("reveal", Arc::new(Reveal));
Ok(())
}
}
let mut registry = ExtensionRegistry::new();
registry.add_plugin(&Actions).unwrap();
let actions = registry.actions();
assert_eq!(actions.len(), 1);
let (name, plugin, action) = &actions[0];
assert_eq!((*name, *plugin), ("reveal", "actions"));
assert!(action.applies("file", "a.txt") && !action.applies("row", "x"));
assert_eq!(action.run("file", "a.txt"), Ok(None));
assert_eq!(
registry.plugins()[0].capabilities,
[Capability::Action("reveal".into())]
);
struct Again;
impl Plugin for Again {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new("again", "Again", "0.0.0")
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.action("reveal", Arc::new(Reveal));
Ok(())
}
}
assert!(matches!(
registry.add_plugin(&Again),
Err(PluginError::Conflict { .. })
));
}
#[test]
fn plugins_register_components_by_name() {
use rich_plugin_api::component::{
ComponentContext, ComponentEvent, ComponentFlow, ComponentView,
};
struct Blank;
impl PluginComponent for Blank {
fn handle(&mut self, _: &ComponentEvent, _: &ComponentContext<'_>) -> ComponentFlow {
ComponentFlow::Ignored
}
fn render(&self, _: &ComponentContext<'_>) -> ComponentView {
ComponentView::default()
}
}
struct Views;
impl Plugin for Views {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new("views", "Views", "0.0.0")
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.component("blank", Arc::new(|| Box::new(Blank)));
registrar.component("broken", Arc::new(|| panic!("no")));
Ok(())
}
}
let mut registry = ExtensionRegistry::new();
registry.add_plugin(&Views).unwrap();
assert_eq!(
registry.component_names(),
[("blank", "views"), ("broken", "views")]
);
assert_eq!(
registry.provided_by(&Capability::Component("blank".into())),
Some("views")
);
assert!(registry.create_component("blank").is_some());
assert!(registry.create_component("broken").is_none());
assert!(
registry.create_component("nope").is_none() && registry.component("nope").is_none()
);
struct Again;
impl Plugin for Again {
fn metadata(&self) -> PluginMetadata {
PluginMetadata::new("again", "Again", "0.0.0")
}
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError> {
registrar.component("blank", Arc::new(|| Box::new(Blank)));
Ok(())
}
}
assert!(matches!(
registry.add_plugin(&Again),
Err(PluginError::Conflict { .. })
));
}
}