From d0d9f7b5b04873168d2653485394222636d8aa88 Mon Sep 17 00:00:00 2001 From: mattwparas Date: Thu, 17 Aug 2023 20:48:37 -0700 Subject: [PATCH] more integration, not pretty but still making progress --- helix-core/src/extensions.rs | 100 +++++++++++++-- helix-core/src/indent.rs | 200 +----------------------------- helix-term/src/commands/engine.rs | 120 ++++++++++++------ rust-toolchain.toml | 2 +- 4 files changed, 176 insertions(+), 246 deletions(-) diff --git a/helix-core/src/extensions.rs b/helix-core/src/extensions.rs index bf2fbdec0..c661eb760 100644 --- a/helix-core/src/extensions.rs +++ b/helix-core/src/extensions.rs @@ -1,29 +1,111 @@ -use steel::gc::unsafe_erased_pointers::CustomReference; +use std::{borrow::Cow, cell::Cell, rc::Rc}; + +use ropey::iter::Chars; +use steel::{ + gc::unsafe_erased_pointers::CustomReference, + rvals::Custom, + steel_vm::{builtin::BuiltInModule, register_fn::RegisterFn, register_fn::RegisterFnBorrowed}, +}; impl steel::rvals::Custom for crate::Position {} impl steel::rvals::Custom for crate::Selection {} -struct SRopeSlice<'a>(crate::RopeSlice<'a>); +pub struct SRopeSlice<'a> { + slice: crate::RopeSlice<'a>, +} steel::custom_reference!(SRopeSlice<'a>); impl<'a> CustomReference for SRopeSlice<'a> {} +// impl Custom for SRopeSlice<'static> {} + +pub struct SRopeSliceCowStr<'a>(Cow<'a, str>); +steel::custom_reference!(SRopeSliceCowStr<'a>); +impl<'a> CustomReference for SRopeSliceCowStr<'a> {} + +struct CharIter<'a>(Chars<'a>); + impl<'a> SRopeSlice<'a> { + pub fn new(slice: crate::RopeSlice<'a>) -> Self { + Self { slice } + } + pub fn char_to_byte(&self, pos: usize) -> usize { - self.0.char_to_byte(pos) + self.slice.char_to_byte(pos) } pub fn byte_slice(&'a self, lower: usize, upper: usize) -> SRopeSlice<'a> { - SRopeSlice(self.0.byte_slice(lower..upper)) + SRopeSlice { + slice: self.slice.byte_slice(lower..upper), + } } pub fn line(&'a self, cursor: usize) -> SRopeSlice<'a> { - SRopeSlice(self.0.line(cursor)) + SRopeSlice { + slice: self.slice.line(cursor), + } + } + + pub fn as_cow(&'a self) -> SRopeSliceCowStr<'a> { + SRopeSliceCowStr(std::borrow::Cow::from(self.slice)) + } + + pub fn to_string(&self) -> String { + self.slice.to_string() + } + + pub fn len_chars(&'a self) -> usize { + self.slice.len_chars() + } + + pub fn slice(&'a self, lower: usize, upper: usize) -> SRopeSlice<'a> { + SRopeSlice { + slice: self.slice.byte_slice(lower..upper), + } } - // Reference types are really sus. Not sure how this is going to work, but it might? Hopefully it cleans - // itself up as we go... - pub fn as_str(&'a self) -> Option<&'a str> { - self.0.as_str() + pub fn get_char(&'a self, index: usize) -> Option { + self.slice.get_char(index) } } + +// RegisterFn::< +// _, +// steel::steel_vm::register_fn::MarkerWrapper7<( +// Context<'_>, +// helix_view::Editor, +// helix_view::Editor, +// Context<'static>, +// )>, +// helix_view::Editor, +// >::register_fn(&mut engine, "cx-editor!", get_editor); + +pub fn rope_slice_module() -> BuiltInModule { + let mut module = BuiltInModule::new("helix/core/text"); + + // (SELF, ARG, SELFSTAT, RET, RETSTAT) + + RegisterFnBorrowed::< + _, + steel::steel_vm::register_fn::MarkerWrapper9<( + SRopeSlice<'_>, + usize, + SRopeSlice<'static>, + SRopeSlice<'_>, + SRopeSlice<'static>, + )>, + SRopeSlice, + >::register_fn_borrowed(&mut module, "slice->line", SRopeSlice::line); + + // TODO: Note the difficulty of the lifetime params here + module.register_fn("slice->string", SRopeSlice::to_string); + + // module + // .register_fn("slice-char->byte", SRopeSlice::char_to_byte) + // .register_fn_borrowed::line", SRopeSlice::line); + // .register_fn("slice->byte-slice", SRopeSlice::byte_slice); + + // module.register_fn("slice-len-chars", SRopeSlice::len_chars); + + module +} diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 4170525d6..59f9e7125 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -758,184 +758,14 @@ static LISP_WORDS: Lazy> = Lazy::new(|| words.iter().copied().collect() }); -// TODO: Allow for injecting hooks on indent -#[allow(clippy::too_many_arguments)] -fn call_indent_hook( - language_config: Option<&LanguageConfiguration>, - syntax: Option<&Syntax>, - indent_style: &IndentStyle, - tab_width: usize, - text: RopeSlice, - line_before: usize, - line_before_end_pos: usize, - current_line: usize, -) -> Option { - if let Some(config) = language_config { - // TODO: If possible, this would be very cool to be implemented in steel itself. If not, - // a rust native method that is embedded in a dylib that this uses would also be helpful - if config.language_id == "scheme" { - log::info!("Implement better scheme indent mode!"); - - // TODO: walk backwards to find the previous s-expression? - - // log::info!("{}", text); - // log::info!("{}", text.line(line_before)); - - let byte_pos = text.char_to_byte(line_before_end_pos); - - let text_up_to_cursor = text.byte_slice(0..byte_pos); - - let mut cursor = line_before; - let mut depth = 0; - - loop { - let line = text_up_to_cursor.line(cursor); - - // We want to ignore comments - if let Some(l) = line.as_str() { - if l.starts_with(";") { - if cursor == 0 { - break; - } - - cursor -= 1; - - continue; - } - } - - // log::info!("Line: {}", line); - - for (index, char) in line.chars_at(line.len_chars()).reversed().enumerate() { - match char { - ')' | ']' | '}' => { - depth += 1; - } - '(' | '[' | '{' => { - // stack.push('(') - - if depth == 0 { - log::info!( - "Found unmatched paren on line, index: {}, {}", - line, - index - ); - - // TODO: Here, then walk FORWARD, parsing the identifiers until there is a thing to line up with, for example: - // (define (foo-bar) RET) <- - // ^probably indent to here - - let offset = line.len_chars() - index; - - let mut char_iter_from_paren = - line.chars_at(line.len_chars() - index).enumerate(); - - let end; - - // Walk until we've found whitespace, and then crunch the whitespace until the start of the next symbol - // if there is _no_ symbol after that, we should just default to the default behavior - while let Some((index, char)) = char_iter_from_paren.next() { - if char.is_whitespace() { - let mut last = index; - - // This is the end of our range - end = index; - - // If we have multiple parens in a row, match to the start: - // for instance, (cond [(equal? x 10) RET]) - // ^ We want to line up to this - // - // To do so, just create an indent that is the width of the offset. - match line.get_char(offset) { - Some('(' | '[' | '{') => { - return Some(" ".repeat(offset)); - } - _ => {} - } - - // TODO: Don't unwrap here, we don't want that - // if LISP_WORDS.contains( - // line.slice(offset..offset + end).as_str().unwrap(), - // ) { - // return Some(" ".repeat(offset + 1)); - // } - - if line - .slice(offset..offset + end) - .as_str() - .map(|x| LISP_WORDS.contains(x)) - .unwrap_or_default() - { - return Some(" ".repeat(offset + 1)); - } - - for _ in char_iter_from_paren - .take_while(|(_, x)| x.is_whitespace()) - { - last += 1; - } - - // If we have something like (list RET) - // We want the result to look like: - // (list - // ) - // - // So we special case the lack of an additional word after - // the first symbol - if line.len_chars() == last + offset + 1 { - if let Some(c) = line.get_char(last + offset) { - if c.is_whitespace() { - return Some(" ".repeat(offset + 1)); - } - } - } - - return Some(" ".repeat(last + offset + 1)); - } - } - - log::info!("Found no symbol after the initial opening symbol"); - - return Some(" ".repeat(offset + 1)); - } - - depth -= 1; - } - _ => {} - } - } - - if cursor == 0 { - break; - } - - cursor -= 1; - } - - // TODO: Implement heuristic for large files so we don't necessarily traverse the entire file backwards to check the matched parens? - return Some("".to_string()); - } - } - - None -} - -// TODO: Do this to allow for custom indent operations. Unfortunately, we'll have to wrap -// all of the lifetimes up into references. -// impl<'a> steel::gc::unsafe_erased_pointers::CustomReference for RopeSlice<'a> {} - /// TODO: Come up with some elegant enough FFI for this, so that Steel can expose an API for this. /// Problem is - the issues with the `Any` type and using things with type id. #[allow(clippy::too_many_arguments)] pub fn custom_indent_for_newline( language_config: Option<&LanguageConfiguration>, - _syntax: Option<&Syntax>, - _indent_style: &IndentStyle, - _tab_width: usize, text: RopeSlice, line_before: usize, line_before_end_pos: usize, - _current_line: usize, ) -> Option { if let Some(config) = language_config { // TODO: If possible, this would be very cool to be implemented in steel itself. If not, @@ -943,11 +773,6 @@ pub fn custom_indent_for_newline( if config.language_id == "scheme" { log::info!("Implement better scheme indent mode!"); - // TODO: walk backwards to find the previous s-expression? - - // log::info!("{}", text); - // log::info!("{}", text.line(line_before)); - let byte_pos = text.char_to_byte(line_before_end_pos); let text_up_to_cursor = text.byte_slice(0..byte_pos); @@ -955,7 +780,6 @@ pub fn custom_indent_for_newline( let mut cursor = line_before; let mut depth = 0; - // for line in text_up_to_cursor.lines().reversed() { loop { let line = text_up_to_cursor.line(cursor); @@ -977,8 +801,6 @@ pub fn custom_indent_for_newline( depth += 1; } '(' | '[' | '{' => { - // stack.push('(') - if depth == 0 { log::info!( "Found unmatched paren on line, index: {}, {}", @@ -986,7 +808,7 @@ pub fn custom_indent_for_newline( index ); - // TODO: Here, then walk FORWARD, parsing the identifiers until there is a thing to line up with, for example: + // Here, then walk FORWARD, parsing the identifiers until there is a thing to line up with, for example: // (define (foo-bar) RET) <- // ^probably indent to here @@ -1018,13 +840,6 @@ pub fn custom_indent_for_newline( _ => {} } - // TODO: Don't unwrap here, we don't want that - // if LISP_WORDS.contains( - // line.slice(offset..offset + end).as_str().unwrap(), - // ) { - // return Some(" ".repeat(offset + 1)); - // } - if line .slice(offset..offset + end) .as_str() @@ -1121,16 +936,9 @@ pub fn indent_for_newline( // TODO: @Matt - see if we can shell out to the steel plugin to identify indentation length // Something naive for steel could work, use the parser and - if let Some(indent_level) = custom_indent_for_newline( - language_config, - syntax, - indent_style, - tab_width, - text, - line_before, - line_before_end_pos, - current_line, - ) { + if let Some(indent_level) = + custom_indent_for_newline(language_config, text, line_before, line_before_end_pos) + { return indent_level; } diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs index 43e14ec35..dca37ccbb 100644 --- a/helix-term/src/commands/engine.rs +++ b/helix-term/src/commands/engine.rs @@ -1,5 +1,10 @@ use fuzzy_matcher::FuzzyMatcher; -use helix_core::{graphemes, shellwords::Shellwords, Selection, Tendril}; +use helix_core::{ + extensions::{rope_slice_module, SRopeSlice}, + graphemes, + shellwords::Shellwords, + Selection, Tendril, +}; use helix_view::{ document::Mode, editor::{Action, ConfigEvent}, @@ -14,7 +19,10 @@ use steel::{ rvals::{ as_underlying_type, AsRefMutSteelValFromRef, AsRefSteelVal, FromSteelVal, IntoSteelVal, }, - steel_vm::{engine::Engine, register_fn::RegisterFn}, + steel_vm::{ + engine::Engine, + register_fn::{RegisterFn, RegisterFnBorrowed}, + }, SteelErr, SteelVal, }; @@ -235,32 +243,41 @@ impl ScriptingEngine { }; if let Some(extension) = extension { - if let SteelVal::HashMapV(map) = BUFFER_OR_EXTENSION_KEYBINDING_MAP.with(|x| x.clone()) + if let SteelVal::Boxed(boxed_map) = + BUFFER_OR_EXTENSION_KEYBINDING_MAP.with(|x| x.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::( - inner.borrow().as_ref(), - ) { - return Some(value.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::( + inner.borrow().as_ref(), + ) { + return Some(value.clone()); + } } } } } } - // TODO: Remove these clones - if let SteelVal::HashMapV(map) = REVERSE_BUFFER_MAP.with(|x| x.clone()) { - if let Some(label) = map.get(&SteelVal::IntV(document_id_to_usize(doc_id) as isize)) { - if let SteelVal::HashMapV(map) = - BUFFER_OR_EXTENSION_KEYBINDING_MAP.with(|x| x.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 Some(value) = map.get(label) { - if let SteelVal::Custom(inner) = value { - if let Some(_) = steel::rvals::as_underlying_type::( - inner.borrow().as_ref(), - ) { - return Some(value.clone()); + 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::( + inner.borrow().as_ref(), + ) + { + return Some(value.clone()); + } + } } } } @@ -425,32 +442,32 @@ impl ScriptingEngine { // 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>> = - Lazy::new(|| { - let mut containers = DylibContainers::new(); +// pub(crate) static EXTERNAL_DYLIBS: Lazy>> = +// Lazy::new(|| { +// let mut containers = DylibContainers::new(); - // Load the plugins with respect to the extensions directory. - // containers.load_modules_from_directory(Some( - // helix_loader::config_dir() - // .join("extensions") - // .to_str() - // .unwrap() - // .to_string(), - // )); +// // Load the plugins with respect to the extensions directory. +// // containers.load_modules_from_directory(Some( +// // helix_loader::config_dir() +// // .join("extensions") +// // .to_str() +// // .unwrap() +// // .to_string(), +// // )); - println!("Found dylibs: {}", containers.containers.len()); +// println!("Found dylibs: {}", containers.containers.len()); - let modules = containers.create_commands(); +// let modules = containers.create_commands(); - println!("Modules length: {}", modules.len()); +// println!("Modules length: {}", modules.len()); - Arc::new(RwLock::new(ExternalContainersAndModules { - containers, - modules, - })) +// Arc::new(RwLock::new(ExternalContainersAndModules { +// containers, +// modules, +// })) - // Arc::new(RwLock::new(containers)) - }); +// // Arc::new(RwLock::new(containers)) +// }); pub fn initialize_engine() { ENGINE.with(|x| x.borrow().globals().first().copied()); @@ -838,6 +855,25 @@ fn configure_engine() -> std::rc::Rcslice", document_to_text); + + RegisterFnBorrowed::< + _, + steel::steel_vm::register_fn::MarkerWrapper9<( + Document, + Document, + SRopeSlice<'_>, + SRopeSlice<'static>, + )>, + SRopeSlice, + >::register_fn_borrowed(&mut rope_slice_module, "document->slice", document_to_text); + + engine.register_module(rope_slice_module); + // engine.register_fn("helix-current-keymap", get_keymap); // engine.register_fn("helix-empty-keymap", empty_keymap); // engine.register_fn("helix-default-keymap", default_keymap); @@ -1454,6 +1490,10 @@ fn get_document(editor: &mut Editor, doc_id: DocumentId) -> &Document { editor.documents.get(&doc_id).unwrap() } +fn document_to_text(doc: &Document) -> SRopeSlice<'_> { + SRopeSlice::new(doc.text().slice(..)) +} + fn is_document_in_view(editor: &mut Editor, doc_id: DocumentId) -> Option { editor .tree diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 2abc56652..0e0d8bc5e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.65.0" +channel = "1.70.0" components = ["rustfmt", "rust-src"]