use std::fmt;
use std::sync::Arc;
pub mod abi;
pub mod component;
mod linked;
pub use component::{ComponentFactory, PluginComponent};
#[doc(hidden)]
pub use linked::__private;
pub use linked::{linked_plugins, LinkedPlugin};
use rich::r#box::Box as BoxStyle;
use rich::{CodeHighlighter, FenceRenderer, Highlighter, Renderable, Text, Theme};
pub const PLUGIN_API_VERSION: u32 = 1;
pub type HighlighterFactory = Box<dyn Fn() -> Box<dyn Highlighter + Send> + Send + Sync>;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct PluginMetadata {
pub id: String,
pub name: String,
pub version: String,
pub api_version: u32,
pub description: Option<String>,
}
impl PluginMetadata {
pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
PluginMetadata {
id: id.into(),
name: name.into(),
version: version.into(),
api_version: PLUGIN_API_VERSION,
description: None,
}
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
pub trait SourceRenderer: Send + Sync {
fn render(&self, source: &str) -> Result<Box<dyn Renderable + Send + Sync>, PluginError>;
}
pub trait TextTransform: Send + Sync {
fn transform(&self, text: Text) -> Result<Text, PluginError>;
}
impl<T: TextTransform + ?Sized> TextTransform for Arc<T> {
fn transform(&self, text: Text) -> Result<Text, PluginError> {
(**self).transform(text)
}
}
pub trait CustomAction: Send + Sync {
fn label(&self) -> String;
fn key(&self) -> Option<String> {
None
}
fn applies(&self, kind: &str, value: &str) -> bool {
let _ = (kind, value);
true
}
fn run(&self, kind: &str, value: &str) -> Result<Option<String>, PluginError> {
let _ = (kind, value);
Ok(None)
}
}
pub trait PluginRegistrar {
fn highlighter(&mut self, factory: HighlighterFactory);
fn code_highlighter(&mut self, name: &str, highlighter: Arc<dyn CodeHighlighter>);
fn theme(&mut self, name: &str, theme: Theme);
fn box_style(&mut self, name: &str, style: BoxStyle);
fn renderer(&mut self, name: &str, renderer: Arc<dyn SourceRenderer>);
fn fence_renderer(&mut self, language: &str, renderer: Arc<dyn FenceRenderer>);
fn transform(&mut self, name: &str, transform: Arc<dyn TextTransform>);
fn action(&mut self, name: &str, action: Arc<dyn CustomAction>) {
let _ = (name, action);
}
fn component(&mut self, name: &str, factory: ComponentFactory) {
let _ = (name, factory);
}
}
pub trait Plugin: Send + Sync {
fn metadata(&self) -> PluginMetadata;
fn register(&self, registrar: &mut dyn PluginRegistrar) -> Result<(), PluginError>;
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Capability {
Highlighter,
CodeHighlighter(String),
Theme(String),
BoxStyle(String),
Renderer(String),
FenceRenderer(String),
Transform(String),
Action(String),
Component(String),
}
impl fmt::Display for Capability {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Capability::Highlighter => write!(f, "highlighter"),
Capability::CodeHighlighter(name) => write!(f, "code highlighter {name:?}"),
Capability::Theme(name) => write!(f, "theme {name:?}"),
Capability::BoxStyle(name) => write!(f, "box style {name:?}"),
Capability::Renderer(name) => write!(f, "renderer {name:?}"),
Capability::FenceRenderer(language) => write!(f, "fence renderer {language:?}"),
Capability::Transform(name) => write!(f, "transform {name:?}"),
Capability::Action(name) => write!(f, "action {name:?}"),
Capability::Component(name) => write!(f, "component {name:?}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum PluginError {
IncompatibleApi {
plugin: String,
built_for: u32,
host: u32,
},
DuplicatePlugin { id: String },
Conflict {
capability: Capability,
existing: String,
plugin: String,
},
InvalidName { plugin: String, name: String },
Failed { plugin: String, message: String },
Other(String),
}
impl fmt::Display for PluginError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PluginError::IncompatibleApi {
plugin,
built_for,
host,
} => write!(
f,
"plugin {plugin:?} was built for plugin API {built_for}, but this host supports \
plugin API {host}; use a version of the plugin built for API {host}"
),
PluginError::DuplicatePlugin { id } => {
write!(f, "a plugin with id {id:?} is already registered")
}
PluginError::Conflict {
capability,
existing,
plugin,
} => write!(
f,
"plugin {plugin:?} registers {capability}, which plugin {existing:?} already provides"
),
PluginError::InvalidName { plugin, name } => write!(
f,
"plugin {plugin:?} uses the invalid name {name:?}: use lowercase letters, digits, \
'-', '_' and '.', starting with a letter or digit"
),
PluginError::Failed { plugin, message } => {
write!(f, "plugin {plugin:?} failed to register: {message}")
}
PluginError::Other(message) => f.write_str(message),
}
}
}
impl std::error::Error for PluginError {}
pub fn is_valid_name(name: &str) -> bool {
name.len() <= 64
&& name
.bytes()
.next()
.is_some_and(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
&& name.bytes().all(|b| {
b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'-' | b'_' | b'.')
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn names_are_safe_cli_values() {
for good in ["syntect", "lumis", "ansi_dark", "a.b-c", "7z"] {
assert!(is_valid_name(good), "{good}");
}
for bad in [
"",
"-",
"-x",
"--help",
".",
"..",
"_x",
"Upper",
"a b",
&"x".repeat(65),
] {
assert!(!is_valid_name(bad), "{bad:?}");
}
}
#[test]
fn metadata_targets_this_api_version() {
let meta = PluginMetadata::new("x", "X", "1.0.0").description("does x");
assert_eq!(meta.api_version, PLUGIN_API_VERSION);
assert_eq!(meta.description.as_deref(), Some("does x"));
}
#[test]
fn names_are_restricted() {
for good in ["syntect", "lumis", "my-plugin_2.x"] {
assert!(is_valid_name(good), "{good}");
}
for bad in [
"",
"Upper",
"has space",
"semi;colon",
"\u{1b}]0;x",
&"a".repeat(65),
] {
assert!(!is_valid_name(bad), "{bad:?}");
}
}
#[test]
fn errors_name_both_sides() {
let conflict = PluginError::Conflict {
capability: Capability::Theme("dark".into()),
existing: "a".into(),
plugin: "b".into(),
};
let message = conflict.to_string();
assert!(message.contains("\"a\"") && message.contains("\"b\"") && message.contains("dark"));
let incompatible = PluginError::IncompatibleApi {
plugin: "old".into(),
built_for: 0,
host: PLUGIN_API_VERSION,
};
assert!(incompatible.to_string().contains("plugin API 0"));
}
}