diff --git a/CHANGELOG.md b/CHANGELOG.md index a930e96..2a18bf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ ### Unreleased * Fixed formatter crash ([#15](https://github.com/rszyma/vscode-kanata/issues/15)) - +* Make formatter work within included files ([#14](https://github.com/rszyma/vscode-kanata/issues/14)) +* Enable formatter by default + ### 0.7.0 * Added experimental support for formatting `deflayer`s according to `defsrc` layout (disabled by default, can be enabled in settings) diff --git a/README.md b/README.md index acdd66f..84bc2a1 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ Also, if you work with multiple main files, and find yourself switching `mainCon there's a handy command palette entry: - `Kanata: Set current file as main` +### Formatter: auto-apply spacial layout of `defsrc` to all `deflayer`s + +This is enabled by default, because I've seen a lot of kanata configs, and it seems like +majority of users prefer to align their `deflayer`s according to spacial layout of `defsrc`. +If you have "Auto format on save enabled" and don't want this feature, you can disable +it in settings (search for "kanata.format"). + ## Contributing If you have an idea what could be improved, feel free to open an issue or a PR. diff --git a/kls/src/formatter/defsrc_layout/get_layout.rs b/kls/src/formatter/defsrc_layout/get_layout.rs new file mode 100644 index 0000000..d884280 --- /dev/null +++ b/kls/src/formatter/defsrc_layout/get_layout.rs @@ -0,0 +1,202 @@ +use super::{parse_into_ext_tree_and_root_span, ExtParseTree}; +use crate::{path_to_url, WorkspaceOptions}; +use anyhow::{anyhow, Ok}; +use lsp_types::{TextDocumentItem, Url}; +use std::{collections::BTreeMap, iter, path::PathBuf, str::FromStr}; + +pub fn get_defsrc_layout( + workspace_options: &WorkspaceOptions, + documents: &BTreeMap, + tab_size: u32, + file_uri: &Url, // of current file + tree: &ExtParseTree, // of current file +) -> anyhow::Result>>> { + match workspace_options { + WorkspaceOptions::Single { .. } => { + if tree.includes()?.is_empty() { + tree.defsrc_layout(tab_size) + } else { + // This is an error, because we don't know if those included files + // and current file collectively don't contain >=2 `defsrc` blocks. + // And if that's the case, we don't want to format `deflayers`. + Err(anyhow!("includes are not supported in Single mode")) + } + } + WorkspaceOptions::Workspace { + main_config_file, + root, + } => { + let main_config_file_path = PathBuf::from_str(main_config_file) + .map_err(|e| anyhow!("main_config_file is an invalid path: {}", e))?; + let main_config_file_url = path_to_url(&main_config_file_path, root) + .map_err(|e| anyhow!("failed to convert main_config_file_path to url: {}", e))?; + + // Check if currently opened file is the main file. + let main_tree: ExtParseTree = if main_config_file_url == *file_uri { + tree.clone() // TODO: prevent clone + } else { + // Currently opened file is non-main file, it's probably an included file. + let text = &documents + .get(&main_config_file_url) + .map(|doc| &doc.text) + .ok_or_else(|| { + anyhow!( + "included file is not present in the workspace: {}", + file_uri.to_string() + ) + })?; + + parse_into_ext_tree_and_root_span(text) + .map(|x| x.0) + .map_err(|e| anyhow!("parse_into_ext_tree_and_root_span failed: {}", e.msg))? + }; + + let includes = main_tree + .includes() + .map_err(|e| anyhow!("workspace [main = {main_config_file_url}]: {e}"))? + .iter() + .map(|path| path_to_url(path, root)) + .collect::>>() + .map_err(|e| anyhow!("path_to_url: {e}"))?; + + // make sure that all includes collectively contain only 1 defsrc + let mut defsrc_layout = None; + for file_url in includes.iter().chain(iter::once(&main_config_file_url)) { + let text = &documents + .get(file_url) + .expect("document should be cached") + .text; + + let tree = parse_into_ext_tree_and_root_span(text) + .map(|x| x.0) + .map_err(|e| { + anyhow!( + "parse_into_ext_tree_and_root_span failed for file '{file_uri}': {}", + e.msg + ) + })?; + + if let Some(layout) = tree + .defsrc_layout(tab_size) + .map_err(|e| anyhow!("tree.defsrc_layout for '{file_url}' failed: {e}"))? + { + defsrc_layout = Some(layout); + } + } + Ok(defsrc_layout) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MAIN_FILE: &str = "main.kbd"; + + fn new_btree(items: &[(&str, &str)]) -> BTreeMap { + let mut btree = BTreeMap::new(); + for item in items { + let uri = Url::from_str(&format!("file:///{}", item.0)).unwrap(); + let doc = TextDocumentItem { + uri: uri.clone(), + language_id: "kanata".to_string(), + version: 0, + text: item.1.to_string(), + }; + btree.insert(uri, doc); + } + btree + } + + #[test] + fn single_without_includes() { + let src = "(defsrc 1 2) (deflayer base 3 4)"; + let layout = get_defsrc_layout( + &WorkspaceOptions::Single { root: None }, + &BTreeMap::new(), + 4, + &Url::from_str(&format!("file:///{MAIN_FILE}")).unwrap(), + &parse_into_ext_tree_and_root_span(src).unwrap().0, + ) + .unwrap() + .ok_or("should be some") + .unwrap(); + + assert_eq!(layout, vec![vec![2], vec![1]]); + } + + #[test] + fn single_with_includes() { + let src = "(defsrc 1 2) (deflayer base 3 4) (include file.kbd)"; + + let _ = get_defsrc_layout( + &WorkspaceOptions::Single { root: None }, + &BTreeMap::new(), + 4, + &Url::from_str(&format!("file:///{MAIN_FILE}")).unwrap(), + &parse_into_ext_tree_and_root_span(src).unwrap().0, + ) + .expect_err("should be error, because includes don't work in Single mode"); + + let _ = get_defsrc_layout( + &WorkspaceOptions::Single { + root: Some(Url::from_str("file:///").unwrap()), + }, + &BTreeMap::new(), + 4, + &Url::from_str(&format!("file:///{MAIN_FILE}")).unwrap(), + &parse_into_ext_tree_and_root_span(src).unwrap().0, + ) + .expect_err( + "should be error, because includes don't work in Single mode,\ + even if opened in workspace", + ); + } + + #[test] + fn layout_of_main_file_in_workspace_with_included_defsrc() { + let items = &[ + (MAIN_FILE, "(deflayer base 3 4) (include included.kbd)"), + ("included.kbd", "(defsrc 1 2) (deflayer numbers 3 4)"), + ]; + let layout = get_defsrc_layout( + &WorkspaceOptions::Workspace { + main_config_file: MAIN_FILE.to_owned(), + root: Url::from_str("file:///").unwrap(), + }, + &new_btree(items), + 4, + &Url::from_str(&format!("file:///{MAIN_FILE}")).unwrap(), + &parse_into_ext_tree_and_root_span(items[0].1).unwrap().0, + ) + .unwrap() + .ok_or("should be some") + .unwrap(); + + assert_eq!(layout, vec![vec![3], vec![1]]); + } + + #[test] + fn layout_of_included_file_in_workspace_with_included_defsrc() { + let items = &[ + (MAIN_FILE, "(deflayer base 3 4) (include included.kbd)"), + ("included.kbd", "(defsrc 1 2) (deflayer numbers 3 4)"), + ]; + let layout = get_defsrc_layout( + &WorkspaceOptions::Workspace { + main_config_file: MAIN_FILE.to_owned(), + root: Url::from_str("file:///").unwrap(), + }, + &new_btree(items), + 4, + &Url::from_str(&format!("file:///{}", items[1].0)).unwrap(), + &parse_into_ext_tree_and_root_span(items[1].1).unwrap().0, + ) + .unwrap() + .ok_or("should be some") + .unwrap(); + + assert_eq!(layout, vec![vec![3], vec![1]]); + } +} diff --git a/kls/src/formatter/use_defsrc_layout_on_deflayers.rs b/kls/src/formatter/defsrc_layout/mod.rs similarity index 85% rename from kls/src/formatter/use_defsrc_layout_on_deflayers.rs rename to kls/src/formatter/defsrc_layout/mod.rs index c9062a0..c0dcbc3 100644 --- a/kls/src/formatter/use_defsrc_layout_on_deflayers.rs +++ b/kls/src/formatter/defsrc_layout/mod.rs @@ -1,12 +1,21 @@ use super::ext_tree::*; use crate::log; +use anyhow::{anyhow, Ok}; use unicode_segmentation::*; +pub mod get_layout; +pub use get_layout::*; + impl ExtParseTree { // TODO: maybe don't format if an atom in defsrc/deflayer is too large. + // TODO: respect `tab_size`. // TODO: respect `insert_spaces` formatter setting. - pub fn use_defsrc_layout_on_deflayers<'a>(&'a mut self, tab_size: u32, _insert_spaces: bool) { - let mut defsrc: Option<&'a NodeList> = None; + pub fn use_defsrc_layout_on_deflayers<'a>( + &'a mut self, + defsrc_layout: &[Vec], + _tab_size: u32, + _insert_spaces: bool, + ) { let mut deflayers: Vec<&'a mut NodeList> = vec![]; for top_level_item in self.0.iter_mut() { @@ -25,39 +34,95 @@ impl ExtParseTree { Expr::List(_) => continue, }; - match first_atom.as_str() { - "defsrc" => match defsrc { + if let "deflayer" = first_atom.as_str() { + deflayers.push(top_level_list); + } + } + + // Apply the `defsrc` layout to each `deflayer` block. + for deflayer in &mut deflayers.iter_mut() { + if deflayer.len() - 2 != defsrc_layout.len() { + let layer_name = deflayer + .get(1) + .map(|f| if let Expr::Atom(x) = &f.expr { x } else { "?" }) + .unwrap_or("?"); + log!( + "Formatting of '{}' deflayer skipped: item count doesn't match defsrc", + layer_name + ); + continue; + } + + let last_expr_index = deflayer.len() - 3; + for (i, deflayer_item) in deflayer.iter_mut().skip(2).enumerate() { + let expr_graphemes_count = deflayer_item.expr.to_string().graphemes(true).count(); + + let post_metadata: Vec<_> = deflayer_item.post_metadata.drain(..).collect(); + + let comments: Vec<_> = post_metadata + .iter() + .filter_map(|md| match md { + Metadata::Comment(x) => Some(x), + Metadata::Whitespace(_) => None, + }) + .collect(); + + let is_the_last_expr_in_deflayer = i == last_expr_index; + + let new_post_metadata = formatted_deflayer_node_metadata( + expr_graphemes_count, + &defsrc_layout[i], + &comments, + is_the_last_expr_in_deflayer, + // insert_spaces, + ); + deflayer_item.post_metadata = new_post_metadata; + } + } + } + + /// Obtains defsrc layout from a given [`ExtParseTree`]. + /// * It doesn't search includes. + /// * Returns `Err` if found more than 1 defsrc, or `defsrc` contains a list. + /// * Returns `Ok(None)` if found 0 defsrc blocks. + /// * Returns `Ok(Some)` otherwise. + pub fn defsrc_layout<'a>(&'a self, tab_size: u32) -> anyhow::Result>>> { + let mut defsrc: Option<&'a NodeList> = None; + + for top_level_item in self.0.iter() { + let top_level_list = match &top_level_item.expr { + Expr::Atom(_) => continue, + Expr::List(list) => list, + }; + + let first_item = match top_level_list.get(0) { + Some(x) => x, + None => continue, + }; + + let first_atom = match &first_item.expr { + Expr::Atom(x) => x, + Expr::List(_) => continue, + }; + + if let "defsrc" = first_atom.as_str() { + match defsrc { Some(_) => { - log!( - "Formatting `deflayer`s failed: config file \ - contains multiple `defsrc` definitions." - ); - return; + return Err(anyhow!("multiple `defsrc` definitions in a single file")); } None => { defsrc = Some(top_level_list); } - }, - "deflayer" => { - deflayers.push(top_level_list); } - "include" => { - // TODO: search defsrc in other files - // TODO: search defsrc in main file if the current one is included - } - _ => {} } } - let defsrc = if let Some(x) = &mut defsrc { - x - } else { - log!( - "Formatting `deflayer`s failed: `defsrc` not found in this file. \ - NOTE: includes (or the main file, if this file is non-main) haven't \ - been checked, because it's not implemented yet." - ); - return; + let defsrc = match defsrc { + Some(x) => x, + None => { + // defsrc not found in this file, but it may be in another. + return Ok(None); + } }; // Get number of atoms from `defsrc` now to prevent additional allocations @@ -70,11 +135,7 @@ impl ExtParseTree { // Read the layout from `defsrc` for (i, defsrc_item) in defsrc.iter().skip(1).enumerate() { if let Expr::List(_) = defsrc_item.expr { - log!( - "Formatting `deflayer`s failed: there shouldn't \ - be any lists in `defsrc`." - ); - return; + return Err(anyhow!("found a list in `defsrc`")); } let defsrc_item_as_str = defsrc_item.expr.to_string(); @@ -120,48 +181,7 @@ impl ExtParseTree { } // Layout no longer needs to be mutable. - let layout = layout; - - // Apply the `defsrc` layout to each `deflayer` block. - for deflayer in &mut deflayers.iter_mut() { - if deflayer.len() - 2 != defsrc_item_count { - let layer_name = deflayer - .get(1) - .map(|f| if let Expr::Atom(x) = &f.expr { x } else { "?" }) - .unwrap_or("?"); - log!( - "Formatting of '{}' deflayer skipped: item count doesn't match defsrc", - layer_name - ); - continue; - } - - let last_expr_index = deflayer.len() - 3; - for (i, deflayer_item) in deflayer.iter_mut().skip(2).enumerate() { - let expr_graphemes_count = deflayer_item.expr.to_string().graphemes(true).count(); - - let post_metadata: Vec<_> = deflayer_item.post_metadata.drain(..).collect(); - - let comments: Vec<_> = post_metadata - .iter() - .filter_map(|md| match md { - Metadata::Comment(x) => Some(x), - Metadata::Whitespace(_) => None, - }) - .collect(); - - let is_the_last_expr_in_deflayer = i == last_expr_index; - - let new_post_metadata = formatted_deflayer_node_metadata( - expr_graphemes_count, - &layout[i], - &comments, - is_the_last_expr_in_deflayer, - // insert_spaces, - ); - deflayer_item.post_metadata = new_post_metadata; - } - } + Ok(Some(layout)) } } @@ -280,7 +300,10 @@ mod tests { fn formats_correctly(input: &str, expected_output: &str) { let mut tree = parse_into_ext_tree(input).expect("parses"); - tree.use_defsrc_layout_on_deflayers(4, true); + let tab_size = 4; + if let Some(layout) = tree.defsrc_layout(tab_size).expect("no err") { + tree.use_defsrc_layout_on_deflayers(&layout, tab_size, true); + }; assert_eq!( tree.to_string(), expected_output, @@ -369,8 +392,13 @@ mod tests { } #[test] - fn invalid_item_in_defsrc() { - should_not_format("(defsrc () 1 2) (deflayer base 0 1 2)"); + fn defsrc_layout_when_invalid_list_item_in_defsrc() { + let input = "(defsrc () 1 2) (deflayer base 0 1 2)"; + let tree = parse_into_ext_tree(input).expect("parses"); + tree.defsrc_layout(4).expect_err( + "should error, because there's a list item in defsrc, \ + which is an error in kanata config", + ); } #[test] diff --git a/kls/src/formatter/ext_tree.rs b/kls/src/formatter/ext_tree.rs index 70b456d..e1835cf 100644 --- a/kls/src/formatter/ext_tree.rs +++ b/kls/src/formatter/ext_tree.rs @@ -1,12 +1,15 @@ +use anyhow::anyhow; use kanata_parser::cfg::{ sexpr::{self, Position, SExpr, SExprMetaData, Span, Spanned}, ParseError, }; -use std::fmt::{Debug, Display}; - -use crate::{helpers, log}; +use std::{ + fmt::{Debug, Display}, + path::PathBuf, + str::FromStr, +}; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct ExtParseTree(pub NodeList); impl ExtParseTree { @@ -56,32 +59,37 @@ impl ExtParseTree { } } } -} -impl ExtParseTree { - // If any step on path is not List, panic. - // If any step is out-of-bounds, return None. - // pub fn get_node(&self, at_path: &[usize]) -> Option<&ParseTreeNode> { - // if at_path.is_empty() { - // return None; - // } - // return match self.0.get(at_path[0]) { - // Some(x) => x, - // None => return None, - // } - // .get_node(&at_path[1..]); - // } - - // pub fn get_node_mut(&mut self, at_path: &[usize]) -> Option<&mut ParseTreeNode> { - // if at_path.is_empty() { - // return None; - // } - // return match self.0.get(at_path[0]) { - // Some(x) => x, - // None => return None, - // } - // .get_node_mut(&at_path[1..]); - // } + pub fn includes(&self) -> anyhow::Result> { + let mut result = vec![]; + for top_level_block in self.0.iter() { + if let Expr::List(NodeList::NonEmptyList(xs)) = &top_level_block.expr { + match &xs[0].expr { + Expr::Atom(x) => match x.as_str() { + "include" => {} + _ => continue, + }, + _ => continue, + }; + + if xs.len() != 2 { + return Err(anyhow!( + "an include block items: 2 != {}; block: \n{}", + xs.len(), + xs.iter().fold(String::new(), |mut acc, x| { + acc.push_str(&x.to_string()); + acc + }) + )); + } + + if let Expr::Atom(x) = &xs[1].expr { + result.push(PathBuf::from_str(x.as_str().trim_matches('\"'))?) + } + }; + } + Ok(result) + } } impl Display for ExtParseTree { @@ -133,7 +141,7 @@ impl From for Metadata { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum NodeList { NonEmptyList(Vec), EmptyList(Vec), @@ -230,7 +238,7 @@ impl Display for NodeList { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum Expr { Atom(String), List(NodeList), @@ -250,7 +258,7 @@ impl Display for Expr { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct ParseTreeNode { pub pre_metadata: Vec, pub expr: Expr, @@ -267,56 +275,6 @@ impl ParseTreeNode { } } -impl ParseTreeNode { - // If any step on path is not List, panic. - // If any step is out-of-bounds, return None. - // pub fn get_node(&self, at_path: &[usize]) -> Option<&ParseTreeNode> { - // let mut head: &ParseTreeNode = self; - // for i in at_path { - // if let ParseTreeNode::List(l) = head { - // head = match l.get(*i) { - // Some(x) => x, - // None => return None, - // }; - // } else { - // panic!("invalid tree path") - // } - // } - // Some(head) - // } - - // pub fn get_node_mut(&mut self, at_path: &[usize]) -> Option<&mut ParseTreeNode> { - // let mut head: &mut ParseTreeNode = self; - // for i in at_path { - // if let ParseTreeNode::List(l) = head { - // head = match l.get_mut(*i) { - // Some(x) => x, - // None => return None, - // }; - // } else { - // panic!("invalid tree path") - // } - // } - // Some(head) - // } - - // Panics if the variant is not List. - // pub fn unwrap_list(&self) -> &NodeList { - // match &self.expr { - // Expr::List(list) => list, - // _ => panic!("not a list"), - // } - // } - - // // Panics if the variant is not List. - // pub fn unwrap_list_mut(&mut self) -> &mut NodeList { - // match &mut self.expr { - // Expr::List(list) => list, - // _ => panic!("not a list"), - // } - // } -} - impl Display for ParseTreeNode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let ParseTreeNode { @@ -371,7 +329,6 @@ impl<'a> From> for Span { pub fn parse_into_ext_tree_and_root_span( src: &str, ) -> std::result::Result<(ExtParseTree, CustomSpan<'_>), ParseError> { - let start_time = helpers::now(); let filename = ""; let (exprs, exprs_ext) = sexpr::parse_(src, filename, false)?; let exprs: Vec = exprs.into_iter().map(SExpr::List).collect(); @@ -463,11 +420,6 @@ pub fn parse_into_ext_tree_and_root_span( tree.push_metadata(tree_depth, metadata.into()); } - log!( - "parse_into_ext_tree_and_root_span in {:.3?}", - helpers::now().duration_since(start_time) - ); - Ok((tree, root_span)) } @@ -680,4 +632,50 @@ mod tests { ); } } + + #[test] + fn test_ext_parse_tree_includes() { + assert_eq!( + parse_into_ext_tree("(include abc.kbd)") + .expect("parses") + .includes() + .unwrap(), + vec![PathBuf::from_str("abc.kbd").unwrap()] + ); + assert_eq!( + parse_into_ext_tree("(qwer abc.kbd)") + .expect("parses") + .includes() + .unwrap(), + Vec::::new() + ); + assert_eq!( + parse_into_ext_tree("(include abc.kbd)(include 123.kbd)") + .expect("parses") + .includes() + .unwrap(), + vec![ + PathBuf::from_str("abc.kbd").unwrap(), + PathBuf::from_str("123.kbd").unwrap(), + ] + ); + assert_eq!( + parse_into_ext_tree("(include \"my config.kbd\")(include \"included file 123.kbd\")") + .expect("parses") + .includes() + .unwrap(), + vec![ + PathBuf::from_str("my config.kbd").unwrap(), + PathBuf::from_str("included file 123.kbd").unwrap(), + ] + ); + } + + #[test] + fn test_ext_parse_tree_multiple_filenames() { + let r = parse_into_ext_tree("(include abc.kbd 123.kbd)") + .expect("parses") + .includes(); + assert!(r.is_err()); + } } diff --git a/kls/src/formatter/mod.rs b/kls/src/formatter/mod.rs index 6480ae7..99b12df 100644 --- a/kls/src/formatter/mod.rs +++ b/kls/src/formatter/mod.rs @@ -1,8 +1,8 @@ pub mod ext_tree; use ext_tree::*; +pub mod defsrc_layout; mod remove_excessive_newlines; -mod use_defsrc_layout_on_deflayers; pub struct Formatter { // Additional options @@ -21,16 +21,25 @@ impl Formatter { &self, tree: &mut ExtParseTree, options: &lsp_types::FormattingOptions, // todo: we should probably handle these options + defsrc_layout: Option<&[Vec]>, ) { if !self.options.enable { return; } + if self.remove_extra_empty_lines { tree.remove_excessive_adjacent_newlines(2); } if self.options.use_defsrc_layout_on_deflayers { - tree.use_defsrc_layout_on_deflayers(options.tab_size, options.insert_spaces) + if let Some(layout) = defsrc_layout { + tree.use_defsrc_layout_on_deflayers( + layout, + options.tab_size, + options.insert_spaces, + // is_main_config_file, + ) + } } } } diff --git a/kls/src/lib.rs b/kls/src/lib.rs index 48b15e9..a466b8b 100644 --- a/kls/src/lib.rs +++ b/kls/src/lib.rs @@ -10,8 +10,8 @@ use lsp_types::{ request::{Formatting, Request}, DeleteFilesParams, Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, - FileChangeType, FileDelete, FileEvent, InitializeParams, PublishDiagnosticsParams, - TextDocumentItem, TextEdit, Url, VersionedTextDocumentIdentifier, + DocumentFormattingParams, FileChangeType, FileDelete, FileEvent, InitializeParams, + PublishDiagnosticsParams, TextDocumentItem, TextEdit, Url, VersionedTextDocumentIdentifier, }; use serde::Deserialize; use serde_wasm_bindgen::{from_value, to_value}; @@ -106,12 +106,7 @@ impl Kanata { let mut loaded_files: HashSet = HashSet::default(); let mut get_file_content_fn_impl = |filepath: &Path| { - let file_url = if filepath.is_absolute() { - Url::from_str(format!("file://{}", filepath.to_string_lossy()).as_ref()) - .map_err(|_| INVALID_PATH_ERROR.to_string())? - } else { - Url::join(root_folder, &filepath.to_string_lossy()).map_err(|e| e.to_string())? - }; + let file_url = path_to_url(filepath, root_folder).map_err(|_| INVALID_PATH_ERROR)?; log!("searching URL across opened documents: {}", file_url); let doc = all_documents.get(&file_url).ok_or_else(|| { @@ -145,6 +140,16 @@ impl Kanata { } } +fn path_to_url(path: &Path, root_folder: &Url) -> anyhow::Result { + let file_url = if path.is_absolute() { + Url::from_str(format!("file://{}", path.to_string_lossy()).as_ref()) + .map_err(|_| anyhow!("invalid path"))? + } else { + Url::join(root_folder, &path.to_string_lossy())? + }; + Ok(file_url) +} + #[derive(Debug, Deserialize, Clone)] struct Config { #[serde(rename = "includesAndWorkspaces")] @@ -189,16 +194,23 @@ impl Display for DefLocalKeysVariant { #[derive(Debug, Clone)] enum WorkspaceOptions { - Single, - Workspace { main_config_file: String }, + /// `root` is `None` in Single mode when file is not opened in a workspace. + Single { + root: Option, + }, + Workspace { + main_config_file: String, + root: Url, + }, } -impl From for WorkspaceOptions { - fn from(value: Config) -> Self { - match value.includes_and_workspaces { - IncludesAndWorkspaces::Single => WorkspaceOptions::Single, +impl WorkspaceOptions { + fn from_config(config: Config, root_folder: Option) -> Self { + match config.includes_and_workspaces { + IncludesAndWorkspaces::Single => WorkspaceOptions::Single { root: root_folder }, IncludesAndWorkspaces::Workspace => WorkspaceOptions::Workspace { - main_config_file: value.main_config_file, + main_config_file: config.main_config_file, + root: root_folder.expect("root folder should be set in workspace mode"), }, } } @@ -217,7 +229,6 @@ pub struct KanataLanguageServer { documents: Documents, kanata: Kanata, workspace_options: WorkspaceOptions, - root: Option, send_diagnostics_callback: js_sys::Function, formatter: formatter::Formatter, } @@ -264,8 +275,7 @@ impl KanataLanguageServer { options: config.format, remove_extra_empty_lines: false, }, - workspace_options: config.into(), - root: root_uri, + workspace_options: WorkspaceOptions::from_config(config, root_uri), send_diagnostics_callback: send_diagnostics_callback.clone(), } @@ -390,46 +400,58 @@ impl KanataLanguageServer { #[allow(unused_variables)] #[wasm_bindgen(js_class = KanataLanguageServer, js_name = onDocumentFormatting)] pub fn on_document_formatting(&mut self, params: JsValue) -> JsValue { - if !self.formatter.options.enable { - log!("Formatting request received, but formatting is disabled in vscode-kanata settings."); - return to_value::(&Some(vec![])).expect("no err"); - } - type Params = ::Params; type Result = ::Result; + let params = from_value::(params).expect("deserializes"); + to_value::(&self.on_document_formatting_impl(¶ms)).expect("no conversion error") + } - let Params { - text_document, - options, // vscode formatting options - .. - } = from_value(params).expect("deserializes"); + /// Returns None on error. + fn on_document_formatting_impl( + &mut self, + params: &DocumentFormattingParams, + ) -> Option> { + if !self.formatter.options.enable { + log!("Formatting request received, but formatting is disabled in vscode-kanata settings."); + return Some(vec![]); + } let text = &self .documents - .get(&text_document.uri) + .get(¶ms.text_document.uri) .expect("document should be cached") .text; let (mut tree, root_span) = match formatter::ext_tree::parse_into_ext_tree_and_root_span(text) { Ok(x) => x, - Err(e) => { - log!("failed to parse into tree"); - return to_value::(&None).expect("serializes"); + Err(_) => { + log!("failed to parse current file into tree"); + return None; } }; let range = lsp_range_from_span(&root_span.into()); - let start = helpers::now(); - self.formatter.format(&mut tree, &options); - log!("format in {:.3?}", helpers::now().duration_since(start)); + let defsrc_layout = formatter::defsrc_layout::get_defsrc_layout( + &self.workspace_options, + &self.documents, + params.options.tab_size, + ¶ms.text_document.uri, + &tree, + ) + .map_err(|e| { + log!("format: get_defsrc_layout error: {}", e); + }) + .unwrap_or(None); + + self.formatter + .format(&mut tree, ¶ms.options, defsrc_layout.as_deref()); - to_value::(&Some(vec![TextEdit { + Some(vec![TextEdit { range, new_text: tree.to_string(), - }])) - .expect("no err") + }]) } } @@ -504,12 +526,13 @@ impl KanataLanguageServer { &self, err: &CustomParseError, ) -> anyhow::Result> { - let url: Url = match &self.root { - Some(root) => { + let url: Url = match &self.workspace_options { + WorkspaceOptions::Workspace { root, .. } + | WorkspaceOptions::Single { root: Some(root) } => { let filename = err.span.file_name(); Url::join(root, &filename).map_err(|e| anyhow!(e.to_string()))? } - None => match &self.documents.first_key_value() { + WorkspaceOptions::Single { root: None } => match &self.documents.first_key_value() { Some(entry) => entry.0.to_owned(), None => bail!("no kanata files are opened"), }, @@ -579,17 +602,13 @@ impl KanataLanguageServer { (doc, diagnostics) } - fn parse_workspace(&self, main_config_file: &str) -> Vec { + fn parse_workspace(&self, main_config_file: &str, root: &Url) -> Vec { log!("parse_workspace for main_config_file={}", main_config_file); let pb = PathBuf::from(main_config_file); let main_cfg_file = pb.as_path(); self.kanata - .parse_workspace( - &self.root.clone().expect("should be set in workspace mode"), - main_cfg_file, - &self.documents, - ) + .parse_workspace(root, main_cfg_file, &self.documents) .map(|_| None) .unwrap_or_else(Some) .into_iter() @@ -601,7 +620,10 @@ impl KanataLanguageServer { let main_cfg_filename: PathBuf = path::PathBuf::from_str(url_path_str) .expect("shoudn't error because it comes from Url"); let main_cfg_text: &str = &doc.text; - let is_opened_in_workspace: bool = self.root.is_some(); + let is_opened_in_workspace: bool = match &self.workspace_options { + WorkspaceOptions::Workspace { .. } => true, + WorkspaceOptions::Single { root } => root.is_some(), + }; self.kanata .parse_single_file(&main_cfg_filename, main_cfg_text, is_opened_in_workspace) .map(|_| None) @@ -636,16 +658,17 @@ impl KanataLanguageServer { let docs: Vec<_> = docs.iter().collect(); let parse_errors = match &self.workspace_options { - WorkspaceOptions::Single => { + WorkspaceOptions::Single { .. } => { let results: Vec<_> = docs .iter() .filter_map(|doc| self.parse_a_single_file_in_workspace(doc)) .collect::>(); results } - WorkspaceOptions::Workspace { main_config_file } => { - self.parse_workspace(main_config_file) - } + WorkspaceOptions::Workspace { + main_config_file, + root, + } => self.parse_workspace(main_config_file, root), }; let new_diags = parse_errors diff --git a/package.json b/package.json index c252175..0085c88 100644 --- a/package.json +++ b/package.json @@ -103,13 +103,13 @@ }, "vscode-kanata.format.enable": { "type": "boolean", - "default": false, - "description": "[experimental] Enable formatting" + "default": true, + "description": "Enable formatting" }, "vscode-kanata.format.useDefsrcLayoutOnDeflayers": { "type": "boolean", - "default": false, - "markdownDescription": "[experimental] Formatting applies spacial layout of `defsrc` to all `deflayer`s." + "default": true, + "markdownDescription": "Formatting applies spacial layout of `defsrc` to all `deflayer`s." } } }