diff --git a/Cargo.lock b/Cargo.lock index e9bd25f47..84e155f85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2910,6 +2910,8 @@ dependencies = [ "log", "once_cell", "rustls", + "serde", + "serde_json", "url", "webpki", "webpki-roots", diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index db780ed97..069e80b6a 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use once_cell::sync::Lazy; use tree_sitter::{Query, QueryCursor, QueryPredicateArg}; use crate::{ @@ -710,6 +711,211 @@ pub fn treesitter_indent_for_pos( Some(result.as_string(indent_style)) } +// TODO: Make this be customizable, similar to how it works for vim +static LISP_WORDS: Lazy> = Lazy::new(|| { + let words = &[ + "define-syntax", + "let*", + "lambda", + "λ", + "case", + "=>", + "quote-splicing", + "unquote-splicing", + "set!", + "let", + "letrec", + "letrec-syntax", + "let-values", + "let*-values", + "do", + "else", + "cond", + "unquote", + "begin", + "let-syntax", + "and", + "quasiquote", + "letrec", + "delay", + "or", + "identifier-syntax", + "assert", + "library", + "export", + "import", + "rename", + "only", + "except", + "prefix", + "provide", + "require", + "define", + "cond", + "if", + "syntax-rules", + "when", + "unless", + ]; + + words.iter().copied().collect() +}); + +/// 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, + // 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; + + // for line in text_up_to_cursor.lines().reversed() { + 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)); + } + + 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 +} + /// 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 #[allow(clippy::too_many_arguments)] @@ -742,6 +948,23 @@ pub fn indent_for_newline( return indent; }; } + + // 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, + ) { + 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-core/src/syntax.rs b/helix-core/src/syntax.rs index 6514b40f5..17c419f43 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -534,13 +534,24 @@ impl LanguageConfiguration { } fn load_query(&self, kind: &str) -> Option { + log::warn!("Loading tree sitter for {}", kind); + let query_text = read_query(&self.language_id, kind); if query_text.is_empty() { + log::warn!("Query text is empty, returning early"); + return None; } let lang = self.highlight_config.get()?.as_ref()?.language; Query::new(lang, &query_text) .map_err(|e| { + log::warn!( + "Failed to parse {} queries for {}: {}", + kind, + self.language_id, + e + ); + log::error!( "Failed to parse {} queries for {}: {}", kind, diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 16955187e..34ce9d6dc 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -584,5 +584,8 @@ fn mtime(path: &Path) -> Result { /// directory pub fn load_runtime_file(language: &str, filename: &str) -> Result { let path = crate::runtime_file(&PathBuf::new().join("queries").join(language).join(filename)); + + log::info!("Loading indents for language: {}", language); + std::fs::read_to_string(path) } diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs index b01f1be51..5f8998ed9 100644 --- a/helix-term/src/commands/engine.rs +++ b/helix-term/src/commands/engine.rs @@ -29,7 +29,7 @@ use crate::{ use super::{ insert::{insert_char, insert_string}, - Context, MappableCommand, TYPABLE_COMMAND_LIST, + shell_impl, Context, MappableCommand, TYPABLE_COMMAND_LIST, }; thread_local! { @@ -164,6 +164,9 @@ fn configure_background_thread() { fn configure_engine() -> std::rc::Rc> { let mut engine = steel::steel_vm::engine::Engine::new(); + // Get the current OS + engine.register_fn("current-os!", || std::env::consts::OS); + let mut module = BuiltInModule::new("helix/core/keybindings".to_string()); module.register_fn("set-keybindings!", SharedKeyBindingsEventQueue::merge); @@ -224,6 +227,7 @@ fn configure_engine() -> std::rc::Rc String { .unwrap() .to_string() } + +fn run_shell_command_text( + cx: &mut Context, + args: &[Cow], + _event: PromptEvent, +) -> anyhow::Result { + let shell = cx.editor.config().shell.clone(); + let args = args.join(" "); + + let (output, success) = shell_impl(&shell, &args, None)?; + + if success { + Ok(output.to_string()) + } else { + anyhow::bail!("Command failed!: {}", output.to_string()) + } +} diff --git a/helix.scm b/helix.scm index 077f9cae9..f04281ca9 100644 --- a/helix.scm +++ b/helix.scm @@ -24,8 +24,7 @@ run-highlight make-minor-mode! git-status - reload-helix-scm -) + reload-helix-scm) ;;@doc diff --git a/languages.toml b/languages.toml index 9a0a538a0..eb8613e5c 100644 --- a/languages.toml +++ b/languages.toml @@ -1705,7 +1705,7 @@ source = { git = "https://github.com/metio/tree-sitter-ssh-client-config", rev = name = "scheme" scope = "source.scheme" injection-regex = "scheme" -file-types = ["ss"] # "scm", +file-types = ["ss", "scm"] # "scm", roots = [] comment-token = ";" indent = { tab-width = 2, unit = " " }