buffer specific keybindings

pull/8675/merge^2
mattwparas 1 year ago
parent 34144490ec
commit 3ee5829ed7

2
Cargo.lock generated

@ -2732,7 +2732,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "steel-core"
version = "0.2.0"
version = "0.4.0"
dependencies = [
"abi_stable",
"anyhow",

@ -48,7 +48,7 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
etcetera = "0.8"
textwrap = "0.16.0"
steel-core = { path = "../../../steel/crates/steel-core", version = "0.2.0", features = ["modules", "anyhow", "blocking_requests"] }
steel-core = { path = "../../../steel/crates/steel-core", version = "0.4.0", features = ["modules", "anyhow", "blocking_requests"] }
[dev-dependencies]

@ -67,7 +67,7 @@ grep-regex = "0.1.11"
grep-searcher = "0.1.11"
# plugin support
steel-core = { path = "../../../steel/crates/steel-core", version = "0.2.0", features = ["modules", "anyhow", "blocking_requests"] }
steel-core = { path = "../../../steel/crates/steel-core", version = "0.4.0", features = ["modules", "anyhow", "blocking_requests"] }
dlopen = "0.1.8"
dlopen_derive = "0.1.4"

@ -55,7 +55,7 @@ use movement::Movement;
use crate::{
args,
commands::engine::CallbackQueue,
commands::engine::{CallbackQueue, ScriptingEngine},
compositor::{self, Component, Compositor},
filter_picker_entry,
job::Callback,
@ -200,29 +200,7 @@ impl MappableCommand {
pub fn execute(&self, cx: &mut Context) {
log::info!("Running command");
// TODO: Move this out to a standalone function
while let Some(callback) = CallbackQueue::dequeue() {
log::info!("Found callback: {}", callback);
if let Err(e) = ENGINE.with(|x| {
let mut guard = x.borrow_mut();
{
let res = guard.run_with_reference::<Context, Context>(
cx,
"*context*",
&format!("({} *context*)", callback),
);
res
}
}) {
cx.editor.set_error(format!("{}", e));
}
}
match &self {
// TODO: @Matt - Add delegating to the engine to run scripts here
Self::Typable { name, args, doc: _ } => {
let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
// TODO: Swap the order to allow overriding the existing commands?
@ -235,33 +213,8 @@ impl MappableCommand {
if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
cx.editor.set_error(format!("{}", e));
}
} else if ENGINE.with(|x| x.borrow().global_exists(name)) {
let args = steel::List::from(
args.iter()
.map(|x| x.clone().into_steelval().unwrap())
.collect::<Vec<_>>(),
);
if let Err(e) = ENGINE.with(|x| {
let mut guard = x.borrow_mut();
{
guard
.register_value("_helix_args", steel::rvals::SteelVal::ListV(args));
let res = guard.run_with_reference::<Context, Context>(
cx,
"*context*",
&format!("(apply {} (cons *context* _helix_args))", name),
);
guard.register_value("_helix_args", steel::rvals::SteelVal::Void);
res
}
}) {
cx.editor.set_error(format!("{}", e));
}
} else {
ScriptingEngine::call_function_if_global_exists(cx, name, args)
}
}
Self::Static { fun, .. } => (fun)(cx),
@ -682,8 +635,6 @@ fn move_impl(cx: &mut Context, move_fn: MoveFn, dir: Direction, behaviour: Movem
use helix_core::movement::{move_horizontally, move_vertically};
use self::engine::ENGINE;
fn move_char_left(cx: &mut Context) {
move_impl(cx, move_horizontally, Direction::Backward, Movement::Move)
}

@ -1,7 +1,8 @@
use fuzzy_matcher::FuzzyMatcher;
use helix_core::{graphemes, Selection, Tendril};
use helix_core::{graphemes, shellwords::Shellwords, Selection, Tendril};
use helix_view::{
document::Mode, editor::Action, extension::document_id_to_usize, Document, DocumentId, Editor,
document::Mode, editor::Action, extension::document_id_to_usize, input::KeyEvent, Document,
DocumentId, Editor,
};
use once_cell::sync::Lazy;
use steel::{
@ -14,6 +15,8 @@ use steel::{
use std::{
borrow::Cow,
collections::{HashMap, VecDeque},
marker::PhantomData,
path::PathBuf,
sync::Mutex,
};
use std::{
@ -25,8 +28,9 @@ use steel::{rvals::Custom, steel_vm::builtin::BuiltInModule};
use crate::{
compositor::{self, Component, Compositor},
config::Config,
job::{self, Callback},
keymap::{merge_keys, KeyTrie},
keymap::{self, merge_keys, KeyTrie, KeymapResult, Keymaps},
ui::{self, menu::Item, overlay::overlaid, Popup, Prompt, PromptEvent},
};
@ -49,6 +53,189 @@ pub struct ExternalContainersAndModules {
mod components;
// pub struct PluginEngine<T: PluginSystem>(PhantomData<T>);
pub struct ScriptingEngine;
pub trait PluginSystem {
fn initialize();
fn run_initialization_script(cx: &mut Context);
fn call_function_if_global_exists(cx: &mut Context, name: &str, args: Vec<Cow<str>>);
fn call_typed_command_if_global_exists<'a>(
cx: &mut compositor::Context,
input: &'a str,
parts: &'a [&'a str],
event: PromptEvent,
) -> bool;
fn get_doc_for_identifier(ident: &str) -> Option<String>;
}
impl ScriptingEngine {
pub fn initialize() {
initialize_engine();
}
pub fn run_initialization_script(cx: &mut Context) {
run_initialization_script(cx)
}
// Attempt to fetch the keymap for the extension
pub fn get_keymap_for_extension<'a>(cx: &'a mut Context) -> Option<SteelVal> {
// Get the currently activated extension, also need to check the
// buffer type.
let extension = {
let current_focus = cx.editor.tree.focus;
let view = cx.editor.tree.get(current_focus);
let doc = &view.doc;
let current_doc = cx.editor.documents.get(doc);
current_doc
.and_then(|x| x.path())
.and_then(|x| x.extension())
.and_then(|x| x.to_str())
};
if let Some(extension) = extension {
let special_buffer_map = "*buffer-or-extension-keybindings*";
let value = ENGINE.with(|x| x.borrow().extract_value(special_buffer_map).clone());
if let Ok(SteelVal::HashMapV(map)) = value {
if let Some(value) = map.get(&SteelVal::StringV(extension.into())) {
if let SteelVal::Custom(inner) = value {
if let Some(_) = steel::rvals::as_underlying_type::<EmbeddedKeyMap>(
inner.borrow().as_ref(),
) {
return Some(value.clone());
}
}
}
}
}
None
}
pub fn call_function_if_global_exists(cx: &mut Context, name: &str, args: Vec<Cow<str>>) {
if ENGINE.with(|x| x.borrow().global_exists(name)) {
let args = steel::List::from(
args.iter()
.map(|x| x.clone().into_steelval().unwrap())
.collect::<Vec<_>>(),
);
if let Err(e) = ENGINE.with(|x| {
let mut guard = x.borrow_mut();
{
guard.register_value("_helix_args", steel::rvals::SteelVal::ListV(args));
let res = guard.run_with_reference::<Context, Context>(
cx,
"*context*",
&format!("(apply {} (cons *context* _helix_args))", name),
);
guard.register_value("_helix_args", steel::rvals::SteelVal::Void);
res
}
}) {
cx.editor.set_error(format!("{}", e));
}
}
}
pub fn call_typed_command_if_global_exists<'a>(
cx: &mut compositor::Context,
input: &'a str,
parts: &'a [&'a str],
event: PromptEvent,
) -> bool {
if ENGINE.with(|x| x.borrow().global_exists(parts[0])) {
let shellwords = Shellwords::from(input);
let args = shellwords.words();
// We're finalizing the event - we actually want to call the function
if event == PromptEvent::Validate {
// TODO: @Matt - extract this whole API call here to just be inside the engine module
// For what its worth, also explore a more elegant API for calling apply with some arguments,
// this does work, but its a little opaque.
if let Err(e) = ENGINE.with(|x| {
let args = steel::List::from(
args[1..]
.iter()
.map(|x| x.clone().into_steelval().unwrap())
.collect::<Vec<_>>(),
);
let mut guard = x.borrow_mut();
// let mut maybe_callback = None;
let res = {
let mut cx = Context {
register: None,
count: std::num::NonZeroUsize::new(1),
editor: cx.editor,
callback: None,
on_next_key_callback: None,
jobs: cx.jobs,
};
guard.register_value("_helix_args", steel::rvals::SteelVal::ListV(args));
let res = guard.run_with_reference::<Context, Context>(
&mut cx,
"*context*",
&format!("(apply {} (cons *context* _helix_args))", parts[0]),
);
guard.register_value("_helix_args", steel::rvals::SteelVal::Void);
// if let Some(callback) = cx.callback.take() {
// panic!("Found a callback!");
// maybe_callback = Some(callback);
// }
res
};
// TODO: Recursively (or otherwise) keep retrying until we're back
// into the engine context, executing a function. We might need to set up
// some sort of fuel or something
// if let Some(callback) = maybe_callback {
// (callback)(_, cx);
// }
res
}) {
compositor_present_error(cx, e);
};
}
// Global exists
true
} else {
// Global does not exist
false
}
}
pub fn get_doc_for_identifier(ident: &str) -> Option<String> {
if ENGINE.with(|x| x.borrow().global_exists(ident)) {
if let Some(v) = super::engine::ExportedIdentifiers::engine_get_doc(ident) {
return Some(v.into());
}
return Some("Run this plugin command!".into());
}
None
}
}
// External modules that can load via rust dylib. These can then be consumed from
// steel as needed, via the standard FFI for plugin functions.
pub(crate) static EXTERNAL_DYLIBS: Lazy<Arc<RwLock<ExternalContainersAndModules>>> =
@ -134,6 +321,39 @@ pub fn present_error(cx: &mut Context, e: SteelErr) {
cx.jobs.callback(callback);
}
// Key maps
#[derive(Clone, Debug)]
pub struct EmbeddedKeyMap(pub HashMap<Mode, KeyTrie>);
impl Custom for EmbeddedKeyMap {}
pub fn get_keymap() -> EmbeddedKeyMap {
// Snapsnot current configuration for use in forking the keymap
let keymap = Config::load_default().unwrap();
// These are the actual mappings that we want
let map = keymap.keys;
EmbeddedKeyMap(map)
}
// Base level - no configuration
pub fn default_keymap() -> EmbeddedKeyMap {
EmbeddedKeyMap(keymap::default())
}
// Completely empty, allow for overriding
pub fn empty_keymap() -> EmbeddedKeyMap {
EmbeddedKeyMap(HashMap::default())
}
pub fn string_to_embedded_keymap(value: String) -> EmbeddedKeyMap {
EmbeddedKeyMap(serde_json::from_str(&value).unwrap())
}
pub fn merge_keybindings(left: &mut EmbeddedKeyMap, right: EmbeddedKeyMap) {
merge_keys(&mut left.0, right.0)
}
/// Run the initialization script located at `$helix_config/init.scm`
/// This runs the script in the global environment, and does _not_ load it as a module directly
pub fn run_initialization_script(cx: &mut Context) {
@ -238,13 +458,13 @@ impl CallbackQueue {
/// queue that the engine and the config push and pull from. Alternatively, we could use a channel
/// directly, however this was easy enough to set up.
pub struct SharedKeyBindingsEventQueue {
raw_bindings: Arc<Mutex<VecDeque<String>>>,
raw_bindings: Arc<Mutex<Vec<String>>>,
}
impl SharedKeyBindingsEventQueue {
pub fn new() -> Self {
Self {
raw_bindings: std::sync::Arc::new(std::sync::Mutex::new(VecDeque::new())),
raw_bindings: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
}
}
@ -253,17 +473,18 @@ impl SharedKeyBindingsEventQueue {
.raw_bindings
.lock()
.unwrap()
.push_back(other_as_json);
.push(other_as_json);
}
pub fn get() -> Option<HashMap<Mode, KeyTrie>> {
let mut guard = KEYBINDING_QUEUE.raw_bindings.lock().unwrap();
let guard = KEYBINDING_QUEUE.raw_bindings.lock().unwrap();
if let Some(initial) = guard.pop_front() {
let mut initial = serde_json::from_str(&initial).unwrap();
if let Some(first) = guard.get(0).clone() {
let mut initial = serde_json::from_str(first).unwrap();
while let Some(remaining_event) = guard.pop_front() {
let bindings = serde_json::from_str(&remaining_event).unwrap();
// while let Some(remaining_event) = guard.pop_front() {
for remaining_event in guard.iter() {
let bindings = serde_json::from_str(remaining_event).unwrap();
merge_keys(&mut initial, bindings);
}
@ -419,6 +640,14 @@ fn configure_engine() -> std::rc::Rc<std::cell::RefCell<steel::steel_vm::engine:
// );
engine.register_fn("enqueue-callback!", CallbackQueue::enqueue);
engine.register_fn("helix-current-keymap", get_keymap);
engine.register_fn("helix-empty-keymap", empty_keymap);
engine.register_fn("helix-default-keymap", default_keymap);
engine.register_fn("helix-merge-keybindings", merge_keybindings);
engine.register_fn("helix-string->keymap", string_to_embedded_keymap);
// Use this to get at buffer specific keybindings
engine.register_value("*buffer-or-extension-keybindings*", SteelVal::Void);
// Find the workspace
engine.register_fn("helix-find-workspace", || {
@ -509,18 +738,18 @@ fn configure_engine() -> std::rc::Rc<std::cell::RefCell<steel::steel_vm::engine:
let cloned_func = callback_fn.clone();
ENGINE
.with(|x| {
x.borrow_mut()
.with_mut_reference::<Context, Context>(&mut ctx)
.consume(move |engine, mut args| {
// Add the string as an argument to the callback
args.push(input.into_steelval().unwrap());
if let Err(e) = ENGINE.with(|x| {
x.borrow_mut()
.with_mut_reference::<Context, Context>(&mut ctx)
.consume(move |engine, mut args| {
// Add the string as an argument to the callback
args.push(input.into_steelval().unwrap());
engine.call_function_with_args(cloned_func.clone(), args)
})
})
.unwrap();
engine.call_function_with_args(cloned_func.clone(), args)
})
}) {
present_error(&mut ctx, e);
}
},
);
@ -792,6 +1021,9 @@ fn configure_engine() -> std::rc::Rc<std::cell::RefCell<steel::steel_vm::engine:
engine.register_fn("push-component!", push_component);
engine.register_fn("enqueue-thread-local-callback", enqueue_command);
// Create directory since we can't do that in the current state
engine.register_fn("hx.create-directory", create_directory);
let helix_module_path = helix_loader::helix_module_file();
engine
@ -1131,15 +1363,15 @@ fn enqueue_command(cx: &mut Context, callback_fn: SteelVal) {
let cloned_func = callback_fn.clone();
ENGINE
.with(|x| {
x.borrow_mut()
.with_mut_reference::<Context, Context>(&mut ctx)
.consume(move |engine, args| {
engine.call_function_with_args(cloned_func.clone(), args)
})
})
.unwrap();
if let Err(e) = ENGINE.with(|x| {
x.borrow_mut()
.with_mut_reference::<Context, Context>(&mut ctx)
.consume(move |engine, args| {
engine.call_function_with_args(cloned_func.clone(), args)
})
}) {
present_error(&mut ctx, e);
}
},
);
Ok(call)
@ -1147,6 +1379,17 @@ fn enqueue_command(cx: &mut Context, callback_fn: SteelVal) {
cx.jobs.local_callback(callback);
}
// Check that we successfully created a directory?
fn create_directory(path: String) {
let path = helix_core::path::get_canonicalized_path(&PathBuf::from(path)).unwrap();
if path.exists() {
return;
} else {
std::fs::create_dir(path).unwrap();
}
}
// fn enqueue_callback(cx: &mut Context, thunk: SteelVal) {
// log::info!("Enqueueing callback!");

@ -3004,67 +3004,9 @@ pub(super) fn command_mode(cx: &mut Context) {
if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e));
}
} else if ENGINE.with(|x| x.borrow().global_exists(parts[0])) {
let shellwords = Shellwords::from(input);
let args = shellwords.words();
// We're finalizing the event - we actually want to call the function
if event == PromptEvent::Validate {
// TODO: @Matt - extract this whole API call here to just be inside the engine module
// For what its worth, also explore a more elegant API for calling apply with some arguments,
// this does work, but its a little opaque.
if let Err(e) = ENGINE.with(|x| {
let args = steel::List::from(
args[1..]
.iter()
.map(|x| x.clone().into_steelval().unwrap())
.collect::<Vec<_>>(),
);
let mut guard = x.borrow_mut();
// let mut maybe_callback = None;
let res = {
let mut cx = Context {
register: None,
count: std::num::NonZeroUsize::new(1),
editor: cx.editor,
callback: None,
on_next_key_callback: None,
jobs: cx.jobs,
};
guard
.register_value("_helix_args", steel::rvals::SteelVal::ListV(args));
let res = guard.run_with_reference::<Context, Context>(
&mut cx,
"*context*",
&format!("(apply {} (cons *context* _helix_args))", parts[0]),
);
guard.register_value("_helix_args", steel::rvals::SteelVal::Void);
// if let Some(callback) = cx.callback.take() {
// panic!("Found a callback!");
// maybe_callback = Some(callback);
// }
res
};
// TODO: Recursively (or otherwise) keep retrying until we're back
// into the engine context, executing a function. We might need to set up
// some sort of fuel or something
// if let Some(callback) = maybe_callback {
// (callback)(_, cx);
// }
res
}) {
compositor_present_error(cx, e)
};
}
} else if ScriptingEngine::call_typed_command_if_global_exists(cx, input, &parts, event)
{
// Engine handles the other cases
} else if event == PromptEvent::Validate {
cx.editor
.set_error(format!("no such command: '{}'", parts[0]));
@ -3081,18 +3023,8 @@ pub(super) fn command_mode(cx: &mut Context) {
return Some((*doc).into());
}
return Some(format!("{}\nAliases: {}", doc, aliases.join(", ")).into());
} else if ENGINE.with(|x| x.borrow().global_exists(part)) {
if let Some(v) = super::engine::ExportedIdentifiers::engine_get_doc(part) {
return Some(v.into());
}
// if let Ok(v) = ENGINE.with(|x| x.borrow().extract_value(&format!("{part}__doc__"))) {
// if let steel::rvals::SteelVal::StringV(s) = v {
// return Some(s.to_string().into());
// }
// }
return Some("Run this plugin command!".into());
} else if let Some(doc) = ScriptingEngine::get_doc_for_identifier(part) {
return Some(doc.into());
}
None

@ -303,12 +303,14 @@ impl Keymaps {
self.sticky.as_ref()
}
/// Lookup `key` in the keymap to try and find a command to execute. Escape
/// key cancels pending keystrokes. If there are no pending keystrokes but a
/// sticky node is in use, it will be cleared.
pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
pub(crate) fn get_with_map(
&mut self,
keymaps: &HashMap<Mode, KeyTrie>,
mode: Mode,
key: KeyEvent,
) -> KeymapResult {
// TODO: remove the sticky part and look up manually
let keymaps = &*self.map();
// let keymaps = &*self.map();
let keymap = &keymaps[&mode];
if key!(Esc) == key {
@ -356,6 +358,13 @@ impl Keymaps {
None => KeymapResult::Cancelled(self.state.drain(..).collect()),
}
}
/// Lookup `key` in the keymap to try and find a command to execute. Escape
/// key cancels pending keystrokes. If there are no pending keystrokes but a
/// sticky node is in use, it will be cleared.
pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
self.get_with_map(&*self.map(), mode, key)
}
}
impl Default for Keymaps {

@ -1,5 +1,5 @@
use crate::{
commands::{self, OnKeyCallback},
commands::{self, engine::ScriptingEngine, OnKeyCallback},
compositor::{Component, Context, Event, EventResult},
job::{self, Callback},
key,
@ -798,20 +798,26 @@ impl EditorView {
) -> Option<KeymapResult> {
let mut last_mode = mode;
self.pseudo_pending.extend(self.keymaps.pending());
let key_result = self.keymaps.get(mode, event);
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
// Get the currently activated minor modes
// let extension = {
// let current_focus = cxt.editor.tree.focus;
// let view = cxt.editor.tree.get(current_focus);
// let doc = &view.doc;
// let current_doc = cxt.editor.documents.get(doc);
// let key_result = self.keymaps.get(mode, event);
// current_doc
// .and_then(|x| x.path())
// .and_then(|x| x.extension());
// };
// Check the engine for any buffer specific keybindings first
let key_result = ScriptingEngine::get_keymap_for_extension(cxt)
.and_then(|map| {
if let steel::SteelVal::Custom(inner) = map {
if let Some(underlying) = steel::rvals::as_underlying_type::<
commands::engine::EmbeddedKeyMap,
>(inner.borrow().as_ref())
{
return Some(self.keymaps.get_with_map(&underlying.0, mode, event));
}
}
None
})
.unwrap_or_else(|| self.keymaps.get(mode, event));
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
let mut execute_command = |command: &commands::MappableCommand| {
command.execute(cxt);
@ -849,20 +855,6 @@ impl EditorView {
match &key_result {
KeymapResult::Matched(command) => {
// TODO: @Matt - check minor modes here.
// Check current path:
// let current_focus = cxt.editor.tree.focus;
// let view = cxt.editor.tree.get(current_focus);
// let doc = &view.doc;
// let current_doc = cxt.editor.documents.get(doc);
// let extension =
// current_doc.and_then(|x| x.path().and_then(|x| x.to_str().map(|x| x.to_string())))
// cxt.editor.
execute_command(command);
}
KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()),

@ -26,4 +26,4 @@ once_cell = "1.18"
log = "~0.4"
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
helix-core = { version = "0.6", path = "../helix-core" }
steel-core = { path = "../../../steel/crates/steel-core", version = "0.2.0", features = ["modules", "anyhow", "blocking_requests"] }
steel-core = { path = "../../../steel/crates/steel-core", version = "0.4.0", features = ["modules", "anyhow", "blocking_requests"] }

@ -46,7 +46,7 @@ which = "4.4"
parking_lot = "0.12.1"
# plugin support
steel-core = { path = "../../../steel/crates/steel-core", version = "0.2.0", features = ["modules", "anyhow", "blocking_requests"] }
steel-core = { path = "../../../steel/crates/steel-core", version = "0.4.0", features = ["modules", "anyhow", "blocking_requests"] }
[target.'cfg(windows)'.dependencies]

@ -1571,6 +1571,8 @@ impl Editor {
if prev_id != view_id {
log::info!("Changing focus: {:?}", view_id);
// TODO: Consult map for modes to change given file type?
self.enter_normal_mode();
self.ensure_cursor_in_view(view_id);

@ -36,20 +36,33 @@ impl Info {
.unwrap();
let mut text = String::new();
let mut height = 0;
for (item, desc) in body {
let _ = writeln!(
text,
"{:width$} {}",
item.as_ref(),
desc.as_ref(),
width = item_width
);
let mut line_iter = desc.as_ref().lines();
if let Some(first_line) = line_iter.next() {
let _ = writeln!(
text,
"{:width$} {}",
item.as_ref(),
first_line,
width = item_width
);
height += 1;
}
for line in line_iter {
let _ = writeln!(text, "{:width$} {}", "", line, width = item_width);
height += 1;
}
}
Self {
title: title.to_string(),
width: text.lines().map(|l| l.width()).max().unwrap() as u16,
height: body.len() as u16,
// height: (body.len() + newlines) as u16,
height,
text,
}
}

Loading…
Cancel
Save