pull/14042/merge
zacoons 2025-07-24 19:21:34 +02:00 committed by GitHub
commit c4f8130399
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",
]
[[package]]
name = "helix"
version = "0.1.0"
[[package]]
name = "helix-core"
version = "25.7.1"
@ -1565,6 +1569,7 @@ dependencies = [
"indexmap",
"indoc",
"libc",
"libloading",
"log",
"nucleo",
"once_cell",
@ -1983,9 +1988,9 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "libloading"
version = "0.8.7"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
dependencies = [
"cfg-if",
"windows-targets 0.53.2",

View File

@ -15,6 +15,9 @@ members = [
"helix-stdx",
"xtask",
]
exclude = [
"helix-plugins"
]
default-members = [
"helix-term"
@ -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"

View File

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

View File

@ -26,6 +26,7 @@ use crate::{
handlers,
job::Jobs,
keymap::Keymaps,
plugins::Plugins,
ui::{self, overlay::overlaid},
};
@ -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,
@ -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(())

View File

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

View File

@ -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(()),
}
}
@ -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()

View File

@ -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)]
@ -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 {
@ -30,6 +34,8 @@ impl Default for Config {
theme: None,
keys: keymap::default(),
editor: helix_view::editor::Config::default(),
plugins: None,
plugin: None,
}
}
}
@ -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
@ -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,
}
}

View File

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

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