diff --git a/kls/src/formatter/defsrc_layout/get_keys.rs b/kls/src/formatter/defsrc_layout/get_keys.rs new file mode 100644 index 0000000..77a8c85 --- /dev/null +++ b/kls/src/formatter/defsrc_layout/get_keys.rs @@ -0,0 +1,87 @@ +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_keys( + workspace_options: &WorkspaceOptions, + documents: &BTreeMap, + 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_keys() + } 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_keys = None; + for file_url in includes.iter().chain(iter::once(&main_config_file_url)) { + let doc = documents + .get(file_url) + .ok_or_else(|| anyhow!("document '{file_url}' is not loaded"))?; + + let tree = parse_into_ext_tree_and_root_span(&doc.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_keys() + .map_err(|e| anyhow!("tree.defsrc_keys for '{file_url}' failed: {e}"))? + { + defsrc_keys = Some(layout); + } + } + Ok(defsrc_keys) + } + } +} diff --git a/kls/src/formatter/defsrc_layout/mod.rs b/kls/src/formatter/defsrc_layout/mod.rs index 2a03257..7200e19 100644 --- a/kls/src/formatter/defsrc_layout/mod.rs +++ b/kls/src/formatter/defsrc_layout/mod.rs @@ -5,6 +5,8 @@ use crate::log; use anyhow::anyhow; use unicode_segmentation::*; +pub mod get_keys; +pub use get_keys::*; pub mod get_layout; pub use get_layout::*; @@ -193,6 +195,64 @@ impl ExtParseTree { // Layout no longer needs to be mutable. Ok(Some(layout)) } + + /// Obtains list of keys in defsrc block in 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_keys<'a>(&'a self) -> 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(_) => { + return Err(anyhow!("multiple `defsrc` definitions in a single file")); + } + None => { + defsrc = Some(top_level_list); + } + } + } + } + + let defsrc = match defsrc { + Some(x) => x, + None => { + // defsrc not found in this file, but it may be in another. + return Ok(None); + } + }; + + let result: Vec = defsrc + .iter() + .skip(1) + .map(|x| { + Ok(match &x.expr { + Expr::List(_) => return Err(anyhow!("found a list in `defsrc`")), + Expr::Atom(x) => x.clone(), + }) + }) + .collect::, _>>()?; + + Ok(Some(result)) + } } /// Format metadata for a definition layer node based on specified constraints. diff --git a/kls/src/formatter/ext_tree.rs b/kls/src/formatter/ext_tree.rs index e1835cf..91faffc 100644 --- a/kls/src/formatter/ext_tree.rs +++ b/kls/src/formatter/ext_tree.rs @@ -1,14 +1,16 @@ use anyhow::anyhow; +use itertools::Itertools; use kanata_parser::cfg::{ sexpr::{self, Position, SExpr, SExprMetaData, Span, Spanned}, ParseError, }; -use std::{ - fmt::{Debug, Display}, - path::PathBuf, - str::FromStr, -}; +use std::{fmt::Display, path::PathBuf, str::FromStr}; +/// ExtParseTree exists to allow efficient modification of nodes, with intention of combining back +/// later to the original source form, easily done by just calling .string() on it. +/// +/// One downside of this form, is that nodes don't hold span/position info. So random access +/// by position will be at best O(n). #[derive(Debug, PartialEq, Eq, Clone)] pub struct ExtParseTree(pub NodeList); @@ -60,6 +62,31 @@ impl ExtParseTree { } } + pub fn _get_node_by_path(&self, path: &[usize]) -> anyhow::Result<&Expr> { + let mut head: &NodeList = &self.0; + let last_path_index = path.len() - 1; + for (path_index, &i) in path.iter().enumerate() { + let node = match head.get(i) { + Some(x) => x, + None => panic!("path out-of-bounds on path index {path_index} "), + }; + if path_index == last_path_index { + return Ok(&node.expr); + } + match &node.expr { + Expr::Atom(_) => { + return Err(anyhow!( + "atom found in the middle of path, while it's only allowed at the end" + )); + } + Expr::List(xs) => { + head = xs; + } + } + } + unreachable!() + } + pub fn includes(&self) -> anyhow::Result> { let mut result = vec![]; for top_level_block in self.0.iter() { @@ -90,6 +117,13 @@ impl ExtParseTree { } Ok(result) } + + pub fn path_to_node_by_lsp_position( + &self, + pos: lsp_types::Position, + ) -> anyhow::Result> { + self.0.path_to_node_by_lsp_pos(pos, &mut 0, &mut 0) + } } impl Display for ExtParseTree { @@ -212,6 +246,74 @@ impl NodeList { NodeList::EmptyList(_) => [].iter_mut(), // Return an empty mutable iterator for EmptyList } } + + fn path_to_node_by_lsp_pos( + &self, + pos: lsp_types::Position, + line: &mut u32, + chars_since_newline: &mut u32, + ) -> Result, anyhow::Error> { + for (i, current_node) in self.iter().enumerate() { + let ParseTreeNode { + pre_metadata, + expr, + post_metadata, + } = current_node; + + for m in pre_metadata { + *line += m.to_string().encode_utf16().fold(0, |acc, n| { + if n == b'\n' as u16 { + *chars_since_newline = 0; + acc + 1 + } else { + *chars_since_newline += 1; + acc + } + }); + } + if *line > pos.line { + return Err(anyhow!("position is inside metadata (1)")); + } + + match expr { + Expr::Atom(atom) => { + let len: u32 = atom.encode_utf16().collect_vec().len() as u32; + if *line == pos.line { + let at_least_lower_bound = pos.character >= *chars_since_newline; + let below_upper_bound = pos.character < *chars_since_newline + len; + if at_least_lower_bound && below_upper_bound { + return Ok(vec![i as u32]); + } + } + *chars_since_newline += len; + } + Expr::List(xs) => { + *chars_since_newline += 1; // account for '(' + if let Ok(mut v) = xs.path_to_node_by_lsp_pos(pos, line, chars_since_newline) { + v.insert(0, i as u32); + return Ok(v); + } + *chars_since_newline += 1; // account for ')' + } + }; + + for m in post_metadata { + *line += m.to_string().encode_utf16().fold(0, |acc, n| { + if n == b'\n' as u16 { + *chars_since_newline = 0; + acc + 1 + } else { + *chars_since_newline += 1; + acc + } + }); + } + if *line > pos.line { + return Err(anyhow!("position is inside metadata (2)")); + } + } + Err(anyhow!("no match in this path")) + } } impl Default for NodeList { @@ -297,8 +399,7 @@ impl Display for ParseTreeNode { } } -/// Parses config from text and combines both [`SExpr`] and [`SExprMetaData`] into [`ExtParseTree`]. -#[allow(unused)] +#[cfg(test)] pub fn parse_into_ext_tree(src: &str) -> std::result::Result { parse_into_ext_tree_and_root_span(src).map(|(x1, _)| x1) } @@ -310,8 +411,7 @@ pub fn parse_into_ext_tree(src: &str) -> std::result::Result { pub start: Position, pub end: Position, - pub file_name: String, - pub file_content: &'a str, + pub file_content: &'a str, // used for utf8 <-> utf16 conversions } impl<'a> From> for Span { @@ -319,18 +419,18 @@ impl<'a> From> for Span { Span { start: val.start, end: val.end, - file_name: val.file_name.into(), + file_name: "".into(), file_content: val.file_content.into(), } } } -/// Parses config from text and combines both [`SExpr`] and [`SExprMetaData`] into [`ExtParseTree`]. +/// Parses config from text, combining both [`SExpr`] and [`SExprMetaData`] into [`ExtParseTree`]. +/// The result can be loselessly combined back into the original form. pub fn parse_into_ext_tree_and_root_span( src: &str, ) -> std::result::Result<(ExtParseTree, CustomSpan<'_>), ParseError> { - let filename = ""; - let (exprs, exprs_ext) = sexpr::parse_(src, filename, false)?; + let (exprs, exprs_ext) = sexpr::parse_(src, "", false)?; let exprs: Vec = exprs.into_iter().map(SExpr::List).collect(); let exprs_len = exprs.len(); let last_sexpr_end = exprs.last().map(|x| x.span().end).unwrap_or_default(); @@ -344,7 +444,6 @@ pub fn parse_into_ext_tree_and_root_span( last_metadata_end } }, - file_name: filename.to_string(), file_content: src, }; let exprs = { @@ -446,6 +545,8 @@ impl<'a> SExprCustom { #[cfg(test)] mod tests { + use std::vec; + use super::*; use crate::log; @@ -678,4 +779,50 @@ mod tests { .includes(); assert!(r.is_err()); } + + #[test] + fn test_path_to_node_by_lsp_position_oneline() { + let test_table = [ + (0, 0, false, vec![]), // "(" + (0, 1, false, vec![]), // "(" + (0, 2, true, vec![0, 0, 0]), // "1" + (0, 3, false, vec![]), // " " + (0, 4, true, vec![0, 0, 1]), // "2" + (0, 5, false, vec![]), // ")" + (0, 6, false, vec![]), // " " + (0, 7, true, vec![0, 1]), // "3" + (0, 8, true, vec![0, 1]), // "4" + (0, 9, true, vec![0, 1]), // "5" + (0, 10, false, vec![]), // " " + (0, 11, false, vec![]), // " " + (0, 12, true, vec![0, 2]), // "6" + (0, 13, false, vec![]), // ")" + (0, 14, false, vec![]), // eof + ]; + + for ref test @ (line, char, expect_ok, ref expected_arr) in test_table { + dbg!(&test); + let pos: lsp_types::Position = lsp_types::Position::new(line, char); + let r = parse_into_ext_tree("((1 2) 345 6)") + .expect("should parse") + .path_to_node_by_lsp_position(pos); + + if expect_ok { + let r = r.expect("finds path"); + assert_eq!(r, *expected_arr); + } else { + r.expect_err("should error, because it's out of node bounds but it returned Ok"); + } + } + } + + #[test] + fn test_path_to_node_by_lsp_position_multiline() { + let pos: lsp_types::Position = lsp_types::Position::new(4, 3); + let r = parse_into_ext_tree("\n(1\n) \n ;; comment \n\t (2) ") + .expect("parses") + .path_to_node_by_lsp_position(pos) + .expect("finds path"); + assert_eq!(r, vec![1, 0]); + } } diff --git a/kls/src/helpers.rs b/kls/src/helpers.rs index 725eb0a..5442848 100644 --- a/kls/src/helpers.rs +++ b/kls/src/helpers.rs @@ -105,7 +105,10 @@ pub struct LocationInfo { pub struct DefinitionLocations(pub kanata_parser::lsp_hints::DefinitionLocations); impl DefinitionLocations { - pub fn search_references_at_position(&self, pos: &lsp_types::Position) -> Option { + pub fn search_references_for_token_at_position( + &self, + pos: &lsp_types::Position, + ) -> Option { log!("looking for references @ {:?}", pos); for ((name, span), ref_kind) in chain!( zip(&self.0.alias, repeat(ReferenceKind::Alias)), @@ -136,11 +139,11 @@ impl DefinitionLocations { pub struct ReferenceLocations(pub kanata_parser::lsp_hints::ReferenceLocations); impl ReferenceLocations { - pub fn search_definitions_at_position( + pub fn definition_for_reference_at_position( &self, pos: &lsp_types::Position, ) -> Option { - log!("looking for definitions @ {:?}", pos); + log!("looking for definition of token @ {:?}", pos); for ((name, spans), ref_kind) in chain!( zip(&self.0.alias.0, repeat(ReferenceKind::Alias)), zip(&self.0.variable.0, repeat(ReferenceKind::Variable)), @@ -194,12 +197,16 @@ pub fn lsp_range_from_span(span: &Span) -> lsp_types::Range { } } -#[derive(Default)] -pub struct KlsParserOutput { - pub errors: Vec, - pub inactive_codes: Vec, - pub definition_locations: DefinitionLocations, - pub reference_locations: ReferenceLocations, +#[allow(clippy::large_enum_variant)] // not created that often +pub enum KlsParserOutput { + Ok { + inactive_codes: Vec, + definition_locations: DefinitionLocations, + reference_locations: ReferenceLocations, + }, + Err { + errors: Vec, + }, } pub fn parse_wrapper( @@ -209,9 +216,8 @@ pub fn parse_wrapper( def_local_keys_variant_to_apply: &str, env_vars: &Vec<(String, String)>, ) -> KlsParserOutput { - let mut result = KlsParserOutput::default(); let parsed_state = &mut kanata_parser::cfg::ParserState::default(); - let _ = kanata_parser::cfg::parse_cfg_raw_string( + let result: anyhow::Result = kanata_parser::cfg::parse_cfg_raw_string( main_cfg_text, parsed_state, main_cfg_path, @@ -224,27 +230,29 @@ pub fn parse_wrapper( "parsed file `{}` without errors", main_cfg_path.to_string_lossy(), ); - result - .inactive_codes - .extend(parsed_state.lsp_hints.borrow().inactive_code.clone()); - result.definition_locations = - DefinitionLocations(parsed_state.lsp_hints.borrow().definition_locations.clone()); - result.reference_locations = - ReferenceLocations(parsed_state.lsp_hints.borrow().reference_locations.clone()); + KlsParserOutput::Ok { + inactive_codes: parsed_state.lsp_hints.borrow().inactive_code.clone(), + definition_locations: DefinitionLocations( + parsed_state.lsp_hints.borrow().definition_locations.clone(), + ), + reference_locations: ReferenceLocations( + parsed_state.lsp_hints.borrow().reference_locations.clone(), + ), + } }) - .map_err(|e: ParseError| { + .or_else(|e: ParseError| { let e = CustomParseError::from_parse_error( e, main_cfg_path.to_string_lossy().to_string().as_str(), ); - result.errors.push(e.clone()); log!( "parsing file `{}` resulted in error: `{}`", e.span.clone().file_name(), e.msg, ); + Ok(KlsParserOutput::Err { errors: vec![e] }) }); - result + result.expect("no err") } pub fn path_to_url(path: &Path, root_folder: &Url) -> anyhow::Result { diff --git a/kls/src/lib.rs b/kls/src/lib.rs index 614b202..9b26e22 100644 --- a/kls/src/lib.rs +++ b/kls/src/lib.rs @@ -8,7 +8,10 @@ use crate::{ helpers::{lsp_range_from_span, path_to_url, HashSet}, }; use anyhow::{anyhow, bail}; -use formatter::Formatter; +use formatter::{ + ext_tree::{Expr, ParseTreeNode}, + Formatter, +}; use kanata_parser::{ cfg::{sexpr::Span, FileContentProvider, ParseError}, lsp_hints::InactiveCode, @@ -18,14 +21,15 @@ use lsp_types::{ DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, DidDeleteFiles, DidOpenTextDocument, DidSaveTextDocument, Initialized, Notification, }, - request::{Formatting, GotoDefinition, Initialize, Request}, + request::{Formatting, GotoDefinition, HoverRequest, Initialize, Request}, DeleteFilesParams, Diagnostic, DiagnosticSeverity, DiagnosticTag, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, FileChangeType, FileDelete, FileEvent, FileOperationFilter, - FileOperationPattern, GotoDefinitionParams, GotoDefinitionResponse, InitializeParams, - InitializeResult, LocationLink, Position, PositionEncodingKind, PublishDiagnosticsParams, - SemanticTokenModifier, SemanticTokenType, SemanticTokensLegend, TextDocumentItem, - TextDocumentSyncKind, TextEdit, Url, VersionedTextDocumentIdentifier, + FileOperationPattern, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, + HoverParams, InitializeParams, InitializeResult, LanguageString, LocationLink, MarkedString, + Position, PositionEncodingKind, PublishDiagnosticsParams, SemanticTokenModifier, + SemanticTokenType, SemanticTokensLegend, TextDocumentItem, TextDocumentSyncKind, TextEdit, Url, + VersionedTextDocumentIdentifier, }; use serde::Deserialize; use serde_wasm_bindgen::{from_value, to_value}; @@ -34,8 +38,10 @@ use std::{ fmt::Display, path::{self, Path, PathBuf}, str::{FromStr, Split}, + vec, }; use wasm_bindgen::prelude::*; +use web_sys::console::log; mod helpers; use helpers::{ @@ -158,9 +164,8 @@ impl Kanata { let text = match text_or_not { Ok(text) => text, Err(err) => { - return KlsParserOutput { + return KlsParserOutput::Err { errors: vec![err.clone()], - ..Default::default() } } }; @@ -390,6 +395,7 @@ impl KanataLanguageServer { )), definition_provider: Some(lsp_types::OneOf::Left(true)), document_formatting_provider: Some(lsp_types::OneOf::Left(true)), + hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)), workspace: Some(lsp_types::WorkspaceServerCapabilities { workspace_folders: Some(lsp_types::WorkspaceFoldersServerCapabilities { supported: Some(false), @@ -429,13 +435,11 @@ impl KanataLanguageServer { Initialized::METHOD => (), DidOpenTextDocument::METHOD => { let DidOpenTextDocumentParams { text_document } = from_value(params).unwrap(); - log!("opening: {}", text_document.uri); if self.upsert_document(text_document).is_some() { log!("reopened tracked doc"); } - - let (diagnostics, _, _) = self.parse(); + let KlsParsedWorkspace { diagnostics, .. } = self.parse(); self.send_diagnostics(&diagnostics); } // We don't care when a document is closed -- we care about all Kanata files in a @@ -510,7 +514,7 @@ impl KanataLanguageServer { log!("detected file deletion: {}", uri); let removed_docs = self.remove_tracked_documents_in_dir(&uri); if !removed_docs.is_empty() { - let (diagnostics, _, _) = self.parse(); + let KlsParsedWorkspace { diagnostics, .. } = self.parse(); self.send_diagnostics(&diagnostics); } } @@ -518,7 +522,7 @@ impl KanataLanguageServer { DidSaveTextDocument::METHOD => { let _params: DidSaveTextDocumentParams = from_value(params).unwrap(); - let (diagnostics, _, _) = self.parse(); + let KlsParsedWorkspace { diagnostics, .. } = self.parse(); self.send_diagnostics(&diagnostics); } @@ -607,13 +611,19 @@ impl KanataLanguageServer { params: &GotoDefinitionParams, ) -> Option { log!("========= on_go_to_definition_impl ========"); - let (_, definition_locations_per_doc, reference_locations_per_doc) = self.parse(); + + let KlsParsedWorkspace { + def_locs: definition_locations_per_doc, + ref_locs: reference_locations_per_doc, + .. + } = self.parse(); + let source_doc_uri = ¶ms.text_document_position_params.text_document.uri; let match_all_defs = match self.workspace_options { WorkspaceOptions::Single { .. } => false, WorkspaceOptions::Workspace { .. } => true, }; - let definition_link = match navigation::definition_location( + let definition_link = match navigation::goto_definition_for_token_at_pos( ¶ms.text_document_position_params.position, source_doc_uri, &definition_locations_per_doc, @@ -663,7 +673,7 @@ impl KanataLanguageServer { WorkspaceOptions::Single { .. } => false, WorkspaceOptions::Workspace { .. } => true, }; - let references = navigation::references( + let references = navigation::references_for_definition_at_pos( position, source_doc_uri, definition_locations_by_doc, @@ -695,6 +705,146 @@ impl KanataLanguageServer { Some(acc) }) } + + #[allow(unused_variables)] + #[wasm_bindgen(js_class = KanataLanguageServer, js_name = onHover)] + pub fn on_hover(&mut self, params: JsValue) -> JsValue { + type Params = ::Params; + type Result = ::Result; + let params = from_value::(params).expect("deserializes"); + let result = self.on_hover_impl(¶ms); + to_value::(&result).expect("no conversion error") + } + + fn on_hover_impl(&mut self, params: &HoverParams) -> Option { + let doc_uri = ¶ms.text_document_position_params.text_document.uri; + let pos = params.text_document_position_params.position; + + let src = &self + .documents + .get(doc_uri) + .expect("document should be cached") + .text; + + // TODO: cache tree? + let (tree, _) = match formatter::ext_tree::parse_into_ext_tree_and_root_span(src) { + Ok(x) => x, + Err(_) => { + log!("hover: failed to parse current file into tree"); + return None; + } + }; + + // Get list of keys in defsrc. + + let defsrc_keys = formatter::defsrc_layout::get_defsrc_keys( + &self.workspace_options, + &self.documents, + doc_uri, + &tree, + ); + let defsrc_keys = match defsrc_keys { + Ok(x) => x, + Err(e) => { + log!("hover: get_defsrc_keys: {}", e); + return None; + } + }; + let defsrc_keys = match defsrc_keys { + Some(x) => x, + None => { + log!("hover: get_defsrc_keys: defsrc not found (?)"); + return None; + } + }; + + let path_to_node = match tree.path_to_node_by_lsp_position(pos) { + Ok(x) => x, + Err(err) => { + log!("hover: tree.path_to_node_by_lsp_position: {:?}", err); + return None; + } + }; + + // Check if the top-level item is valid for showing hover hints + // (we only want to show hovers for deflayer keys (for now)). + match path_to_node.first() { + Some(toplevel_deflayer_index) => { + let node = tree.0.get(*toplevel_deflayer_index as usize); + let node = match node { + Some(x) => x, + None => { + log!( + "hover: toplevel item with index {} doesn't exist (bug?)", + toplevel_deflayer_index + ); + return None; + } + }; + match &node.expr { + Expr::Atom(_) => return None, + Expr::List(node_list) => { + if let Some(&ParseTreeNode { + expr: Expr::Atom(ref atom), + .. + }) = node_list.get(0) + { + if atom != "deflayer" { + return None; + } + } else { + return None; + } + + // Only give hover hints if deflayer has same number of items as defsrc. + if node_list.len() - 2 != defsrc_keys.len() { + log!( + "hover: skipping key hints: deflayer vs defsrc item count mismatch" + ); + return None; + } + } + } + } + None => { + log!("hover: path_to_node is empty (this is a bug!)"); + return None; + } + } + + let key_index_in_deflayer = match path_to_node.last() { + Some(x) => { + if *x < 2 { + // hovering over "deflayer" keyword or layer name + return None; + } + (x - 2) as usize + } + None => { + log!("hover: get_defsrc_keys: last node not found (???)"); + return None; + } + }; + + let text_to_display: String = match defsrc_keys.get(key_index_in_deflayer) { + Some(x) => x.clone(), + None => { + log!( + "hover: defsrc key with such index not found: {}", + key_index_in_deflayer + ); + return None; + } + }; + + Some(Hover { + contents: HoverContents::Scalar(MarkedString::LanguageString(LanguageString { + language: "kanata".to_owned(), + value: format!("{text_to_display} ;; on defsrc"), + })), + range: None, + }) + } } /// Individual LSP notification handlers. @@ -908,13 +1058,7 @@ impl KanataLanguageServer { .collect() } - fn parse( - &self, - ) -> ( - Diagnostics, - HashMap, - HashMap, - ) { + fn parse(&self) -> KlsParsedWorkspace { let docs = self .documents .values() @@ -936,48 +1080,52 @@ impl KanataLanguageServer { let mut references: HashMap = Default::default(); for doc in docs { - let KlsParserOutput { - errors, - inactive_codes, - definition_locations, - reference_locations, - } = self.parse_a_single_file_in_workspace(doc); - errs.extend(errors); - inactives.extend(inactive_codes); - definitions.insert(doc.uri.clone(), definition_locations); - references.insert(doc.uri.clone(), reference_locations); + match self.parse_a_single_file_in_workspace(doc) { + KlsParserOutput::Ok { + inactive_codes, + definition_locations, + reference_locations, + } => { + inactives.extend(inactive_codes); + definitions.insert(doc.uri.clone(), definition_locations); + references.insert(doc.uri.clone(), reference_locations); + } + KlsParserOutput::Err { errors } => { + errs.extend(errors); + } + } } (errs, inactives, definitions, references) } WorkspaceOptions::Workspace { main_config_file, root, - } => { - let KlsParserOutput { - errors, + } => match self.parse_workspace(main_config_file, root) { + KlsParserOutput::Ok { inactive_codes, definition_locations, reference_locations, - } = self.parse_workspace(main_config_file, root); - - let mut definitions: HashMap = Default::default(); - let mut references: HashMap = Default::default(); - - url_map_definitions!(alias, root, definitions, definition_locations.0); - url_map_definitions!(variable, root, definitions, definition_locations.0); - url_map_definitions!(virtual_key, root, definitions, definition_locations.0); - url_map_definitions!(layer, root, definitions, definition_locations.0); - url_map_definitions!(template, root, definitions, definition_locations.0); - - url_map_references!(alias, root, references, reference_locations.0); - url_map_references!(variable, root, references, reference_locations.0); - url_map_references!(virtual_key, root, references, reference_locations.0); - url_map_references!(layer, root, references, reference_locations.0); - url_map_references!(template, root, references, reference_locations.0); - url_map_references!(include, root, references, reference_locations.0); - - (errors, inactive_codes, definitions, references) - } + } => { + let mut definitions: HashMap = Default::default(); + let mut references: HashMap = Default::default(); + + url_map_definitions!(alias, root, definitions, definition_locations.0); + url_map_definitions!(variable, root, definitions, definition_locations.0); + url_map_definitions!(virtual_key, root, definitions, definition_locations.0); + url_map_definitions!(layer, root, definitions, definition_locations.0); + url_map_definitions!(template, root, definitions, definition_locations.0); + + url_map_references!(alias, root, references, reference_locations.0); + url_map_references!(variable, root, references, reference_locations.0); + url_map_references!(virtual_key, root, references, reference_locations.0); + url_map_references!(layer, root, references, reference_locations.0); + url_map_references!(template, root, references, reference_locations.0); + url_map_references!(include, root, references, reference_locations.0); + + (vec![], inactive_codes, definitions, references) + } + KlsParserOutput::Err { errors } => (errors, vec![], HashMap::new(), HashMap::new()), + }, }; let new_error_diags = parse_errors @@ -1044,6 +1192,17 @@ impl KanataLanguageServer { if self.dim_inactive_config_items { diagnostics.extend(new_inactive_codes_diags); } - (diagnostics, identifiers, references) + + KlsParsedWorkspace { + diagnostics, + def_locs: identifiers, + ref_locs: references, + } } } + +struct KlsParsedWorkspace { + diagnostics: Diagnostics, + def_locs: HashMap, + ref_locs: HashMap, +} diff --git a/kls/src/navigation/mod.rs b/kls/src/navigation/mod.rs index c9703f8..e80790b 100644 --- a/kls/src/navigation/mod.rs +++ b/kls/src/navigation/mod.rs @@ -14,53 +14,53 @@ pub struct GotoDefinitionLink { pub target_filename: String, } -pub fn definition_location( +pub fn goto_definition_for_token_at_pos( pos: &Position, source_doc: &Url, definition_locations_by_doc: &HashMap, reference_locations_by_doc: &HashMap, - match_all_defs: bool, + search_all_docs: bool, ) -> Option { let source_doc_reference_locations = reference_locations_by_doc.get(source_doc)?; - let location_info = match source_doc_reference_locations.search_definitions_at_position(pos) { - Some(x) => x, - None => return None, - }; - log!("{:?}", &location_info); + let definition_loc = + source_doc_reference_locations.definition_for_reference_at_position(pos)?; + log!("{:?}", &definition_loc); - let mut map: HashMap = HashMap::new(); - let defs_iter: std::collections::hash_map::Iter = if match_all_defs { - definition_locations_by_doc.iter() - } else { - let item: DefinitionLocations = - definition_locations_by_doc.get(source_doc).unwrap().clone(); - map.insert(source_doc.clone(), item); - map.iter() - }; - for (_, definition_locations) in defs_iter { + let mut map: HashMap = HashMap::new(); // todo: inline in else clause? + let definitions_per_file: std::collections::hash_map::Iter = + if search_all_docs { + definition_locations_by_doc.iter() + } else { + let item: DefinitionLocations = + definition_locations_by_doc.get(source_doc).unwrap().clone(); + map.insert(source_doc.clone(), item); + map.iter() + }; + for (_, defs_in_file) in definitions_per_file { use ReferenceKind::*; - let location_map = match location_info.ref_kind { - Alias => &definition_locations.0.alias, - Variable => &definition_locations.0.variable, - VirtualKey => &definition_locations.0.virtual_key, - Layer => &definition_locations.0.layer, - Template => &definition_locations.0.template, + let location_map = match definition_loc.ref_kind { + Alias => &defs_in_file.0.alias, + Variable => &defs_in_file.0.variable, + VirtualKey => &defs_in_file.0.virtual_key, + Layer => &defs_in_file.0.layer, + Template => &defs_in_file.0.template, Include => { + // Short cirtuit since we target_range will be always zero. return { Some(GotoDefinitionLink { - source_range: location_info.source_range, + source_range: definition_loc.source_range, target_range: Range::default(), - target_filename: location_info.ref_name, + target_filename: definition_loc.ref_name, }) }; } }; let loc = location_map - .get(&location_info.ref_name) + .get(&definition_loc.ref_name) .map(|span| GotoDefinitionLink { - source_range: location_info.source_range, + source_range: definition_loc.source_range, target_range: lsp_range_from_span(span), target_filename: span.file_name(), }); @@ -75,8 +75,9 @@ pub fn definition_location( None } -pub fn references( - pos: &Position, +// Returns None if the token at given position is not a definition. +pub fn references_for_definition_at_pos( + source_pos: &Position, source_doc: &Url, definition_locations_by_doc: &HashMap, reference_locations_by_doc: &HashMap, @@ -84,10 +85,11 @@ pub fn references( ) -> Option> { let source_doc_definition_locations = definition_locations_by_doc.get(source_doc)?; - let location_info = match source_doc_definition_locations.search_references_at_position(pos) { - Some(x) => x, - None => return None, - }; + let location_info = + match source_doc_definition_locations.search_references_for_token_at_position(source_pos) { + Some(x) => x, + None => return None, + }; log!("{:?}", &location_info); let mut reference_links: Vec = Vec::new(); diff --git a/server/src/index.ts b/server/src/index.ts index f800829..752f35f 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -14,17 +14,25 @@ connection.onInitialize((params: InitializeParams) => { params, (params: PublishDiagnosticsParams) => connection.sendDiagnostics(params), ); + connection.onNotification((...args) => kls.onNotification(...args)); + connection.onDocumentFormatting((...args) => // eslint-disable-next-line @typescript-eslint/no-unsafe-return kls.onDocumentFormatting(args[0]), ); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return connection.onDefinition((...args) => kls.onDefinition(args[0])); - connection.languages.semanticTokens.on((...args) => - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call - kls.onSemanticTokens(args[0]), - ); + + // connection.languages.semanticTokens.on((...args) => + // // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + // kls.onSemanticTokens(args[0]), + // ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + connection.onHover((...args) => kls.onHover(args[0])); + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call return kls.initialize(params); });