From 72aa2d9520b8d6a6be08991d8b4c4cdd3284baca Mon Sep 17 00:00:00 2001 From: mattwparas Date: Sat, 19 Aug 2023 22:13:56 -0700 Subject: [PATCH] indenting full in steel now --- helix-core/src/indent.rs | 292 +++++++++++++++--------------- helix-term/src/commands/engine.rs | 178 ++++++++++++++++-- 2 files changed, 312 insertions(+), 158 deletions(-) diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 8da8c02aa..b24740565 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -760,144 +760,150 @@ static LISP_WORDS: Lazy> = Lazy::new(|| /// 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>, - text: RopeSlice, - line_before: usize, - line_before_end_pos: 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!"); - - 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 - let l = std::borrow::Cow::from(line); - if l.trim_start().starts_with(";") { - if cursor == 0 { - break; - } - - cursor -= 1; - - continue; - } - - for (index, char) in line.chars_at(line.len_chars()).reversed().enumerate() { - match char { - ')' | ']' | '}' => { - depth += 1; - } - '(' | '[' | '{' => { - if depth == 0 { - log::info!( - "Found unmatched paren on line, index: {}, {}", - line, - index - ); - - // 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(offset).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)); - } - _ => {} - } - - 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 -} +// #[allow(clippy::too_many_arguments)] +// pub fn custom_indent_for_newline( +// language_config: Option<&LanguageConfiguration>, +// text: RopeSlice, +// line_before: usize, +// line_before_end_pos: 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!"); + +// 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 +// let l = std::borrow::Cow::from(line); +// if l.trim_start().starts_with(";") { +// if cursor == 0 { +// break; +// } + +// cursor -= 1; + +// continue; +// } + +// for (index, char) in line.chars_at(line.len_chars()).reversed().enumerate() { +// // log::info!("Char: {}, Index: {}", char, index); + +// match char { +// ')' | ']' | '}' => { +// depth += 1; +// } +// '(' | '[' | '{' => { +// if depth == 0 { +// log::info!( +// "Found unmatched paren on line, index: {}, {}", +// line, +// index +// ); + +// // 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; + +// log::info!("Offset: {}", offset); + +// let mut char_iter_from_paren = line.chars_at(offset).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)); +// } +// _ => {} +// } + +// 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)); +// } +// } +// } + +// log::info!("Last + offset + 1: {}", last + 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 +// } /// Returns the indentation for a new line. /// This is done either using treesitter, or if that's not available by copying the indentation from the current line @@ -935,11 +941,11 @@ 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, text, line_before, line_before_end_pos) - { - return indent_level; - } + // if let Some(indent_level) = + // custom_indent_for_newline(language_config, text, line_before, line_before_end_pos) + // { + // return indent_level; + // } let indent_level = indent_level_for_line(text.line(current_line), tab_width, indent_width); indent_style.as_str().repeat(indent_level) diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs index dca37ccbb..a08ad11d4 100644 --- a/helix-term/src/commands/engine.rs +++ b/helix-term/src/commands/engine.rs @@ -1,9 +1,9 @@ use fuzzy_matcher::FuzzyMatcher; use helix_core::{ - extensions::{rope_slice_module, SRopeSlice}, + extensions::{rope_module, rope_slice_module, SRopeSlice, SteelRopeSlice}, graphemes, shellwords::Shellwords, - Selection, Tendril, + Range, Selection, Tendril, }; use helix_view::{ document::Mode, @@ -52,6 +52,7 @@ use crate::{ use self::components::SteelDynamicComponent; use super::{ + indent, insert::{insert_char, insert_string}, plugin::{DylibContainers, ExternalModule}, shell_impl, Context, MappableCommand, TYPABLE_COMMAND_LIST, @@ -839,6 +840,11 @@ fn configure_engine() -> std::rc::Rcpos", cx_pos_within_text); + // Load native modules from the directory. Another idea - have a separate dlopen loading system // in place that does not use the type id, and instead we generate the module after the dylib // is added. That way functions _must_ have a specific signature, and then we add the integration @@ -855,22 +861,24 @@ fn configure_engine() -> std::rc::Rcslice", document_to_text); // Load the ropes + slice module // engine.register_module(rope_slice_module()); // rope_slice_module.register_fn("document->slice", 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); + // 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); @@ -1490,8 +1498,8 @@ 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 document_to_text(doc: &Document) -> SteelRopeSlice { + SteelRopeSlice::new(doc.text().clone()) } fn is_document_in_view(editor: &mut Editor, doc_id: DocumentId) -> Option { @@ -1746,3 +1754,143 @@ fn set_options( .send(ConfigEvent::Update(config))?; Ok(()) } + +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 +} + +// 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 { + // let indent = indent::indent_for_newline( + // doc.language_config(), + // doc.syntax(), + // &doc.indent_style, + // doc.tab_width(), + // text, + // current_line, + // pos, + // current_line, + // ); + + // let cloned_func = thunk.clone(); + // let steel_rope = SteelRopeSlice::new(rope.clone()).into_steelval().unwrap(); + + // let indent = if let Ok(result) = ENGINE.with(|x| { + // x.borrow_mut().call_function_with_args( + // cloned_func, + // vec![ + // steel_rope, + // current_line.into_steelval().unwrap(), + // pos.into_steelval().unwrap(), + // ], + // ) + // }) { + // result.as_string().unwrap().to_string() + // } else { + // "".to_string() + // }; + + // 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); +}