mirror of https://github.com/helix-editor/helix
add scheme indentation
parent
c0ea1ebbf4
commit
7dd04240b2
|
@ -2910,6 +2910,8 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustls",
|
"rustls",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"url",
|
"url",
|
||||||
"webpki",
|
"webpki",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
|
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -710,6 +711,211 @@ pub fn treesitter_indent_for_pos(
|
||||||
Some(result.as_string(indent_style))
|
Some(result.as_string(indent_style))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Make this be customizable, similar to how it works for vim
|
||||||
|
static LISP_WORDS: Lazy<std::collections::HashSet<&'static str>> = 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<String> {
|
||||||
|
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.
|
/// 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
|
/// 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
@ -742,6 +948,23 @@ pub fn indent_for_newline(
|
||||||
return indent;
|
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);
|
let indent_level = indent_level_for_line(text.line(current_line), tab_width, indent_width);
|
||||||
indent_style.as_str().repeat(indent_level)
|
indent_style.as_str().repeat(indent_level)
|
||||||
}
|
}
|
||||||
|
|
|
@ -534,13 +534,24 @@ impl LanguageConfiguration {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_query(&self, kind: &str) -> Option<Query> {
|
fn load_query(&self, kind: &str) -> Option<Query> {
|
||||||
|
log::warn!("Loading tree sitter for {}", kind);
|
||||||
|
|
||||||
let query_text = read_query(&self.language_id, kind);
|
let query_text = read_query(&self.language_id, kind);
|
||||||
if query_text.is_empty() {
|
if query_text.is_empty() {
|
||||||
|
log::warn!("Query text is empty, returning early");
|
||||||
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let lang = self.highlight_config.get()?.as_ref()?.language;
|
let lang = self.highlight_config.get()?.as_ref()?.language;
|
||||||
Query::new(lang, &query_text)
|
Query::new(lang, &query_text)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
log::warn!(
|
||||||
|
"Failed to parse {} queries for {}: {}",
|
||||||
|
kind,
|
||||||
|
self.language_id,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
|
||||||
log::error!(
|
log::error!(
|
||||||
"Failed to parse {} queries for {}: {}",
|
"Failed to parse {} queries for {}: {}",
|
||||||
kind,
|
kind,
|
||||||
|
|
|
@ -584,5 +584,8 @@ fn mtime(path: &Path) -> Result<SystemTime> {
|
||||||
/// directory
|
/// directory
|
||||||
pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
||||||
let path = crate::runtime_file(&PathBuf::new().join("queries").join(language).join(filename));
|
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)
|
std::fs::read_to_string(path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
insert::{insert_char, insert_string},
|
insert::{insert_char, insert_string},
|
||||||
Context, MappableCommand, TYPABLE_COMMAND_LIST,
|
shell_impl, Context, MappableCommand, TYPABLE_COMMAND_LIST,
|
||||||
};
|
};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
|
@ -164,6 +164,9 @@ fn configure_background_thread() {
|
||||||
fn configure_engine() -> std::rc::Rc<std::cell::RefCell<steel::steel_vm::engine::Engine>> {
|
fn configure_engine() -> std::rc::Rc<std::cell::RefCell<steel::steel_vm::engine::Engine>> {
|
||||||
let mut engine = steel::steel_vm::engine::Engine::new();
|
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());
|
let mut module = BuiltInModule::new("helix/core/keybindings".to_string());
|
||||||
module.register_fn("set-keybindings!", SharedKeyBindingsEventQueue::merge);
|
module.register_fn("set-keybindings!", SharedKeyBindingsEventQueue::merge);
|
||||||
|
|
||||||
|
@ -224,6 +227,7 @@ fn configure_engine() -> std::rc::Rc<std::cell::RefCell<steel::steel_vm::engine:
|
||||||
module.register_fn("run-in-engine!", run_in_engine);
|
module.register_fn("run-in-engine!", run_in_engine);
|
||||||
module.register_fn("get-helix-scm-path", get_helix_scm_path);
|
module.register_fn("get-helix-scm-path", get_helix_scm_path);
|
||||||
module.register_fn("get-init-scm-path", get_init_scm_path);
|
module.register_fn("get-init-scm-path", get_init_scm_path);
|
||||||
|
module.register_fn("block-on-shell-command", run_shell_command_text);
|
||||||
|
|
||||||
engine.register_module(module);
|
engine.register_module(module);
|
||||||
|
|
||||||
|
@ -415,3 +419,20 @@ fn get_init_scm_path() -> String {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_shell_command_text(
|
||||||
|
cx: &mut Context,
|
||||||
|
args: &[Cow<str>],
|
||||||
|
_event: PromptEvent,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -24,8 +24,7 @@
|
||||||
run-highlight
|
run-highlight
|
||||||
make-minor-mode!
|
make-minor-mode!
|
||||||
git-status
|
git-status
|
||||||
reload-helix-scm
|
reload-helix-scm)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
;;@doc
|
;;@doc
|
||||||
|
|
|
@ -1705,7 +1705,7 @@ source = { git = "https://github.com/metio/tree-sitter-ssh-client-config", rev =
|
||||||
name = "scheme"
|
name = "scheme"
|
||||||
scope = "source.scheme"
|
scope = "source.scheme"
|
||||||
injection-regex = "scheme"
|
injection-regex = "scheme"
|
||||||
file-types = ["ss"] # "scm",
|
file-types = ["ss", "scm"] # "scm",
|
||||||
roots = []
|
roots = []
|
||||||
comment-token = ";"
|
comment-token = ";"
|
||||||
indent = { tab-width = 2, unit = " " }
|
indent = { tab-width = 2, unit = " " }
|
||||||
|
|
Loading…
Reference in New Issue