mirror of https://github.com/helix-editor/helix
2610 lines
84 KiB
Rust
2610 lines
84 KiB
Rust
use arc_swap::ArcSwapAny;
|
|
use helix_core::{
|
|
extensions::steel_implementations::{rope_module, SteelRopeSlice},
|
|
graphemes,
|
|
regex::Regex,
|
|
shellwords::Shellwords,
|
|
syntax::{AutoPairConfig, SoftWrap},
|
|
Range, Selection, Tendril,
|
|
};
|
|
use helix_event::register_hook;
|
|
use helix_stdx::path::expand_tilde;
|
|
use helix_view::{
|
|
document::Mode,
|
|
editor::{
|
|
Action, BufferLine, ConfigEvent, CursorShapeConfig, FilePickerConfig, GutterConfig,
|
|
IndentGuidesConfig, LineEndingConfig, LineNumber, LspConfig, SearchConfig, SmartTabConfig,
|
|
StatusLineConfig, TerminalConfig, WhitespaceConfig,
|
|
},
|
|
extension::document_id_to_usize,
|
|
input::KeyEvent,
|
|
Document, DocumentId, Editor, ViewId,
|
|
};
|
|
use once_cell::sync::Lazy;
|
|
use steel::{
|
|
gc::unsafe_erased_pointers::CustomReference,
|
|
rerrs::ErrorKind,
|
|
rvals::{as_underlying_type, FromSteelVal, IntoSteelVal, SteelString},
|
|
steel_vm::{engine::Engine, register_fn::RegisterFn},
|
|
steelerr, SteelErr, SteelVal,
|
|
};
|
|
|
|
use std::{
|
|
borrow::Cow,
|
|
collections::HashMap,
|
|
path::PathBuf,
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
time::Duration,
|
|
};
|
|
use std::{
|
|
collections::HashSet,
|
|
sync::{Arc, RwLock},
|
|
};
|
|
|
|
use steel::{rvals::Custom, steel_vm::builtin::BuiltInModule};
|
|
|
|
use crate::{
|
|
commands::insert,
|
|
compositor::{self, Component, Compositor},
|
|
config::Config,
|
|
events::{OnModeSwitch, PostCommand, PostInsertChar},
|
|
job::{self, Callback},
|
|
keymap::{self, merge_keys, KeyTrie, KeymapResult},
|
|
ui::{self, Popup, Prompt, PromptEvent},
|
|
};
|
|
|
|
use components::SteelDynamicComponent;
|
|
|
|
use super::{
|
|
components::{self, helix_component_module},
|
|
Context, MappableCommand, TYPABLE_COMMAND_LIST,
|
|
};
|
|
use insert::{insert_char, insert_string};
|
|
|
|
static ENGINE_COUNT: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
// The Steel scripting engine instance. This is what drives the whole integration.
|
|
// Since the engine is not thread safe, we instead (more or less) pin the engine
|
|
// to a single thread by panicking if it is access on another thread. The only way
|
|
// this can really happen is if a callback is made that touches this variable.
|
|
//
|
|
// As long as the `local_callback` queue is used for any callbacks, all accesses
|
|
// to this variable will stay on the main thread. If you make a mistake, it will be
|
|
// obvious since the editor will immediately crash when running anything that
|
|
// violates this constraint.
|
|
//
|
|
// It isn't explicitly an issue if another thread spins up another instance of
|
|
// the engine, the overhead here is relatively minimal. However the assumption
|
|
// based on the way the integration works is that only one instance of the engine
|
|
// exists at any point in time. Since this is the case, we want to minimize the chance
|
|
// that that isn't true. This assumption could also be revisited at some point in
|
|
// the future.
|
|
thread_local! {
|
|
pub static ENGINE: std::rc::Rc<std::cell::RefCell<steel::steel_vm::engine::Engine>> = {
|
|
if ENGINE_COUNT.fetch_add(1, Ordering::SeqCst) != 0 {
|
|
panic!("More than one instance of the steel runtime is not allowed!");
|
|
}
|
|
configure_engine()
|
|
};
|
|
}
|
|
|
|
pub struct KeyMapApi {
|
|
default_keymap: fn() -> EmbeddedKeyMap,
|
|
empty_keymap: fn() -> EmbeddedKeyMap,
|
|
string_to_embedded_keymap: fn(String) -> EmbeddedKeyMap,
|
|
merge_keybindings: fn(&mut EmbeddedKeyMap, EmbeddedKeyMap),
|
|
is_keymap: fn(SteelVal) -> bool,
|
|
deep_copy_keymap: fn(EmbeddedKeyMap) -> EmbeddedKeyMap,
|
|
}
|
|
|
|
impl KeyMapApi {
|
|
fn new() -> Self {
|
|
KeyMapApi {
|
|
default_keymap,
|
|
empty_keymap,
|
|
string_to_embedded_keymap,
|
|
merge_keybindings,
|
|
is_keymap,
|
|
deep_copy_keymap,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle buffer and extension specific keybindings in userspace.
|
|
thread_local! {
|
|
pub static BUFFER_OR_EXTENSION_KEYBINDING_MAP: SteelVal =
|
|
SteelVal::boxed(SteelVal::empty_hashmap());
|
|
|
|
pub static REVERSE_BUFFER_MAP: SteelVal =
|
|
SteelVal::boxed(SteelVal::empty_hashmap());
|
|
|
|
}
|
|
|
|
fn load_component_api(engine: &mut Engine) {
|
|
let module = helix_component_module();
|
|
engine.register_module(module);
|
|
}
|
|
|
|
fn load_keymap_api(engine: &mut Engine, api: KeyMapApi) {
|
|
let mut module = BuiltInModule::new("helix/core/keymaps");
|
|
|
|
module.register_fn("helix-empty-keymap", api.empty_keymap);
|
|
module.register_fn("helix-default-keymap", api.default_keymap);
|
|
module.register_fn("helix-merge-keybindings", api.merge_keybindings);
|
|
module.register_fn("helix-string->keymap", api.string_to_embedded_keymap);
|
|
module.register_fn("keymap?", api.is_keymap);
|
|
|
|
module.register_fn("helix-deep-copy-keymap", api.deep_copy_keymap);
|
|
|
|
// This should be associated with a corresponding scheme module to wrap this up
|
|
module.register_value(
|
|
"*buffer-or-extension-keybindings*",
|
|
BUFFER_OR_EXTENSION_KEYBINDING_MAP.with(|x| x.clone()),
|
|
);
|
|
module.register_value(
|
|
"*reverse-buffer-map*",
|
|
REVERSE_BUFFER_MAP.with(|x| x.clone()),
|
|
);
|
|
|
|
engine.register_module(module);
|
|
}
|
|
|
|
fn load_static_commands(engine: &mut Engine, generate_sources: bool) {
|
|
let mut module = BuiltInModule::new("helix/core/static");
|
|
|
|
let mut builtin_static_command_module = if generate_sources {
|
|
"(require-builtin helix/core/static as helix.static.)".to_string()
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
for command in TYPABLE_COMMAND_LIST {
|
|
let func = |cx: &mut Context| {
|
|
let mut cx = compositor::Context {
|
|
editor: cx.editor,
|
|
scroll: None,
|
|
jobs: cx.jobs,
|
|
};
|
|
|
|
(command.fun)(&mut cx, &[], PromptEvent::Validate)
|
|
};
|
|
|
|
module.register_fn(command.name, func);
|
|
}
|
|
|
|
// Register everything in the static command list as well
|
|
// These just accept the context, no arguments
|
|
for command in MappableCommand::STATIC_COMMAND_LIST {
|
|
if let MappableCommand::Static { name, fun, .. } = command {
|
|
module.register_fn(name, fun);
|
|
|
|
if generate_sources {
|
|
builtin_static_command_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({})
|
|
(helix.static.{} *helix.cx*))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut template_function_arity_1 = |name: &str| {
|
|
if generate_sources {
|
|
builtin_static_command_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({} arg)
|
|
(helix.static.{} *helix.cx* arg))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
}
|
|
};
|
|
|
|
// Adhoc static commands that probably needs evaluating
|
|
// Arity 1
|
|
module.register_fn("insert_char", insert_char);
|
|
template_function_arity_1("insert_char");
|
|
module.register_fn("insert_string", insert_string);
|
|
template_function_arity_1("insert_string");
|
|
module.register_fn("set-current-selection-object!", set_selection);
|
|
template_function_arity_1("set-current-selection-object!");
|
|
|
|
module.register_fn("search-in-directory", search_in_directory);
|
|
template_function_arity_1("search-in-directory");
|
|
module.register_fn("regex-selection", regex_selection);
|
|
template_function_arity_1("regex-selection");
|
|
module.register_fn("replace-selection-with", replace_selection);
|
|
template_function_arity_1("replace-selection-with");
|
|
module.register_fn("cx->current-file", current_path);
|
|
template_function_arity_1("cx->current-file");
|
|
|
|
let mut template_function_arity_0 = |name: &str| {
|
|
if generate_sources {
|
|
builtin_static_command_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({})
|
|
(helix.static.{} *helix.cx*))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
}
|
|
};
|
|
|
|
// Arity 0
|
|
module.register_fn("current_selection", get_selection);
|
|
template_function_arity_0("current_selection");
|
|
|
|
// Execute a whole buffer to change configurations.
|
|
module.register_fn("load-buffer!", load_buffer);
|
|
template_function_arity_0("load-buffer!");
|
|
|
|
module.register_fn("current-highlighted-text!", get_highlighted_text);
|
|
template_function_arity_0("current-highlighted-text!");
|
|
|
|
module.register_fn("get-current-line-number", current_line_number);
|
|
template_function_arity_0("get-current-line-number");
|
|
|
|
module.register_fn("current-selection-object", current_selection);
|
|
template_function_arity_0("current-selection-object");
|
|
|
|
module.register_fn("get-helix-cwd", get_helix_cwd);
|
|
template_function_arity_0("get-helix-cwd");
|
|
|
|
module.register_fn("move-window-far-left", move_window_to_the_left);
|
|
template_function_arity_0("move-window-far-left");
|
|
|
|
module.register_fn("move-window-far-right", move_window_to_the_right);
|
|
template_function_arity_0("move-window-far-right");
|
|
|
|
let mut template_function_no_context = |name: &str| {
|
|
if generate_sources {
|
|
builtin_static_command_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define {} helix.static.{})
|
|
"#,
|
|
name, name, name
|
|
))
|
|
}
|
|
};
|
|
|
|
module.register_fn("get-helix-scm-path", get_helix_scm_path);
|
|
module.register_fn("get-init-scm-path", get_init_scm_path);
|
|
|
|
template_function_no_context("get-helix-scm-path");
|
|
template_function_no_context("get-init-scm-path");
|
|
|
|
if generate_sources {
|
|
let mut target_directory = helix_runtime_search_path();
|
|
|
|
if !target_directory.exists() {
|
|
std::fs::create_dir(&target_directory).unwrap();
|
|
}
|
|
|
|
target_directory.push("static.scm");
|
|
|
|
std::fs::write(target_directory, builtin_static_command_module).unwrap();
|
|
}
|
|
|
|
engine.register_module(module);
|
|
}
|
|
|
|
fn load_typed_commands(engine: &mut Engine, generate_sources: bool) {
|
|
let mut module = BuiltInModule::new("helix/core/typable".to_string());
|
|
|
|
let mut builtin_typable_command_module = if generate_sources {
|
|
"(require-builtin helix/core/typable as helix.)".to_string()
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
// Register everything in the typable command list. Now these are all available
|
|
for command in TYPABLE_COMMAND_LIST {
|
|
let func = |cx: &mut Context, args: &[Cow<str>]| {
|
|
let mut cx = compositor::Context {
|
|
editor: cx.editor,
|
|
scroll: None,
|
|
jobs: cx.jobs,
|
|
};
|
|
|
|
(command.fun)(&mut cx, args, PromptEvent::Validate)
|
|
};
|
|
|
|
module.register_fn(command.name, func);
|
|
|
|
if generate_sources {
|
|
// Create an ephemeral builtin module to reference until I figure out how
|
|
// to wrap the functions with a reference to the engine context better.
|
|
builtin_typable_command_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
|
|
;;@doc
|
|
{}
|
|
(define ({} . args)
|
|
(helix.{} *helix.cx* args))
|
|
"#,
|
|
command.name,
|
|
{
|
|
// Ugly hack to drop the extra newline from
|
|
// the docstring
|
|
let mut docstring = command
|
|
.doc
|
|
.lines()
|
|
.map(|x| {
|
|
let mut line = ";;".to_string();
|
|
line.push_str(x);
|
|
line.push_str("\n");
|
|
line
|
|
})
|
|
.collect::<String>();
|
|
|
|
docstring.pop();
|
|
|
|
docstring
|
|
},
|
|
command.name,
|
|
command.name
|
|
));
|
|
}
|
|
}
|
|
|
|
if generate_sources {
|
|
let mut target_directory = helix_runtime_search_path();
|
|
if !target_directory.exists() {
|
|
std::fs::create_dir(&target_directory).unwrap();
|
|
}
|
|
|
|
target_directory.push("commands.scm");
|
|
|
|
std::fs::write(target_directory, builtin_typable_command_module).unwrap();
|
|
}
|
|
|
|
engine.register_module(module);
|
|
}
|
|
|
|
// File picker configurations
|
|
fn fp_hidden(config: &mut FilePickerConfig, option: bool) {
|
|
config.hidden = option;
|
|
}
|
|
|
|
fn fp_follow_symlinks(config: &mut FilePickerConfig, option: bool) {
|
|
config.follow_symlinks = option;
|
|
}
|
|
|
|
fn fp_deduplicate_links(config: &mut FilePickerConfig, option: bool) {
|
|
config.deduplicate_links = option;
|
|
}
|
|
|
|
fn fp_parents(config: &mut FilePickerConfig, option: bool) {
|
|
config.parents = option;
|
|
}
|
|
|
|
fn fp_ignore(config: &mut FilePickerConfig, option: bool) {
|
|
config.ignore = option;
|
|
}
|
|
|
|
fn fp_git_ignore(config: &mut FilePickerConfig, option: bool) {
|
|
config.git_ignore = option;
|
|
}
|
|
|
|
fn fp_git_global(config: &mut FilePickerConfig, option: bool) {
|
|
config.git_global = option;
|
|
}
|
|
|
|
fn fp_git_exclude(config: &mut FilePickerConfig, option: bool) {
|
|
config.git_exclude = option;
|
|
}
|
|
|
|
fn fp_max_depth(config: &mut FilePickerConfig, option: Option<usize>) {
|
|
config.max_depth = option;
|
|
}
|
|
|
|
// Soft wrap configurations
|
|
fn sw_enable(config: &mut SoftWrap, option: Option<bool>) {
|
|
config.enable = option;
|
|
}
|
|
|
|
fn sw_max_wrap(config: &mut SoftWrap, option: Option<u16>) {
|
|
config.max_wrap = option;
|
|
}
|
|
|
|
fn sw_max_indent_retain(config: &mut SoftWrap, option: Option<u16>) {
|
|
config.max_indent_retain = option;
|
|
}
|
|
|
|
fn sw_wrap_indicator(config: &mut SoftWrap, option: Option<String>) {
|
|
config.wrap_indicator = option;
|
|
}
|
|
|
|
fn wrap_at_text_width(config: &mut SoftWrap, option: Option<bool>) {
|
|
config.wrap_at_text_width = option;
|
|
}
|
|
|
|
fn load_configuration_api(engine: &mut Engine, generate_sources: bool) {
|
|
let mut module = BuiltInModule::new("helix/core/configuration");
|
|
|
|
module.register_fn("update-configuration!", |ctx: &mut Context| {
|
|
ctx.editor
|
|
.config_events
|
|
.0
|
|
.send(ConfigEvent::Change)
|
|
.unwrap();
|
|
});
|
|
|
|
module
|
|
.register_fn("raw-file-picker", || FilePickerConfig::default())
|
|
.register_fn("register-file-picker", HelixConfiguration::file_picker)
|
|
.register_fn("fp-hidden", fp_hidden)
|
|
.register_fn("fp-follow-symlinks", fp_follow_symlinks)
|
|
.register_fn("fp-deduplicate-links", fp_deduplicate_links)
|
|
.register_fn("fp-parents", fp_parents)
|
|
.register_fn("fp-ignore", fp_ignore)
|
|
.register_fn("fp-git-ignore", fp_git_ignore)
|
|
.register_fn("fp-git-global", fp_git_global)
|
|
.register_fn("fp-git-exclude", fp_git_exclude)
|
|
.register_fn("fp-max-depth", fp_max_depth);
|
|
|
|
module
|
|
.register_fn("raw-soft-wrap", || SoftWrap::default())
|
|
.register_fn("register-soft-wrap", HelixConfiguration::soft_wrap)
|
|
.register_fn("sw-enable", sw_enable)
|
|
.register_fn("sw-max-wrap", sw_max_wrap)
|
|
.register_fn("sw-max-indent-retain", sw_max_indent_retain)
|
|
.register_fn("sw-wrap-indicator", sw_wrap_indicator)
|
|
.register_fn("sw-wrap-at-text-width", wrap_at_text_width);
|
|
|
|
module
|
|
.register_fn("scrolloff", HelixConfiguration::scrolloff)
|
|
.register_fn("scroll_lines", HelixConfiguration::scroll_lines)
|
|
.register_fn("mouse", HelixConfiguration::mouse)
|
|
.register_fn("shell", HelixConfiguration::shell)
|
|
.register_fn("line-number", HelixConfiguration::line_number)
|
|
.register_fn("cursorline", HelixConfiguration::cursorline)
|
|
.register_fn("cursorcolumn", HelixConfiguration::cursorcolumn)
|
|
.register_fn("middle-click-paste", HelixConfiguration::middle_click_paste)
|
|
.register_fn("auto-pairs", HelixConfiguration::auto_pairs)
|
|
// Specific constructors for the auto pairs configuration
|
|
.register_fn("auto-pairs-default", |enabled: bool| {
|
|
AutoPairConfig::Enable(enabled)
|
|
})
|
|
.register_fn("auto-pairs-map", |map: HashMap<char, char>| {
|
|
AutoPairConfig::Pairs(map)
|
|
})
|
|
.register_fn("auto-completion", HelixConfiguration::auto_completion)
|
|
.register_fn("auto-format", HelixConfiguration::auto_format)
|
|
.register_fn("auto-save", HelixConfiguration::auto_save)
|
|
.register_fn("text-width", HelixConfiguration::text_width)
|
|
.register_fn("idle-timeout", HelixConfiguration::idle_timeout)
|
|
.register_fn("completion-timeout", HelixConfiguration::completion_timeout)
|
|
.register_fn(
|
|
"preview-completion-insert",
|
|
HelixConfiguration::preview_completion_insert,
|
|
)
|
|
.register_fn(
|
|
"completion-trigger-len",
|
|
HelixConfiguration::completion_trigger_len,
|
|
)
|
|
.register_fn("completion-replace", HelixConfiguration::completion_replace)
|
|
.register_fn("auto-info", HelixConfiguration::auto_info)
|
|
.register_fn("cursor-shape", HelixConfiguration::cursor_shape)
|
|
.register_fn("true-color", HelixConfiguration::true_color)
|
|
.register_fn(
|
|
"insert-final-newline",
|
|
HelixConfiguration::insert_final_newline,
|
|
)
|
|
.register_fn("color-modes", HelixConfiguration::color_modes)
|
|
.register_fn("gutters", HelixConfiguration::gutters)
|
|
// .register_fn("file-picker", HelixConfiguration::file_picker)
|
|
.register_fn("statusline", HelixConfiguration::statusline)
|
|
.register_fn("undercurl", HelixConfiguration::undercurl)
|
|
.register_fn("search", HelixConfiguration::search)
|
|
.register_fn("lsp", HelixConfiguration::lsp)
|
|
.register_fn("terminal", HelixConfiguration::terminal)
|
|
.register_fn("rulers", HelixConfiguration::rulers)
|
|
.register_fn("whitespace", HelixConfiguration::whitespace)
|
|
.register_fn("bufferline", HelixConfiguration::bufferline)
|
|
.register_fn("indent-guides", HelixConfiguration::indent_guides)
|
|
.register_fn("soft-wrap", HelixConfiguration::soft_wrap)
|
|
.register_fn(
|
|
"workspace-lsp-roots",
|
|
HelixConfiguration::workspace_lsp_roots,
|
|
)
|
|
.register_fn(
|
|
"default-line-ending",
|
|
HelixConfiguration::default_line_ending,
|
|
)
|
|
.register_fn("smart-tab", HelixConfiguration::smart_tab);
|
|
|
|
// Keybinding stuff
|
|
module
|
|
.register_fn("keybindings", HelixConfiguration::keybindings)
|
|
.register_fn("get-keybindings", HelixConfiguration::get_keybindings);
|
|
|
|
if generate_sources {
|
|
let mut builtin_configuration_module =
|
|
"(require-builtin helix/core/configuration as helix.)".to_string();
|
|
|
|
builtin_configuration_module.push_str(&format!(
|
|
r#"
|
|
(provide update-configuration!)
|
|
(define (update-configuration!)
|
|
(helix.update-configuration! *helix.config*))
|
|
"#,
|
|
));
|
|
|
|
// Register the get keybindings function
|
|
builtin_configuration_module.push_str(&format!(
|
|
r#"
|
|
(provide get-keybindings)
|
|
(define (get-keybindings)
|
|
(helix.get-keybindings *helix.config*))
|
|
"#,
|
|
));
|
|
|
|
let mut template_soft_wrap = |name: &str| {
|
|
builtin_configuration_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({} arg)
|
|
(lambda (picker)
|
|
(helix.{} picker arg)
|
|
picker))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
};
|
|
|
|
let soft_wrap_functions = &[
|
|
"sw-enable",
|
|
"sw-max-wrap",
|
|
"sw-max-indent-retain",
|
|
"sw-wrap-indicator",
|
|
"sw-wrap-at-text-width",
|
|
];
|
|
|
|
for name in soft_wrap_functions {
|
|
template_soft_wrap(name);
|
|
}
|
|
|
|
let mut template_file_picker_function = |name: &str| {
|
|
builtin_configuration_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({} arg)
|
|
(lambda (picker)
|
|
(helix.{} picker arg)
|
|
picker))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
};
|
|
|
|
let file_picker_functions = &[
|
|
"fp-hidden",
|
|
"fp-follow-symlinks",
|
|
"fp-deduplicate-links",
|
|
"fp-parents",
|
|
"fp-ignore",
|
|
"fp-git-ignore",
|
|
"fp-git-global",
|
|
"fp-git-exclude",
|
|
"fp-max-depth",
|
|
];
|
|
|
|
for name in file_picker_functions {
|
|
template_file_picker_function(name);
|
|
}
|
|
|
|
builtin_configuration_module.push_str(&format!(
|
|
r#"
|
|
(provide file-picker)
|
|
(define (file-picker . args)
|
|
(helix.register-file-picker
|
|
*helix.config*
|
|
(foldl (lambda (func config) (func config)) (helix.raw-file-picker) args)))
|
|
"#,
|
|
));
|
|
|
|
builtin_configuration_module.push_str(&format!(
|
|
r#"
|
|
(provide soft-wrap)
|
|
(define (soft-wrap . args)
|
|
(helix.register-soft-wrap
|
|
*helix.config*
|
|
(foldl (lambda (func config) (func config)) (helix.raw-soft-wrap) args)))
|
|
"#,
|
|
));
|
|
|
|
let mut template_function_arity_1 = |name: &str| {
|
|
builtin_configuration_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({} arg)
|
|
(helix.{} *helix.config* arg))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
};
|
|
|
|
let functions = &[
|
|
"scrolloff",
|
|
"scroll_lines",
|
|
"mouse",
|
|
"shell",
|
|
"line-number",
|
|
"cursorline",
|
|
"cursorcolumn",
|
|
"middle-click-paste",
|
|
"auto-pairs",
|
|
"auto-completion",
|
|
"auto-format",
|
|
"auto-save",
|
|
"text-width",
|
|
"idle-timeout",
|
|
"completion-timeout",
|
|
"preview-completion-insert",
|
|
"completion-trigger-len",
|
|
"completion-replace",
|
|
"auto-info",
|
|
"cursor-shape",
|
|
"true-color",
|
|
"insert-final-newline",
|
|
"color-modes",
|
|
"gutters",
|
|
"statusline",
|
|
"undercurl",
|
|
"search",
|
|
"lsp",
|
|
"terminal",
|
|
"rulers",
|
|
"whitespace",
|
|
"bufferline",
|
|
"indent-guides",
|
|
"workspace-lsp-roots",
|
|
"default-line-ending",
|
|
"smart-tab",
|
|
"keybindings",
|
|
];
|
|
|
|
for func in functions {
|
|
template_function_arity_1(func);
|
|
}
|
|
|
|
let mut target_directory = helix_runtime_search_path();
|
|
|
|
if !target_directory.exists() {
|
|
std::fs::create_dir(&target_directory).unwrap();
|
|
}
|
|
|
|
target_directory.push("configuration.scm");
|
|
|
|
std::fs::write(target_directory, builtin_configuration_module).unwrap();
|
|
}
|
|
|
|
engine.register_module(module);
|
|
}
|
|
|
|
fn load_editor_api(engine: &mut Engine, generate_sources: bool) {
|
|
let mut module = BuiltInModule::new("helix/core/editor");
|
|
|
|
// Arity 0
|
|
module.register_fn("editor-focus", cx_current_focus);
|
|
module.register_fn("editor-mode", cx_get_mode);
|
|
module.register_fn("cx->themes", get_themes);
|
|
module.register_fn("editor-all-documents", cx_editor_all_documents);
|
|
module.register_fn("cx->cursor", |cx: &mut Context| cx.editor.cursor());
|
|
|
|
// Arity 1
|
|
module.register_fn("editor->doc-id", cx_get_document_id);
|
|
module.register_fn("editor-switch!", cx_switch);
|
|
module.register_fn("editor-set-focus!", |cx: &mut Context, view_id: ViewId| {
|
|
cx.editor.focus(view_id)
|
|
});
|
|
module.register_fn("editor-set-mode!", cx_set_mode);
|
|
module.register_fn("editor-doc-in-view?", cx_is_document_in_view);
|
|
module.register_fn("set-scratch-buffer-name!", set_scratch_buffer_name);
|
|
module.register_fn("editor-doc-exists?", cx_document_exists);
|
|
|
|
// Arity 1
|
|
RegisterFn::<
|
|
_,
|
|
steel::steel_vm::register_fn::MarkerWrapper8<(
|
|
Context,
|
|
DocumentId,
|
|
Document,
|
|
Document,
|
|
Context,
|
|
)>,
|
|
Document,
|
|
>::register_fn(&mut module, "editor->get-document", cx_get_document); // I do not like this
|
|
|
|
if generate_sources {
|
|
let mut builtin_editor_command_module =
|
|
"(require-builtin helix/core/editor as helix.)".to_string();
|
|
|
|
let mut template_function_arity_0 = |name: &str| {
|
|
builtin_editor_command_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({})
|
|
(helix.{} *helix.cx*))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
};
|
|
|
|
template_function_arity_0("editor-focus");
|
|
template_function_arity_0("editor-mode");
|
|
template_function_arity_0("cx->themes");
|
|
template_function_arity_0("editor-all-documents");
|
|
template_function_arity_0("cx->cursor");
|
|
|
|
let mut template_function_arity_1 = |name: &str| {
|
|
builtin_editor_command_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({} arg)
|
|
(helix.{} *helix.cx* arg))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
};
|
|
|
|
template_function_arity_1("editor->doc-id");
|
|
template_function_arity_1("editor-switch!");
|
|
template_function_arity_1("editor-set-focus!");
|
|
template_function_arity_1("editor-set-mode!");
|
|
template_function_arity_1("editor-doc-in-view?");
|
|
template_function_arity_1("set-scratch-buffer-name!");
|
|
template_function_arity_1("editor-doc-exists?");
|
|
template_function_arity_1("editor->get-document");
|
|
|
|
let mut target_directory = helix_runtime_search_path();
|
|
|
|
if !target_directory.exists() {
|
|
std::fs::create_dir(&target_directory).unwrap();
|
|
}
|
|
|
|
target_directory.push("editor.scm");
|
|
|
|
std::fs::write(target_directory, builtin_editor_command_module).unwrap();
|
|
}
|
|
|
|
engine.register_module(module);
|
|
}
|
|
|
|
pub struct SteelScriptingEngine;
|
|
|
|
impl super::PluginSystem for SteelScriptingEngine {
|
|
fn initialize(&self) {
|
|
initialize_engine();
|
|
}
|
|
|
|
fn engine_name(&self) -> super::PluginSystemKind {
|
|
super::PluginSystemKind::Steel
|
|
}
|
|
|
|
fn run_initialization_script(
|
|
&self,
|
|
cx: &mut Context,
|
|
configuration: Arc<ArcSwapAny<Arc<Config>>>,
|
|
) {
|
|
run_initialization_script(cx, configuration);
|
|
}
|
|
|
|
fn handle_keymap_event(
|
|
&self,
|
|
editor: &mut ui::EditorView,
|
|
mode: Mode,
|
|
cxt: &mut Context,
|
|
event: KeyEvent,
|
|
) -> Option<KeymapResult> {
|
|
SteelScriptingEngine::get_keymap_for_extension(cxt).and_then(|map| {
|
|
if let steel::SteelVal::Custom(inner) = map {
|
|
if let Some(underlying) =
|
|
steel::rvals::as_underlying_type::<EmbeddedKeyMap>(inner.borrow().as_ref())
|
|
{
|
|
return Some(editor.keymaps.get_with_map(&underlying.0, mode, event));
|
|
}
|
|
}
|
|
|
|
None
|
|
})
|
|
}
|
|
|
|
fn call_function_by_name(&self, cx: &mut Context, name: &str, args: &[Cow<str>]) -> bool {
|
|
if ENGINE.with(|x| x.borrow().global_exists(name)) {
|
|
let args = 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.with_mut_reference::<Context, Context>(cx).consume(
|
|
move |engine, arguments| {
|
|
let context = arguments[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
// TODO: Get rid of this clone
|
|
engine.call_function_by_name_with_args(name, args.clone())
|
|
},
|
|
)
|
|
}
|
|
}) {
|
|
cx.editor.set_error(format!("{}", e));
|
|
}
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
fn call_typed_command<'a>(
|
|
&self,
|
|
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 {
|
|
if let Err(e) = ENGINE.with(|x| {
|
|
let args = args[1..]
|
|
.iter()
|
|
.map(|x| x.clone().into_steelval().unwrap())
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut guard = x.borrow_mut();
|
|
|
|
let res = {
|
|
let mut cx = Context {
|
|
register: None,
|
|
count: std::num::NonZeroUsize::new(1),
|
|
editor: cx.editor,
|
|
callback: Vec::new(),
|
|
on_next_key_callback: None,
|
|
jobs: cx.jobs,
|
|
};
|
|
|
|
guard
|
|
.with_mut_reference(&mut cx)
|
|
.consume(move |engine, arguments| {
|
|
let context = arguments[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
// TODO: Fix this clone
|
|
engine.call_function_by_name_with_args(&parts[0], args.clone())
|
|
})
|
|
};
|
|
|
|
res
|
|
}) {
|
|
let mut ctx = Context {
|
|
register: None,
|
|
count: None,
|
|
editor: &mut cx.editor,
|
|
callback: Vec::new(),
|
|
on_next_key_callback: None,
|
|
jobs: &mut cx.jobs,
|
|
};
|
|
|
|
ENGINE.with(|x| {
|
|
present_error_inside_engine_context(&mut ctx, &mut x.borrow_mut(), e)
|
|
});
|
|
};
|
|
}
|
|
|
|
// Global exists
|
|
true
|
|
} else {
|
|
// Global does not exist
|
|
false
|
|
}
|
|
}
|
|
|
|
fn get_doc_for_identifier(&self, ident: &str) -> Option<String> {
|
|
ExportedIdentifiers::engine_get_doc(ident)
|
|
.map(|v| v.into())
|
|
.or_else(|| {
|
|
if Self::is_exported(self, ident) {
|
|
Some("Run this plugin command!".into())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn available_commands<'a>(&self) -> Vec<Cow<'a, str>> {
|
|
EXPORTED_IDENTIFIERS
|
|
.identifiers
|
|
.read()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|x| x.clone().into())
|
|
.collect::<Vec<_>>()
|
|
}
|
|
|
|
fn generate_sources(&self) {
|
|
// Generate sources directly with a fresh engine
|
|
let mut engine = Engine::new();
|
|
configure_builtin_sources(&mut engine, true);
|
|
}
|
|
}
|
|
|
|
impl SteelScriptingEngine {
|
|
fn is_exported(&self, ident: &str) -> bool {
|
|
EXPORTED_IDENTIFIERS
|
|
.identifiers
|
|
.read()
|
|
.unwrap()
|
|
.contains(ident)
|
|
}
|
|
|
|
// Attempt to fetch the keymap for the extension
|
|
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())
|
|
};
|
|
|
|
let doc_id = {
|
|
let current_focus = cx.editor.tree.focus;
|
|
let view = cx.editor.tree.get(current_focus);
|
|
let doc = &view.doc;
|
|
|
|
doc
|
|
};
|
|
|
|
if let Some(extension) = extension {
|
|
if let SteelVal::Boxed(boxed_map) =
|
|
BUFFER_OR_EXTENSION_KEYBINDING_MAP.with(|x| x.clone())
|
|
{
|
|
if let SteelVal::HashMapV(map) = boxed_map.borrow().clone() {
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let SteelVal::Boxed(boxed_map) = REVERSE_BUFFER_MAP.with(|x| x.clone()) {
|
|
if let SteelVal::HashMapV(map) = boxed_map.borrow().clone() {
|
|
if let Some(label) = map.get(&SteelVal::IntV(document_id_to_usize(doc_id) as isize))
|
|
{
|
|
if let SteelVal::Boxed(boxed_map) =
|
|
BUFFER_OR_EXTENSION_KEYBINDING_MAP.with(|x| x.clone())
|
|
{
|
|
if let SteelVal::HashMapV(map) = boxed_map.borrow().clone() {
|
|
if let Some(value) = map.get(label) {
|
|
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 initialize_engine() {
|
|
ENGINE.with(|x| x.borrow().globals().first().copied());
|
|
}
|
|
|
|
pub fn present_error_inside_engine_context(cx: &mut Context, engine: &mut Engine, e: SteelErr) {
|
|
cx.editor.set_error(e.to_string());
|
|
|
|
let backtrace = engine.raise_error_to_string(e);
|
|
|
|
let callback = async move {
|
|
let call: job::Callback = Callback::EditorCompositor(Box::new(
|
|
move |editor: &mut Editor, compositor: &mut Compositor| {
|
|
if let Some(backtrace) = backtrace {
|
|
let contents = ui::Markdown::new(
|
|
format!("```\n{}\n```", backtrace),
|
|
editor.syn_loader.clone(),
|
|
);
|
|
let popup = Popup::new("engine", contents).position(Some(
|
|
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
|
|
));
|
|
compositor.replace_or_push("engine", popup);
|
|
}
|
|
},
|
|
));
|
|
Ok(call)
|
|
};
|
|
cx.jobs.callback(callback);
|
|
}
|
|
|
|
// Key maps
|
|
#[derive(Clone, Debug)]
|
|
pub struct EmbeddedKeyMap(pub HashMap<Mode, KeyTrie>);
|
|
impl Custom for EmbeddedKeyMap {}
|
|
|
|
// Will deep copy a value by default when using a value type
|
|
pub fn deep_copy_keymap(copied: EmbeddedKeyMap) -> EmbeddedKeyMap {
|
|
copied
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
pub fn is_keymap(keymap: SteelVal) -> bool {
|
|
if let SteelVal::Custom(underlying) = keymap {
|
|
as_underlying_type::<EmbeddedKeyMap>(underlying.borrow().as_ref()).is_some()
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn helix_module_file() -> PathBuf {
|
|
helix_loader::config_dir().join("helix.scm")
|
|
}
|
|
|
|
pub fn steel_init_file() -> PathBuf {
|
|
helix_loader::config_dir().join("init.scm")
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct HelixConfiguration {
|
|
configuration: Arc<ArcSwapAny<Arc<Config>>>,
|
|
}
|
|
|
|
impl Custom for HelixConfiguration {}
|
|
// impl Custom for LineNumber {}
|
|
|
|
impl HelixConfiguration {
|
|
fn load_config(&self) -> Config {
|
|
(*self.configuration.load().clone()).clone()
|
|
}
|
|
|
|
fn store_config(&self, config: Config) {
|
|
self.configuration.store(Arc::new(config));
|
|
}
|
|
|
|
// Overlay new keybindings
|
|
fn keybindings(&self, keybindings: EmbeddedKeyMap) {
|
|
let mut app_config = self.load_config();
|
|
merge_keys(&mut app_config.keys, keybindings.0);
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn get_keybindings(&self) -> EmbeddedKeyMap {
|
|
EmbeddedKeyMap(self.load_config().keys.clone())
|
|
}
|
|
|
|
fn scrolloff(&self, lines: usize) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.scrolloff = lines;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn scroll_lines(&self, lines: isize) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.scroll_lines = lines;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn mouse(&self, m: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.mouse = m;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn shell(&self, shell: Vec<String>) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.shell = shell;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
// TODO: Make this a symbol, probably!
|
|
fn line_number(&self, mode: LineNumber) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.line_number = mode;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn cursorline(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.cursorline = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn cursorcolumn(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.cursorcolumn = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn middle_click_paste(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.middle_click_paste = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn auto_pairs(&self, config: AutoPairConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.auto_pairs = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn auto_completion(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.auto_completion = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn auto_format(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.auto_format = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn auto_save(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.auto_save = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn text_width(&self, width: usize) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.text_width = width;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn idle_timeout(&self, ms: usize) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.idle_timeout = Duration::from_millis(ms as u64);
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn completion_timeout(&self, ms: usize) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.completion_timeout = Duration::from_millis(ms as u64);
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn preview_completion_insert(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.preview_completion_insert = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
// TODO: Make sure this conversion works automatically
|
|
fn completion_trigger_len(&self, length: u8) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.completion_trigger_len = length;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn completion_replace(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.completion_replace = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn auto_info(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.auto_info = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn cursor_shape(&self, config: CursorShapeConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.cursor_shape = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn true_color(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.true_color = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn insert_final_newline(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.insert_final_newline = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn color_modes(&self, option: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.color_modes = option;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn gutters(&self, config: GutterConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.gutters = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn file_picker(&self, picker: FilePickerConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.file_picker = picker;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn statusline(&self, config: StatusLineConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.statusline = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn undercurl(&self, undercurl: bool) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.undercurl = undercurl;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn search(&self, config: SearchConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.search = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn lsp(&self, config: LspConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.lsp = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn terminal(&self, config: Option<TerminalConfig>) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.terminal = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn rulers(&self, cols: Vec<u16>) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.rulers = cols;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn whitespace(&self, config: WhitespaceConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.whitespace = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn bufferline(&self, config: BufferLine) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.bufferline = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn indent_guides(&self, config: IndentGuidesConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.indent_guides = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn soft_wrap(&self, config: SoftWrap) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.soft_wrap = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn workspace_lsp_roots(&self, roots: Vec<PathBuf>) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.workspace_lsp_roots = roots;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn default_line_ending(&self, config: LineEndingConfig) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.default_line_ending = config;
|
|
self.store_config(app_config);
|
|
}
|
|
|
|
fn smart_tab(&self, config: Option<SmartTabConfig>) {
|
|
let mut app_config = self.load_config();
|
|
app_config.editor.smart_tab = config;
|
|
self.store_config(app_config);
|
|
}
|
|
}
|
|
|
|
/// 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
|
|
fn run_initialization_script(cx: &mut Context, configuration: Arc<ArcSwapAny<Arc<Config>>>) {
|
|
log::info!("Loading init.scm...");
|
|
|
|
let helix_module_path = helix_module_file();
|
|
|
|
// TODO: Report the error from requiring the file!
|
|
ENGINE.with(|engine| {
|
|
let mut guard = engine.borrow_mut();
|
|
|
|
// Embed the configuration so we don't have to communicate over the refresh
|
|
// channel. The state is still stored within the `Application` struct, but
|
|
// now we can just access it and signal a refresh of the config when we need to.
|
|
guard.update_value(
|
|
"*helix.config*",
|
|
HelixConfiguration { configuration }
|
|
.into_steelval()
|
|
.unwrap(),
|
|
);
|
|
|
|
let res = guard.run_with_reference(
|
|
cx,
|
|
"*helix.cx*",
|
|
&format!(r#"(require "{}")"#, helix_module_path.to_str().unwrap()),
|
|
);
|
|
|
|
// Present the error in the helix.scm loading
|
|
if let Err(e) = res {
|
|
present_error_inside_engine_context(cx, &mut guard, e);
|
|
return;
|
|
}
|
|
|
|
if let Ok(module) = guard.get_module(helix_module_path) {
|
|
if let steel::rvals::SteelVal::HashMapV(m) = module {
|
|
let exported = m
|
|
.iter()
|
|
.filter(|(_, v)| v.is_function())
|
|
.map(|(k, _)| {
|
|
if let steel::rvals::SteelVal::SymbolV(s) = k {
|
|
s.to_string()
|
|
} else {
|
|
panic!("Found a non symbol!")
|
|
}
|
|
})
|
|
.collect::<HashSet<_>>();
|
|
|
|
let docs = exported
|
|
.iter()
|
|
.filter_map(|x| {
|
|
if let Ok(value) = guard.compile_and_run_raw_program(format!(
|
|
"(#%function-ptr-table-get #%function-ptr-table {})",
|
|
x
|
|
)) {
|
|
if let Some(SteelVal::StringV(doc)) = value.first() {
|
|
Some((x.to_string(), doc.to_string()))
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
*EXPORTED_IDENTIFIERS.identifiers.write().unwrap() = exported;
|
|
*EXPORTED_IDENTIFIERS.docs.write().unwrap() = docs;
|
|
} else {
|
|
present_error_inside_engine_context(
|
|
cx,
|
|
&mut guard,
|
|
SteelErr::new(
|
|
ErrorKind::Generic,
|
|
"Unable to parse exported identifiers from helix module!".to_string(),
|
|
),
|
|
);
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
let helix_module_path = steel_init_file();
|
|
|
|
// These contents need to be registered with the path?
|
|
if let Ok(contents) = std::fs::read_to_string(&helix_module_path) {
|
|
let res = guard.run_with_reference_from_path::<Context, Context>(
|
|
cx,
|
|
"*helix.cx*",
|
|
&contents,
|
|
helix_module_path,
|
|
);
|
|
|
|
match res {
|
|
Ok(_) => {}
|
|
Err(e) => present_error_inside_engine_context(cx, &mut guard, e),
|
|
}
|
|
|
|
log::info!("Finished loading init.scm!")
|
|
} else {
|
|
log::info!("No init.scm found, skipping loading.")
|
|
}
|
|
});
|
|
}
|
|
|
|
pub static EXPORTED_IDENTIFIERS: Lazy<ExportedIdentifiers> =
|
|
Lazy::new(|| ExportedIdentifiers::default());
|
|
|
|
impl Custom for PromptEvent {}
|
|
|
|
impl<'a> CustomReference for Context<'a> {}
|
|
|
|
steel::custom_reference!(Context<'a>);
|
|
|
|
fn get_themes(cx: &mut Context) -> Vec<String> {
|
|
ui::completers::theme(cx.editor, "")
|
|
.into_iter()
|
|
.map(|x| x.1.to_string())
|
|
.collect()
|
|
}
|
|
|
|
/// A dynamic component, used for rendering thing
|
|
impl Custom for compositor::EventResult {}
|
|
|
|
pub struct WrappedDynComponent {
|
|
pub(crate) inner: Option<Box<dyn Component>>,
|
|
}
|
|
|
|
impl Custom for WrappedDynComponent {}
|
|
|
|
pub struct BoxDynComponent {
|
|
inner: Box<dyn Component>,
|
|
}
|
|
|
|
impl BoxDynComponent {
|
|
pub fn new(inner: Box<dyn Component>) -> Self {
|
|
Self { inner }
|
|
}
|
|
}
|
|
|
|
impl Component for BoxDynComponent {
|
|
fn handle_event(
|
|
&mut self,
|
|
_event: &helix_view::input::Event,
|
|
_ctx: &mut compositor::Context,
|
|
) -> compositor::EventResult {
|
|
self.inner.handle_event(_event, _ctx)
|
|
}
|
|
|
|
fn should_update(&self) -> bool {
|
|
self.inner.should_update()
|
|
}
|
|
|
|
fn cursor(
|
|
&self,
|
|
_area: helix_view::graphics::Rect,
|
|
_ctx: &Editor,
|
|
) -> (
|
|
Option<helix_core::Position>,
|
|
helix_view::graphics::CursorKind,
|
|
) {
|
|
self.inner.cursor(_area, _ctx)
|
|
}
|
|
|
|
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
|
|
self.inner.required_size(_viewport)
|
|
}
|
|
|
|
fn type_name(&self) -> &'static str {
|
|
std::any::type_name::<Self>()
|
|
}
|
|
|
|
fn id(&self) -> Option<&'static str> {
|
|
None
|
|
}
|
|
|
|
fn render(
|
|
&mut self,
|
|
area: helix_view::graphics::Rect,
|
|
frame: &mut tui::buffer::Buffer,
|
|
ctx: &mut compositor::Context,
|
|
) {
|
|
self.inner.render(area, frame, ctx)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct OnModeSwitchEvent {
|
|
old_mode: Mode,
|
|
new_mode: Mode,
|
|
}
|
|
|
|
impl Custom for OnModeSwitchEvent {}
|
|
impl Custom for MappableCommand {}
|
|
|
|
fn register_hook(event_kind: String, function_name: String) -> steel::UnRecoverableResult {
|
|
match event_kind.as_str() {
|
|
"on-mode-switch" => {
|
|
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
|
|
if ENGINE.with(|x| x.borrow().global_exists(&function_name)) {
|
|
if let Err(e) = ENGINE.with(|x| {
|
|
let mut guard = x.borrow_mut();
|
|
|
|
let minimized_event = OnModeSwitchEvent {
|
|
old_mode: event.old_mode,
|
|
new_mode: event.new_mode,
|
|
};
|
|
|
|
guard.with_mut_reference(event.cx).consume(|engine, args| {
|
|
let context = args[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
let args = vec![minimized_event.into_steelval().unwrap()];
|
|
engine.call_function_by_name_with_args(&function_name, args)
|
|
})
|
|
}) {
|
|
event.cx.editor.set_error(e.to_string());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
});
|
|
|
|
Ok(SteelVal::Void).into()
|
|
}
|
|
"post-insert-char" => {
|
|
register_hook!(move |event: &mut PostInsertChar<'_, '_>| {
|
|
if ENGINE.with(|x| x.borrow().global_exists(&function_name)) {
|
|
if let Err(e) = ENGINE.with(|x| {
|
|
let mut guard = x.borrow_mut();
|
|
|
|
guard.with_mut_reference(event.cx).consume(|engine, args| {
|
|
let context = args[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
// args.push(event.c.into());
|
|
engine.call_function_by_name_with_args(
|
|
&function_name,
|
|
vec![event.c.into()],
|
|
)
|
|
})
|
|
}) {
|
|
event.cx.editor.set_error(e.to_string());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
});
|
|
|
|
Ok(SteelVal::Void).into()
|
|
}
|
|
"post-command" => {
|
|
register_hook!(move |event: &mut PostCommand<'_, '_>| {
|
|
if ENGINE.with(|x| x.borrow().global_exists(&function_name)) {
|
|
if let Err(e) = ENGINE.with(|x| {
|
|
let mut guard = x.borrow_mut();
|
|
|
|
guard.with_mut_reference(event.cx).consume(|engine, args| {
|
|
let context = args[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
// args.push(event.command.clone().into_steelval().unwrap());
|
|
engine.call_function_by_name_with_args(
|
|
&function_name,
|
|
vec![event.command.clone().into_steelval().unwrap()],
|
|
)
|
|
})
|
|
}) {
|
|
event.cx.editor.set_error(e.to_string());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
});
|
|
|
|
Ok(SteelVal::Void).into()
|
|
}
|
|
// Unimplemented!
|
|
// "document-did-change" => {
|
|
// todo!()
|
|
// }
|
|
// "selection-did-change" => {
|
|
// todo!()
|
|
// }
|
|
_ => steelerr!(Generic => "Unable to register hook: Unknown event type: {}", event_kind)
|
|
.into(),
|
|
}
|
|
}
|
|
|
|
fn load_rope_api(engine: &mut Engine) {
|
|
let mut rope_slice_module = rope_module();
|
|
|
|
rope_slice_module.register_fn("document->slice", document_to_text);
|
|
|
|
engine.register_module(rope_slice_module);
|
|
}
|
|
|
|
struct SteelEngine(Engine);
|
|
|
|
impl SteelEngine {
|
|
pub fn call_function_by_name(
|
|
&mut self,
|
|
function_name: SteelString,
|
|
args: Vec<SteelVal>,
|
|
) -> steel::rvals::Result<SteelVal> {
|
|
self.0
|
|
.call_function_by_name_with_args(function_name.as_str(), args.into_iter().collect())
|
|
}
|
|
|
|
/// Calling a function that was not defined in the runtime it was created in could
|
|
/// result in panics. You have been warned.
|
|
pub fn call_function(
|
|
&mut self,
|
|
function: SteelVal,
|
|
args: Vec<SteelVal>,
|
|
) -> steel::rvals::Result<SteelVal> {
|
|
self.0
|
|
.call_function_with_args(function, args.into_iter().collect())
|
|
}
|
|
|
|
pub fn require_module(&mut self, module: SteelString) -> steel::rvals::Result<()> {
|
|
self.0.run(format!("(require \"{}\")", module)).map(|_| ())
|
|
}
|
|
}
|
|
|
|
impl Custom for SteelEngine {}
|
|
|
|
static ENGINE_ID: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
thread_local! {
|
|
pub static ENGINE_MAP: SteelVal =
|
|
SteelVal::boxed(SteelVal::empty_hashmap());
|
|
}
|
|
|
|
// Low level API work, these need to be loaded into the global environment in a predictable
|
|
// location, otherwise callbacks from plugin engines will not be handled properly!
|
|
fn load_engine_api(engine: &mut Engine) {
|
|
fn id_to_engine(value: SteelVal) -> Option<SteelVal> {
|
|
if let SteelVal::Boxed(b) = ENGINE_MAP.with(|x| x.clone()) {
|
|
if let SteelVal::HashMapV(h) = b.borrow().clone() {
|
|
return h.get(&value).cloned();
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
// module
|
|
engine
|
|
.register_fn("helix.controller.create-engine", || {
|
|
SteelEngine(configure_engine_impl(Engine::new()))
|
|
})
|
|
.register_fn("helix.controller.fresh-engine-id", || {
|
|
ENGINE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
|
|
})
|
|
.register_fn(
|
|
"helix.controller.call-function-by-name",
|
|
SteelEngine::call_function_by_name,
|
|
)
|
|
.register_fn("helix.controller.call-function", SteelEngine::call_function)
|
|
.register_fn(
|
|
"helix.controller.require-module",
|
|
SteelEngine::require_module,
|
|
)
|
|
.register_value(
|
|
"helix.controller.engine-map",
|
|
ENGINE_MAP.with(|x| x.clone()),
|
|
)
|
|
.register_fn("helix.controller.id->engine", id_to_engine);
|
|
}
|
|
|
|
fn load_misc_api(engine: &mut Engine, generate_sources: bool) {
|
|
let mut module = BuiltInModule::new("helix/core/misc");
|
|
|
|
let mut builtin_misc_module = if generate_sources {
|
|
"(require-builtin helix/core/misc as helix.)".to_string()
|
|
} else {
|
|
"".to_string()
|
|
};
|
|
|
|
let mut template_function_arity_0 = |name: &str| {
|
|
if generate_sources {
|
|
builtin_misc_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({})
|
|
(helix.{} *helix.cx*))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
}
|
|
};
|
|
|
|
// Arity 0
|
|
module.register_fn("hx.cx->pos", cx_pos_within_text);
|
|
|
|
template_function_arity_0("hx.cx->pos");
|
|
|
|
let mut template_function_arity_1 = |name: &str| {
|
|
if generate_sources {
|
|
builtin_misc_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({} arg)
|
|
(helix.{} *helix.cx* arg))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
}
|
|
};
|
|
|
|
// Arity 1
|
|
module.register_fn("hx.custom-insert-newline", custom_insert_newline);
|
|
module.register_fn("push-component!", push_component);
|
|
module.register_fn("enqueue-thread-local-callback", enqueue_command);
|
|
|
|
template_function_arity_1("hx.custom-insert-newline");
|
|
template_function_arity_1("push-component!");
|
|
template_function_arity_1("enqueue-thread-local-callback");
|
|
|
|
let mut template_function_arity_2 = |name: &str| {
|
|
if generate_sources {
|
|
builtin_misc_module.push_str(&format!(
|
|
r#"
|
|
(provide {})
|
|
(define ({} arg1 arg2)
|
|
(helix.{} *helix.cx* arg1 arg2))
|
|
"#,
|
|
name, name, name
|
|
));
|
|
}
|
|
};
|
|
|
|
// Arity 2
|
|
module.register_fn(
|
|
"enqueue-thread-local-callback-with-delay",
|
|
enqueue_command_with_delay,
|
|
);
|
|
|
|
// Arity 2
|
|
module.register_fn("helix-await-callback", await_value);
|
|
|
|
template_function_arity_2("enqueue-thread-local-callback-with-delay");
|
|
template_function_arity_2("helix-await-callback");
|
|
|
|
if generate_sources {
|
|
let mut target_directory = helix_runtime_search_path();
|
|
|
|
if !target_directory.exists() {
|
|
std::fs::create_dir(&target_directory).unwrap();
|
|
}
|
|
|
|
target_directory.push("misc.scm");
|
|
|
|
std::fs::write(target_directory, builtin_misc_module).unwrap();
|
|
}
|
|
|
|
engine.register_module(module);
|
|
}
|
|
|
|
fn helix_runtime_search_path() -> PathBuf {
|
|
helix_loader::config_dir().join("helix")
|
|
}
|
|
|
|
pub fn configure_builtin_sources(engine: &mut Engine, generate_sources: bool) {
|
|
load_editor_api(engine, generate_sources);
|
|
load_configuration_api(engine, generate_sources);
|
|
load_typed_commands(engine, generate_sources);
|
|
load_static_commands(engine, generate_sources);
|
|
if !generate_sources {
|
|
// Note: This is going to be completely revamped soon.
|
|
load_keymap_api(engine, KeyMapApi::new());
|
|
}
|
|
load_rope_api(engine);
|
|
load_misc_api(engine, generate_sources);
|
|
if !generate_sources {
|
|
load_component_api(engine);
|
|
}
|
|
}
|
|
|
|
fn configure_engine_impl(mut engine: Engine) -> Engine {
|
|
log::info!("Loading engine!");
|
|
|
|
engine.add_search_directory(helix_loader::config_dir());
|
|
|
|
engine.register_value("*helix.cx*", SteelVal::Void);
|
|
engine.register_value("*helix.config*", SteelVal::Void);
|
|
|
|
// Don't generate source directories here
|
|
configure_builtin_sources(&mut engine, false);
|
|
|
|
// Hooks
|
|
engine.register_fn("register-hook!", register_hook);
|
|
engine.register_fn("log::info!", |message: String| log::info!("{}", message));
|
|
|
|
// Find the workspace
|
|
engine.register_fn("helix-find-workspace", || {
|
|
helix_core::find_workspace().0.to_str().unwrap().to_string()
|
|
});
|
|
|
|
engine.register_fn("Document-path", document_path);
|
|
engine.register_fn("doc-id->usize", document_id_to_usize);
|
|
|
|
// Get the current OS
|
|
engine.register_fn("new-component!", SteelDynamicComponent::new_dyn);
|
|
|
|
engine.register_fn("SteelDynamicComponent?", |object: SteelVal| {
|
|
if let SteelVal::Custom(v) = object {
|
|
if let Some(wrapped) = v.borrow().as_any_ref().downcast_ref::<BoxDynComponent>() {
|
|
return wrapped.inner.as_any().is::<SteelDynamicComponent>();
|
|
} else {
|
|
false
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
|
|
engine.register_fn(
|
|
"SteelDynamicComponent-state",
|
|
SteelDynamicComponent::get_state,
|
|
);
|
|
engine.register_fn(
|
|
"SteelDynamicComponent-render",
|
|
SteelDynamicComponent::get_render,
|
|
);
|
|
engine.register_fn(
|
|
"SteelDynamicComponent-handle-event",
|
|
SteelDynamicComponent::get_handle_event,
|
|
);
|
|
engine.register_fn(
|
|
"SteelDynamicComponent-should-update",
|
|
SteelDynamicComponent::should_update,
|
|
);
|
|
engine.register_fn(
|
|
"SteelDynamicComponent-cursor",
|
|
SteelDynamicComponent::cursor,
|
|
);
|
|
engine.register_fn(
|
|
"SteelDynamicComponent-required-size",
|
|
SteelDynamicComponent::get_required_size,
|
|
);
|
|
|
|
engine.register_fn(
|
|
"prompt",
|
|
|prompt: String, callback_fn: SteelVal| -> WrappedDynComponent {
|
|
let callback_fn_guard = callback_fn.as_rooted();
|
|
|
|
let prompt = Prompt::new(
|
|
prompt.into(),
|
|
None,
|
|
|_, _| Vec::new(),
|
|
move |cx, input, prompt_event| {
|
|
log::info!("Calling dynamic prompt callback");
|
|
|
|
if prompt_event != PromptEvent::Validate {
|
|
return;
|
|
}
|
|
|
|
let mut ctx = Context {
|
|
register: None,
|
|
count: None,
|
|
editor: cx.editor,
|
|
callback: Vec::new(),
|
|
on_next_key_callback: None,
|
|
jobs: cx.jobs,
|
|
};
|
|
|
|
let cloned_func = callback_fn_guard.value();
|
|
|
|
ENGINE.with(|x| {
|
|
let mut guard = x.borrow_mut();
|
|
|
|
if let Err(e) = guard
|
|
.with_mut_reference::<Context, Context>(&mut ctx)
|
|
.consume(move |engine, args| {
|
|
let context = args[0].clone();
|
|
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
engine.call_function_with_args(
|
|
cloned_func.clone(),
|
|
vec![input.into_steelval().unwrap()],
|
|
)
|
|
})
|
|
{
|
|
present_error_inside_engine_context(&mut ctx, &mut guard, e);
|
|
}
|
|
})
|
|
},
|
|
);
|
|
|
|
WrappedDynComponent {
|
|
inner: Some(Box::new(prompt)),
|
|
}
|
|
},
|
|
);
|
|
|
|
engine.register_fn("picker", |values: Vec<String>| -> WrappedDynComponent {
|
|
let picker = ui::Picker::new(
|
|
Vec::new(),
|
|
PathBuf::from(""),
|
|
move |cx, path: &PathBuf, action| {
|
|
if let Err(e) = cx.editor.open(path, action) {
|
|
let err = if let Some(err) = e.source() {
|
|
format!("{}", err)
|
|
} else {
|
|
format!("unable to open \"{}\"", path.display())
|
|
};
|
|
cx.editor.set_error(err);
|
|
}
|
|
},
|
|
)
|
|
.with_preview(|_editor, path| Some((path.clone().into(), None)));
|
|
|
|
let injector = picker.injector();
|
|
|
|
for file in values {
|
|
if injector.push(PathBuf::from(file)).is_err() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
WrappedDynComponent {
|
|
inner: Some(Box::new(ui::overlay::overlaid(picker))),
|
|
}
|
|
});
|
|
|
|
engine.register_fn("Component::Text", |contents: String| WrappedDynComponent {
|
|
inner: Some(Box::new(crate::ui::Text::new(contents))),
|
|
});
|
|
|
|
// Separate this out into its own component module - This just lets us call the underlying
|
|
// component, not sure if we can go from trait object -> trait object easily but we'll see!
|
|
engine.register_fn(
|
|
"Component::render",
|
|
|t: &mut WrappedDynComponent,
|
|
area: helix_view::graphics::Rect,
|
|
frame: &mut tui::buffer::Buffer,
|
|
ctx: &mut Context| {
|
|
t.inner.as_mut().unwrap().render(
|
|
area,
|
|
frame,
|
|
&mut compositor::Context {
|
|
jobs: ctx.jobs,
|
|
editor: ctx.editor,
|
|
scroll: None,
|
|
},
|
|
)
|
|
},
|
|
);
|
|
|
|
engine.register_fn(
|
|
"Component::handle-event",
|
|
|s: &mut WrappedDynComponent, event: &helix_view::input::Event, ctx: &mut Context| {
|
|
s.inner.as_mut().unwrap().handle_event(
|
|
event,
|
|
&mut compositor::Context {
|
|
jobs: ctx.jobs,
|
|
editor: ctx.editor,
|
|
scroll: None,
|
|
},
|
|
)
|
|
},
|
|
);
|
|
|
|
engine.register_fn("Component::should-update", |s: &mut WrappedDynComponent| {
|
|
s.inner.as_mut().unwrap().should_update()
|
|
});
|
|
|
|
engine.register_fn(
|
|
"Component::cursor",
|
|
|s: &WrappedDynComponent, area: helix_view::graphics::Rect, ctx: &Editor| {
|
|
s.inner.as_ref().unwrap().cursor(area, ctx)
|
|
},
|
|
);
|
|
|
|
engine.register_fn(
|
|
"Component::required-size",
|
|
|s: &mut WrappedDynComponent, viewport: (u16, u16)| {
|
|
s.inner.as_mut().unwrap().required_size(viewport)
|
|
},
|
|
);
|
|
|
|
// Create directory since we can't do that in the current state
|
|
engine.register_fn("hx.create-directory", create_directory);
|
|
|
|
engine
|
|
}
|
|
|
|
pub fn configure_engine() -> std::rc::Rc<std::cell::RefCell<steel::steel_vm::engine::Engine>> {
|
|
let engine = configure_engine_impl(steel::steel_vm::engine::Engine::new());
|
|
|
|
std::rc::Rc::new(std::cell::RefCell::new(engine))
|
|
}
|
|
|
|
#[derive(Default, Debug)]
|
|
pub struct ExportedIdentifiers {
|
|
identifiers: Arc<RwLock<HashSet<String>>>,
|
|
docs: Arc<RwLock<HashMap<String, String>>>,
|
|
}
|
|
|
|
impl ExportedIdentifiers {
|
|
pub(crate) fn engine_get_doc(ident: &str) -> Option<String> {
|
|
EXPORTED_IDENTIFIERS
|
|
.docs
|
|
.read()
|
|
.unwrap()
|
|
.get(ident)
|
|
.cloned()
|
|
}
|
|
}
|
|
|
|
fn get_highlighted_text(cx: &mut Context) -> String {
|
|
let (view, doc) = current_ref!(cx.editor);
|
|
let text = doc.text().slice(..);
|
|
doc.selection(view.id).primary().slice(text).to_string()
|
|
}
|
|
|
|
fn current_selection(cx: &mut Context) -> Selection {
|
|
let (view, doc) = current_ref!(cx.editor);
|
|
doc.selection(view.id).clone()
|
|
}
|
|
|
|
fn set_selection(cx: &mut Context, selection: Selection) {
|
|
let (view, doc) = current!(cx.editor);
|
|
doc.set_selection(view.id, selection)
|
|
}
|
|
|
|
fn current_line_number(cx: &mut Context) -> usize {
|
|
let (view, doc) = current_ref!(cx.editor);
|
|
helix_core::coords_at_pos(
|
|
doc.text().slice(..),
|
|
doc.selection(view.id)
|
|
.primary()
|
|
.cursor(doc.text().slice(..)),
|
|
)
|
|
.row
|
|
}
|
|
|
|
fn get_selection(cx: &mut Context) -> String {
|
|
let (view, doc) = current_ref!(cx.editor);
|
|
let text = doc.text().slice(..);
|
|
|
|
let grapheme_start = doc.selection(view.id).primary().cursor(text);
|
|
let grapheme_end = graphemes::next_grapheme_boundary(text, grapheme_start);
|
|
|
|
if grapheme_start == grapheme_end {
|
|
return "".into();
|
|
}
|
|
|
|
let grapheme = text.slice(grapheme_start..grapheme_end).to_string();
|
|
|
|
let printable = grapheme.chars().fold(String::new(), |mut s, c| {
|
|
match c {
|
|
'\0' => s.push_str("\\0"),
|
|
'\t' => s.push_str("\\t"),
|
|
'\n' => s.push_str("\\n"),
|
|
'\r' => s.push_str("\\r"),
|
|
_ => s.push(c),
|
|
}
|
|
|
|
s
|
|
});
|
|
|
|
printable
|
|
}
|
|
|
|
pub fn load_buffer(cx: &mut Context) -> anyhow::Result<()> {
|
|
let (text, path) = {
|
|
let (_, doc) = current!(cx.editor);
|
|
|
|
let text = doc.text().to_string();
|
|
let path = current_path(cx);
|
|
|
|
(text, path)
|
|
};
|
|
|
|
let callback = async move {
|
|
let call: Box<dyn FnOnce(&mut Editor, &mut Compositor, &mut job::Jobs)> = Box::new(
|
|
move |editor: &mut Editor, compositor: &mut Compositor, jobs: &mut job::Jobs| {
|
|
let mut ctx = Context {
|
|
register: None,
|
|
count: None,
|
|
editor,
|
|
callback: Vec::new(),
|
|
on_next_key_callback: None,
|
|
jobs,
|
|
};
|
|
|
|
let output = ENGINE.with(|x| {
|
|
let mut guard = x.borrow_mut();
|
|
|
|
guard
|
|
.with_mut_reference::<Context, Context>(&mut ctx)
|
|
.consume(move |engine, args| {
|
|
let context = args[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
match path.clone() {
|
|
Some(path) => engine.compile_and_run_raw_program_with_path(
|
|
// TODO: Figure out why I have to clone this text here.
|
|
text.clone(),
|
|
PathBuf::from(path),
|
|
),
|
|
None => engine.compile_and_run_raw_program(text.clone()),
|
|
}
|
|
})
|
|
});
|
|
|
|
match output {
|
|
Ok(output) => {
|
|
let (output, _success) = (Tendril::from(format!("{:?}", output)), true);
|
|
|
|
let contents = ui::Markdown::new(
|
|
format!("```\n{}\n```", output),
|
|
editor.syn_loader.clone(),
|
|
);
|
|
let popup = Popup::new("engine", contents).position(Some(
|
|
helix_core::Position::new(editor.cursor().0.unwrap_or_default().row, 2),
|
|
));
|
|
compositor.replace_or_push("engine", popup);
|
|
}
|
|
Err(e) => ENGINE.with(|x| {
|
|
present_error_inside_engine_context(&mut ctx, &mut x.borrow_mut(), e)
|
|
}),
|
|
}
|
|
},
|
|
);
|
|
Ok(call)
|
|
};
|
|
cx.jobs.local_callback(callback);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_helix_scm_path() -> String {
|
|
helix_module_file().to_str().unwrap().to_string()
|
|
}
|
|
|
|
fn get_init_scm_path() -> String {
|
|
steel_init_file().to_str().unwrap().to_string()
|
|
}
|
|
|
|
/// Get the current path! See if this can be done _without_ this function?
|
|
// TODO:
|
|
fn current_path(cx: &mut Context) -> Option<String> {
|
|
let current_focus = cx.editor.tree.focus;
|
|
let view = cx.editor.tree.get(current_focus);
|
|
let doc = &view.doc;
|
|
// Lifetime of this needs to be tied to the existing document
|
|
let current_doc = cx.editor.documents.get(doc);
|
|
current_doc.and_then(|x| x.path().and_then(|x| x.to_str().map(|x| x.to_string())))
|
|
}
|
|
|
|
fn set_scratch_buffer_name(cx: &mut Context, name: String) {
|
|
let current_focus = cx.editor.tree.focus;
|
|
let view = cx.editor.tree.get(current_focus);
|
|
let doc = &view.doc;
|
|
// Lifetime of this needs to be tied to the existing document
|
|
let current_doc = cx.editor.documents.get_mut(doc);
|
|
|
|
if let Some(current_doc) = current_doc {
|
|
current_doc.name = Some(name);
|
|
}
|
|
}
|
|
|
|
fn cx_current_focus(cx: &mut Context) -> helix_view::ViewId {
|
|
cx.editor.tree.focus
|
|
}
|
|
|
|
fn cx_get_document_id(cx: &mut Context, view_id: helix_view::ViewId) -> DocumentId {
|
|
cx.editor.tree.get(view_id).doc
|
|
}
|
|
|
|
fn cx_get_document<'a>(cx: &'a mut Context, doc_id: DocumentId) -> &'a Document {
|
|
cx.editor.documents.get(&doc_id).unwrap()
|
|
}
|
|
|
|
fn document_to_text(doc: &Document) -> SteelRopeSlice {
|
|
SteelRopeSlice::new(doc.text().clone())
|
|
}
|
|
|
|
fn cx_is_document_in_view(cx: &mut Context, doc_id: DocumentId) -> Option<helix_view::ViewId> {
|
|
cx.editor
|
|
.tree
|
|
.traverse()
|
|
.find(|(_, v)| v.doc == doc_id)
|
|
.map(|(id, _)| id)
|
|
}
|
|
|
|
fn cx_document_exists(cx: &mut Context, doc_id: DocumentId) -> bool {
|
|
cx.editor.documents.get(&doc_id).is_some()
|
|
}
|
|
|
|
fn document_path(doc: &Document) -> Option<String> {
|
|
doc.path().and_then(|x| x.to_str()).map(|x| x.to_string())
|
|
}
|
|
|
|
fn cx_editor_all_documents(cx: &mut Context) -> Vec<DocumentId> {
|
|
cx.editor.documents.keys().copied().collect()
|
|
}
|
|
|
|
fn cx_switch(cx: &mut Context, doc_id: DocumentId) {
|
|
cx.editor.switch(doc_id, Action::VerticalSplit)
|
|
}
|
|
|
|
fn cx_get_mode(cx: &mut Context) -> Mode {
|
|
cx.editor.mode
|
|
}
|
|
|
|
fn cx_set_mode(cx: &mut Context, mode: Mode) {
|
|
cx.editor.mode = mode
|
|
}
|
|
|
|
// Overlay the dynamic component, see what happens?
|
|
// Probably need to pin the values to this thread - wrap it in a shim which pins the value
|
|
// to this thread? - call methods on the thread local value?
|
|
fn push_component(cx: &mut Context, component: &mut WrappedDynComponent) {
|
|
log::info!("Pushing dynamic component!");
|
|
|
|
let inner = component.inner.take().unwrap();
|
|
|
|
let callback = async move {
|
|
let call: Box<dyn FnOnce(&mut Editor, &mut Compositor, &mut job::Jobs)> = Box::new(
|
|
move |_editor: &mut Editor, compositor: &mut Compositor, _| compositor.push(inner),
|
|
);
|
|
Ok(call)
|
|
};
|
|
cx.jobs.local_callback(callback);
|
|
}
|
|
|
|
fn enqueue_command(cx: &mut Context, callback_fn: SteelVal) {
|
|
let rooted = callback_fn.as_rooted();
|
|
|
|
let callback = async move {
|
|
let call: Box<dyn FnOnce(&mut Editor, &mut Compositor, &mut job::Jobs)> = Box::new(
|
|
move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| {
|
|
let mut ctx = Context {
|
|
register: None,
|
|
count: None,
|
|
editor,
|
|
callback: Vec::new(),
|
|
on_next_key_callback: None,
|
|
jobs,
|
|
};
|
|
|
|
let cloned_func = rooted.value();
|
|
|
|
ENGINE.with(|x| {
|
|
let mut guard = x.borrow_mut();
|
|
|
|
if let Err(e) = guard
|
|
.with_mut_reference::<Context, Context>(&mut ctx)
|
|
.consume(move |engine, args| {
|
|
let context = args[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
engine.call_function_with_args(cloned_func.clone(), Vec::new())
|
|
})
|
|
{
|
|
present_error_inside_engine_context(&mut ctx, &mut guard, e);
|
|
}
|
|
})
|
|
},
|
|
);
|
|
Ok(call)
|
|
};
|
|
cx.jobs.local_callback(callback);
|
|
}
|
|
|
|
// Apply arbitrary delay for update rate...
|
|
fn enqueue_command_with_delay(cx: &mut Context, delay: SteelVal, callback_fn: SteelVal) {
|
|
let rooted = callback_fn.as_rooted();
|
|
|
|
let callback = async move {
|
|
let delay = delay.int_or_else(|| panic!("FIX ME")).unwrap();
|
|
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(delay as u64)).await;
|
|
|
|
let call: Box<dyn FnOnce(&mut Editor, &mut Compositor, &mut job::Jobs)> = Box::new(
|
|
move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| {
|
|
let mut ctx = Context {
|
|
register: None,
|
|
count: None,
|
|
editor,
|
|
callback: Vec::new(),
|
|
on_next_key_callback: None,
|
|
jobs,
|
|
};
|
|
|
|
let cloned_func = rooted.value();
|
|
|
|
ENGINE.with(|x| {
|
|
let mut guard = x.borrow_mut();
|
|
|
|
if let Err(e) = guard
|
|
.with_mut_reference::<Context, Context>(&mut ctx)
|
|
.consume(move |engine, args| {
|
|
let context = args[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
engine.call_function_with_args(cloned_func.clone(), Vec::new())
|
|
})
|
|
{
|
|
present_error_inside_engine_context(&mut ctx, &mut guard, e);
|
|
}
|
|
})
|
|
},
|
|
);
|
|
Ok(call)
|
|
};
|
|
cx.jobs.local_callback(callback);
|
|
}
|
|
|
|
// value _must_ be a future here. Otherwise awaiting will cause problems!
|
|
fn await_value(cx: &mut Context, value: SteelVal, callback_fn: SteelVal) {
|
|
if !value.is_future() {
|
|
return;
|
|
}
|
|
|
|
let rooted = callback_fn.as_rooted();
|
|
|
|
let callback = async move {
|
|
let future_value = value.as_future().unwrap().await;
|
|
|
|
let call: Box<dyn FnOnce(&mut Editor, &mut Compositor, &mut job::Jobs)> = Box::new(
|
|
move |editor: &mut Editor, _compositor: &mut Compositor, jobs: &mut job::Jobs| {
|
|
let mut ctx = Context {
|
|
register: None,
|
|
count: None,
|
|
editor,
|
|
callback: Vec::new(),
|
|
on_next_key_callback: None,
|
|
jobs,
|
|
};
|
|
|
|
let cloned_func = rooted.value();
|
|
|
|
match future_value {
|
|
Ok(inner) => {
|
|
let callback = move |engine: &mut Engine, args: Vec<SteelVal>| {
|
|
let context = args[0].clone();
|
|
engine.update_value("*helix.cx*", context);
|
|
|
|
// args.push(inner);
|
|
engine.call_function_with_args(cloned_func.clone(), vec![inner])
|
|
};
|
|
|
|
ENGINE.with(|x| {
|
|
let mut guard = x.borrow_mut();
|
|
|
|
if let Err(e) = guard
|
|
.with_mut_reference::<Context, Context>(&mut ctx)
|
|
.consume_once(callback)
|
|
{
|
|
present_error_inside_engine_context(&mut ctx, &mut guard, e);
|
|
}
|
|
})
|
|
}
|
|
Err(e) => ENGINE.with(|x| {
|
|
present_error_inside_engine_context(&mut ctx, &mut x.borrow_mut(), e)
|
|
}),
|
|
}
|
|
},
|
|
);
|
|
Ok(call)
|
|
};
|
|
cx.jobs.local_callback(callback);
|
|
}
|
|
// Check that we successfully created a directory?
|
|
fn create_directory(path: String) {
|
|
let path = helix_stdx::path::canonicalize(&PathBuf::from(path));
|
|
|
|
if path.exists() {
|
|
return;
|
|
} else {
|
|
std::fs::create_dir(path).unwrap();
|
|
}
|
|
}
|
|
|
|
pub fn cx_pos_within_text(cx: &mut Context) -> usize {
|
|
let (view, doc) = current_ref!(cx.editor);
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
let selection = doc.selection(view.id).clone();
|
|
|
|
let pos = selection.primary().cursor(text);
|
|
|
|
pos
|
|
}
|
|
|
|
pub fn get_helix_cwd(_cx: &mut Context) -> Option<String> {
|
|
helix_stdx::env::current_working_dir()
|
|
.as_os_str()
|
|
.to_str()
|
|
.map(|x| x.into())
|
|
}
|
|
|
|
// Special newline...
|
|
pub fn custom_insert_newline(cx: &mut Context, indent: String) {
|
|
let (view, doc) = current_ref!(cx.editor);
|
|
|
|
// let rope = doc.text().clone();
|
|
|
|
let text = doc.text().slice(..);
|
|
|
|
let contents = doc.text();
|
|
let selection = doc.selection(view.id).clone();
|
|
let mut ranges = helix_core::SmallVec::with_capacity(selection.len());
|
|
|
|
// TODO: this is annoying, but we need to do it to properly calculate pos after edits
|
|
let mut global_offs = 0;
|
|
|
|
let mut transaction =
|
|
helix_core::Transaction::change_by_selection(contents, &selection, |range| {
|
|
let pos = range.cursor(text);
|
|
|
|
let prev = if pos == 0 {
|
|
' '
|
|
} else {
|
|
contents.char(pos - 1)
|
|
};
|
|
let curr = contents.get_char(pos).unwrap_or(' ');
|
|
|
|
let current_line = text.char_to_line(pos);
|
|
let line_is_only_whitespace = text
|
|
.line(current_line)
|
|
.chars()
|
|
.all(|char| char.is_ascii_whitespace());
|
|
|
|
let mut new_text = String::new();
|
|
|
|
// If the current line is all whitespace, insert a line ending at the beginning of
|
|
// the current line. This makes the current line empty and the new line contain the
|
|
// indentation of the old line.
|
|
let (from, to, local_offs) = if line_is_only_whitespace {
|
|
let line_start = text.line_to_char(current_line);
|
|
new_text.push_str(doc.line_ending.as_str());
|
|
|
|
(line_start, line_start, new_text.chars().count())
|
|
} else {
|
|
// If we are between pairs (such as brackets), we want to
|
|
// insert an additional line which is indented one level
|
|
// more and place the cursor there
|
|
let on_auto_pair = doc
|
|
.auto_pairs(cx.editor)
|
|
.and_then(|pairs| pairs.get(prev))
|
|
.map_or(false, |pair| pair.open == prev && pair.close == curr);
|
|
|
|
let local_offs = if on_auto_pair {
|
|
let inner_indent = indent.clone() + doc.indent_style.as_str();
|
|
new_text.reserve_exact(2 + indent.len() + inner_indent.len());
|
|
new_text.push_str(doc.line_ending.as_str());
|
|
new_text.push_str(&inner_indent);
|
|
let local_offs = new_text.chars().count();
|
|
new_text.push_str(doc.line_ending.as_str());
|
|
new_text.push_str(&indent);
|
|
local_offs
|
|
} else {
|
|
new_text.reserve_exact(1 + indent.len());
|
|
new_text.push_str(doc.line_ending.as_str());
|
|
new_text.push_str(&indent);
|
|
new_text.chars().count()
|
|
};
|
|
|
|
(pos, pos, local_offs)
|
|
};
|
|
|
|
let new_range = if doc.restore_cursor {
|
|
// when appending, extend the range by local_offs
|
|
Range::new(
|
|
range.anchor + global_offs,
|
|
range.head + local_offs + global_offs,
|
|
)
|
|
} else {
|
|
// when inserting, slide the range by local_offs
|
|
Range::new(
|
|
range.anchor + local_offs + global_offs,
|
|
range.head + local_offs + global_offs,
|
|
)
|
|
};
|
|
|
|
// TODO: range replace or extend
|
|
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
|
|
// can be used with cx.mode to do replace or extend on most changes
|
|
ranges.push(new_range);
|
|
global_offs += new_text.chars().count();
|
|
|
|
(from, to, Some(new_text.into()))
|
|
});
|
|
|
|
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
|
|
|
|
let (view, doc) = current!(cx.editor);
|
|
doc.apply(&transaction, view.id);
|
|
}
|
|
|
|
fn search_in_directory(cx: &mut Context, directory: String) {
|
|
let search_path = expand_tilde(&PathBuf::from(directory));
|
|
crate::commands::search_in_directory(cx, search_path);
|
|
}
|
|
|
|
// TODO: Result should create unrecoverable result, and should have a special
|
|
// recoverable result - that way we can handle both, not one in particular
|
|
fn regex_selection(cx: &mut Context, regex: String) {
|
|
if let Ok(regex) = Regex::new(®ex) {
|
|
let (view, doc) = current!(cx.editor);
|
|
let text = doc.text().slice(..);
|
|
if let Some(selection) =
|
|
helix_core::selection::select_on_matches(text, doc.selection(view.id), ®ex)
|
|
{
|
|
doc.set_selection(view.id, selection);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn replace_selection(cx: &mut Context, value: String) {
|
|
let (view, doc) = current!(cx.editor);
|
|
|
|
let selection = doc.selection(view.id);
|
|
let transaction =
|
|
helix_core::Transaction::change_by_selection(doc.text(), selection, |range| {
|
|
if !range.is_empty() {
|
|
(range.from(), range.to(), Some(value.to_owned().into()))
|
|
} else {
|
|
(range.from(), range.to(), None)
|
|
}
|
|
});
|
|
|
|
doc.apply(&transaction, view.id);
|
|
}
|
|
|
|
// TODO: Remove this!
|
|
fn move_window_to_the_left(cx: &mut Context) {
|
|
while cx
|
|
.editor
|
|
.tree
|
|
.swap_split_in_direction(helix_view::tree::Direction::Left)
|
|
.is_some()
|
|
{}
|
|
}
|
|
|
|
// TODO: Remove this!
|
|
fn move_window_to_the_right(cx: &mut Context) {
|
|
while cx
|
|
.editor
|
|
.tree
|
|
.swap_split_in_direction(helix_view::tree::Direction::Right)
|
|
.is_some()
|
|
{}
|
|
}
|