Skip to content

Rust-based plugin system #14042

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ members = [
"helix-stdx",
"xtask",
]
exclude = [
"helix-plugins"
]

default-members = [
"helix-term"
Expand Down Expand Up @@ -61,3 +64,11 @@ repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
license = "MPL-2.0"
rust-version = "1.82"

[package]
name = "helix"
version = "0.1.0"

[lib]
name = "helix"
path = "helix-core/src/lib.rs"
1 change: 1 addition & 0 deletions helix-term/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ grep-regex = "0.1.13"
grep-searcher = "0.1.14"

dashmap = "6.0"
libloading = "0.8.8"

[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
Expand Down
7 changes: 7 additions & 0 deletions helix-term/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::{
handlers,
job::Jobs,
keymap::Keymaps,
plugins::Plugins,
ui::{self, overlay::overlaid},
};

Expand Down Expand Up @@ -234,6 +235,9 @@ impl Application {
])
.context("build signal handler")?;

// after everything has been set up, load all plugins
Plugins::load_all(&config.load());

let app = Self {
compositor,
terminal,
Expand Down Expand Up @@ -414,6 +418,9 @@ impl Application {

self.terminal
.reconfigure(default_config.editor.clone().into())?;

Plugins::config_updated(&default_config);

// Store new config
self.config.store(Arc::new(default_config));
Ok(())
Expand Down
5 changes: 4 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ use crate::{
compositor::{self, Component, Compositor},
filter_picker_entry,
job::Callback,
plugins::Plugins,
ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent},
};

Expand Down Expand Up @@ -246,7 +247,9 @@ impl MappableCommand {
pub fn execute(&self, cx: &mut Context) {
match &self {
Self::Typable { name, args, doc: _ } => {
if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) {
//if Plugins::call_typed_command(name, cx, args) {
if false {
} else if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) {
let mut cx = compositor::Context {
editor: cx.editor,
jobs: cx.jobs,
Expand Down
13 changes: 11 additions & 2 deletions helix-term/src/commands/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3692,7 +3692,13 @@ fn execute_command_line(

match typed::TYPABLE_COMMAND_MAP.get(command) {
Some(cmd) => execute_command(cx, cmd, rest, event),
None if event == PromptEvent::Validate => Err(anyhow!("no such command: '{command}'")),
None if event == PromptEvent::Validate => {
if Plugins::call_typed_command(cx, command, rest) {
Ok(())
} else {
Err(anyhow!("no such command: '{command}'"))
}
}
None => Ok(()),
}
}
Expand Down Expand Up @@ -3813,7 +3819,10 @@ fn complete_command_line(editor: &Editor, input: &str) -> Vec<ui::prompt::Comple
if complete_command {
fuzzy_match(
input,
TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
TYPABLE_COMMAND_LIST
.iter()
.map(|command| command.name.to_string())
.chain(crate::plugins::Plugins::available_commands()),
false,
)
.into_iter()
Expand Down
20 changes: 20 additions & 0 deletions helix-term/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub struct Config {
pub theme: Option<String>,
pub keys: HashMap<Mode, KeyTrie>,
pub editor: helix_view::editor::Config,
pub plugins: Option<Vec<String>>,
pub plugin: Option<toml::Table>,
}

#[derive(Debug, Clone, PartialEq, Deserialize)]
Expand All @@ -22,6 +24,8 @@ pub struct ConfigRaw {
pub theme: Option<String>,
pub keys: Option<HashMap<Mode, KeyTrie>>,
pub editor: Option<toml::Value>,
pub plugins: Option<Vec<String>>,
pub plugin: Option<toml::Table>,
}

impl Default for Config {
Expand All @@ -30,6 +34,8 @@ impl Default for Config {
theme: None,
keys: keymap::default(),
editor: helix_view::editor::Config::default(),
plugins: None,
plugin: None,
}
}
}
Expand Down Expand Up @@ -84,10 +90,22 @@ impl Config {
.map_err(ConfigLoadError::BadConfig)?,
};

let plugins = match (global.plugins, local.plugins) {
(None, None) => None,
(None, Some(val)) | (Some(val), None) => Some(val),
(Some(global), Some(local)) => {
let mut plugins = global.clone();
plugins.extend(local);
Some(plugins)
}
};

Config {
theme: local.theme.or(global.theme),
keys,
editor,
plugins,
plugin: local.plugin.or(global.plugin), // TODO: merge
}
}
// if any configs are invalid return that first
Expand All @@ -107,6 +125,8 @@ impl Config {
|| Ok(helix_view::editor::Config::default()),
|val| val.try_into().map_err(ConfigLoadError::BadConfig),
)?,
plugins: config.plugins,
plugin: config.plugin,
}
}

Expand Down
1 change: 1 addition & 0 deletions helix-term/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub mod events;
pub mod health;
pub mod job;
pub mod keymap;
pub mod plugins;
pub mod ui;

use std::path::Path;
Expand Down
126 changes: 126 additions & 0 deletions helix-term/src/plugins.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use crate::{compositor::Context, config::Config};

use libloading::Library;
use serde::Deserialize;
use std::{
ffi::{c_char, CString},
sync::Mutex,
};

enum PluginInstance {}
type InitFn = fn(&Config) -> &'static mut PluginInstance;
type ConfigUpdatedFn = fn(&mut PluginInstance, &Config);
type AvailableCmdsFn = fn(&mut PluginInstance) -> Vec<String>;
type CmdFn = fn(&mut PluginInstance, &mut Context, *const c_char);

#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct PluginInfo {
pub name: String,
pub version: String,
pub lib_path: String,
pub min_helix_version: Option<String>,
pub dependencies: Option<Vec<String>>,
}

struct Plugin {
info: PluginInfo,
lib: Library,
instance: &'static mut PluginInstance,
}

static PLUGINS: Mutex<Vec<Plugin>> = Mutex::<Vec<Plugin>>::new(vec![]);

pub struct Plugins {}

impl Plugins {
// Utils for plugin developers

pub fn alloc<T>() -> &'static mut T {
let layout = std::alloc::Layout::new::<T>();
unsafe {
let t = std::alloc::alloc(layout) as *mut T;
t.as_mut().unwrap()
}
}

pub fn get_config_for<'a>(config: &'a Config, name: &str) -> Option<&'a toml::Table> {
if let Some(config) = &config.plugin {
if let Some(config) = config.get(name) {
return config.as_table();
}
}
None
}

// Internal plugin API

pub fn load_all(config: &Config) {
if let Some(plugins) = &config.plugins {
for plugin in plugins {
Self::load(&plugin, config);
}
}
}

pub fn load(info_path: &str, config: &Config) {
let info_str = std::fs::read_to_string(info_path).unwrap();
let info = toml::from_str::<PluginInfo>(&info_str).unwrap();

// TODO: do nothing if this version is too low
// if (VERSION_AND_GIT_HASH < info.min_helix_version) {
// return;
// }

// load the dynamic lib
let lib_path = std::path::Path::new(info_path)
.parent()
.unwrap()
.join(&info.lib_path);
let lib = unsafe { Library::new(lib_path).unwrap() };

// call the plugin's init method (if it has one)
let func_opt = unsafe { lib.get::<InitFn>(b"init") };
let instance = func_opt.unwrap()(config);

PLUGINS.lock().unwrap().push(Plugin {
info,
lib,
instance,
});
}

pub fn config_updated(config: &Config) {
for plugin in PLUGINS.lock().unwrap().iter_mut() {
let func_opt = unsafe { plugin.lib.get::<ConfigUpdatedFn>(b"config_updated") };
if let Ok(func) = func_opt {
func(plugin.instance, config);
}
}
}

pub fn available_commands() -> Vec<String> {
let mut commands = Vec::<String>::new();
for plugin in PLUGINS.lock().unwrap().iter_mut() {
let func_opt = unsafe { plugin.lib.get::<AvailableCmdsFn>(b"available_commands") };
if let Ok(func) = func_opt {
let plugin_commands = func(plugin.instance);
for command in plugin_commands {
commands.push(command.to_string());
}
}
}
commands
}

pub fn call_typed_command(cx: &mut Context, name: &str, args: &str) -> bool {
for plugin in PLUGINS.lock().unwrap().iter_mut() {
let func_opt = unsafe { plugin.lib.get::<CmdFn>(name.as_bytes()) };
if let Ok(func) = func_opt {
func(plugin.instance, cx, CString::new(args).unwrap().as_ptr());
return true;
}
}
false
}
}