mirror of https://github.com/helix-editor/helix
Merge 7f43480cf6
into 395a71bf53
commit
e76b254650
|
@ -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",
|
||||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -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"
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue