zacoons 2025-07-23 11:52:25 -05:00 committed by GitHub
commit e76b254650
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 188 additions and 5 deletions

9
Cargo.lock generated
View File

@ -1395,6 +1395,10 @@ dependencies = [
"stable_deref_trait", "stable_deref_trait",
] ]
[[package]]
name = "helix"
version = "0.1.0"
[[package]] [[package]]
name = "helix-core" name = "helix-core"
version = "25.7.1" version = "25.7.1"
@ -1565,6 +1569,7 @@ dependencies = [
"indexmap", "indexmap",
"indoc", "indoc",
"libc", "libc",
"libloading",
"log", "log",
"nucleo", "nucleo",
"once_cell", "once_cell",
@ -1983,9 +1988,9 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.8.7" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.53.2", "windows-targets 0.53.2",

View File

@ -15,6 +15,9 @@ members = [
"helix-stdx", "helix-stdx",
"xtask", "xtask",
] ]
exclude = [
"helix-plugins"
]
default-members = [ default-members = [
"helix-term" "helix-term"
@ -61,3 +64,11 @@ repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com" homepage = "https://helix-editor.com"
license = "MPL-2.0" license = "MPL-2.0"
rust-version = "1.82" rust-version = "1.82"
[package]
name = "helix"
version = "0.1.0"
[lib]
name = "helix"
path = "helix-core/src/lib.rs"

View File

@ -92,6 +92,7 @@ grep-regex = "0.1.13"
grep-searcher = "0.1.14" grep-searcher = "0.1.14"
dashmap = "6.0" dashmap = "6.0"
libloading = "0.8.8"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

View File

@ -26,6 +26,7 @@ use crate::{
handlers, handlers,
job::Jobs, job::Jobs,
keymap::Keymaps, keymap::Keymaps,
plugins::Plugins,
ui::{self, overlay::overlaid}, ui::{self, overlay::overlaid},
}; };
@ -234,6 +235,9 @@ impl Application {
]) ])
.context("build signal handler")?; .context("build signal handler")?;
// after everything has been set up, load all plugins
Plugins::load_all(&config.load());
let app = Self { let app = Self {
compositor, compositor,
terminal, terminal,
@ -414,6 +418,9 @@ impl Application {
self.terminal self.terminal
.reconfigure(default_config.editor.clone().into())?; .reconfigure(default_config.editor.clone().into())?;
Plugins::config_updated(&default_config);
// Store new config // Store new config
self.config.store(Arc::new(default_config)); self.config.store(Arc::new(default_config));
Ok(()) Ok(())

View File

@ -63,6 +63,7 @@ use crate::{
compositor::{self, Component, Compositor}, compositor::{self, Component, Compositor},
filter_picker_entry, filter_picker_entry,
job::Callback, job::Callback,
plugins::Plugins,
ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent}, ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent},
}; };
@ -246,7 +247,9 @@ impl MappableCommand {
pub fn execute(&self, cx: &mut Context) { pub fn execute(&self, cx: &mut Context) {
match &self { match &self {
Self::Typable { name, args, doc: _ } => { 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 { let mut cx = compositor::Context {
editor: cx.editor, editor: cx.editor,
jobs: cx.jobs, jobs: cx.jobs,

View File

@ -3692,7 +3692,13 @@ fn execute_command_line(
match typed::TYPABLE_COMMAND_MAP.get(command) { match typed::TYPABLE_COMMAND_MAP.get(command) {
Some(cmd) => execute_command(cx, cmd, rest, event), 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(()), None => Ok(()),
} }
} }
@ -3813,7 +3819,10 @@ fn complete_command_line(editor: &Editor, input: &str) -> Vec<ui::prompt::Comple
if complete_command { if complete_command {
fuzzy_match( fuzzy_match(
input, 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, false,
) )
.into_iter() .into_iter()

View File

@ -14,6 +14,8 @@ pub struct Config {
pub theme: Option<String>, pub theme: Option<String>,
pub keys: HashMap<Mode, KeyTrie>, pub keys: HashMap<Mode, KeyTrie>,
pub editor: helix_view::editor::Config, pub editor: helix_view::editor::Config,
pub plugins: Option<Vec<String>>,
pub plugin: Option<toml::Table>,
} }
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
@ -22,6 +24,8 @@ pub struct ConfigRaw {
pub theme: Option<String>, pub theme: Option<String>,
pub keys: Option<HashMap<Mode, KeyTrie>>, pub keys: Option<HashMap<Mode, KeyTrie>>,
pub editor: Option<toml::Value>, pub editor: Option<toml::Value>,
pub plugins: Option<Vec<String>>,
pub plugin: Option<toml::Table>,
} }
impl Default for Config { impl Default for Config {
@ -30,6 +34,8 @@ impl Default for Config {
theme: None, theme: None,
keys: keymap::default(), keys: keymap::default(),
editor: helix_view::editor::Config::default(), editor: helix_view::editor::Config::default(),
plugins: None,
plugin: None,
} }
} }
} }
@ -84,10 +90,22 @@ impl Config {
.map_err(ConfigLoadError::BadConfig)?, .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 { Config {
theme: local.theme.or(global.theme), theme: local.theme.or(global.theme),
keys, keys,
editor, editor,
plugins,
plugin: local.plugin.or(global.plugin), // TODO: merge
} }
} }
// if any configs are invalid return that first // if any configs are invalid return that first
@ -107,6 +125,8 @@ impl Config {
|| Ok(helix_view::editor::Config::default()), || Ok(helix_view::editor::Config::default()),
|val| val.try_into().map_err(ConfigLoadError::BadConfig), |val| val.try_into().map_err(ConfigLoadError::BadConfig),
)?, )?,
plugins: config.plugins,
plugin: config.plugin,
} }
} }

View File

@ -10,6 +10,7 @@ pub mod events;
pub mod health; pub mod health;
pub mod job; pub mod job;
pub mod keymap; pub mod keymap;
pub mod plugins;
pub mod ui; pub mod ui;
use std::path::Path; use std::path::Path;

View File

@ -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
}
}