diff --git a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs index 00e0014c..0ea40183 100644 --- a/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs +++ b/crates/emmylua_code_analysis/src/compilation/analyzer/doc/type_ref_tags.rs @@ -389,11 +389,16 @@ pub fn analyze_see(analyzer: &mut DocAnalyzer, tag: LuaDocTagSee) -> Option<()> let owner = get_owner_id_or_report(analyzer, &tag)?; let content = tag.get_see_content()?; let text = content.get_text(); - - analyzer - .db - .get_property_index_mut() - .add_see(analyzer.file_id, owner, text.to_string()); + let descriptions = tag + .get_description() + .map(|description| description.get_description_text()); + + analyzer.db.get_property_index_mut().add_see( + analyzer.file_id, + owner, + text.to_string(), + descriptions, + ); Some(()) } diff --git a/crates/emmylua_code_analysis/src/db_index/property/mod.rs b/crates/emmylua_code_analysis/src/db_index/property/mod.rs index f840fc12..8d4f1537 100644 --- a/crates/emmylua_code_analysis/src/db_index/property/mod.rs +++ b/crates/emmylua_code_analysis/src/db_index/property/mod.rs @@ -160,13 +160,19 @@ impl LuaPropertyIndex { &mut self, file_id: FileId, owner_id: LuaSemanticDeclId, - see_content: String, + mut see_content: String, + see_description: Option, ) -> Option<()> { let property = self.get_or_create_property(owner_id.clone())?; let tag_content = property .tag_content .get_or_insert_with(|| Box::new(LuaTagContent::new())); + if let Some(see_description) = see_description { + see_content += " "; + see_content += &see_description; + } + tag_content.add_tag("see".into(), see_content); self.in_filed_owner diff --git a/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs index 55923355..f20bcd07 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/desc_provider.rs @@ -6,8 +6,8 @@ use crate::handlers::completion::providers::member_provider::add_completions_for use crate::handlers::completion::providers::module_path_provider::add_modules; use crate::util::{find_comment_scope, find_ref_at, resolve_ref}; use emmylua_code_analysis::{LuaType, WorkspaceId}; -use emmylua_parser::{LuaAstNode, LuaDocDescription}; -use emmylua_parser_desc::LuaDescRefPathItem; +use emmylua_parser::{LuaAstNode, LuaDocDescription, LuaTokenKind}; +use emmylua_parser_desc::{LuaDescRefPathItem, parse_ref_target}; use rowan::TextRange; use std::collections::HashSet; @@ -18,28 +18,41 @@ pub fn add_completions(builder: &mut CompletionBuilder) -> Option<()> { let semantic_model = &builder.semantic_model; let document = semantic_model.get_document(); - let description = LuaDocDescription::cast(builder.trigger_token.parent()?)?; - // Quickly scan the line before actually parsing comment. - let line = document.get_line(builder.position_offset)?; - let line_range = document.get_line_range(line)?; - let line_text = - &document.get_text()[line_range.intersect(TextRange::up_to(builder.position_offset))?]; + let path = if let Some(description) = builder + .trigger_token + .parent() + .and_then(LuaDocDescription::cast) + { + // Quickly scan the line before actually parsing comment. + let line = document.get_line(builder.position_offset)?; + let line_range = document.get_line_range(line)?; + let line_text = &document.get_text() + [line_range.intersect(TextRange::up_to(builder.position_offset))?]; + + if !line_text.contains('`') { + return None; + } - if !line_text.contains('`') { + find_ref_at( + semantic_model + .get_module() + .map(|m| m.workspace_id) + .unwrap_or(WorkspaceId::MAIN), + semantic_model.get_emmyrc(), + document.get_text(), + description, + builder.position_offset, + )? + } else if builder.trigger_token.kind() == LuaTokenKind::TkDocSeeContent.into() { + parse_ref_target( + document.get_text(), + builder.trigger_token.text_range(), + builder.position_offset, + )? + } else { return None; - } - - let path = find_ref_at( - semantic_model - .get_module() - .map(|m| m.workspace_id) - .unwrap_or(WorkspaceId::MAIN), - semantic_model.get_emmyrc(), - document.get_text(), - description.clone(), - builder.position_offset, - )?; + }; if path.is_empty() { add_global_completions(builder); diff --git a/crates/emmylua_ls/src/handlers/completion/providers/doc_type_provider.rs b/crates/emmylua_ls/src/handlers/completion/providers/doc_type_provider.rs index 5a0cd3dc..02039d9d 100644 --- a/crates/emmylua_ls/src/handlers/completion/providers/doc_type_provider.rs +++ b/crates/emmylua_ls/src/handlers/completion/providers/doc_type_provider.rs @@ -63,7 +63,7 @@ fn check_can_add_type_completion(builder: &CompletionBuilder) -> Option<()> { LuaTokenKind::TkWhitespace => { let left_token = builder.trigger_token.prev_token()?; match left_token.kind().into() { - LuaTokenKind::TkTagReturn | LuaTokenKind::TkTagType | LuaTokenKind::TkTagSee => { + LuaTokenKind::TkTagReturn | LuaTokenKind::TkTagType => { return Some(()); } LuaTokenKind::TkName => { diff --git a/crates/emmylua_ls/src/handlers/definition/goto_doc_see.rs b/crates/emmylua_ls/src/handlers/definition/goto_doc_see.rs index 94e99f53..c880d7ba 100644 --- a/crates/emmylua_ls/src/handlers/definition/goto_doc_see.rs +++ b/crates/emmylua_ls/src/handlers/definition/goto_doc_see.rs @@ -1,45 +1,41 @@ -use emmylua_code_analysis::{LuaMemberKey, LuaSemanticDeclId, LuaType, SemanticModel}; +use crate::handlers::definition::goto_path::goto_path; +use emmylua_code_analysis::{ + LuaCompilation, LuaMemberKey, LuaSemanticDeclId, LuaType, SemanticModel, +}; use emmylua_parser::{LuaAstToken, LuaGeneralToken}; +use emmylua_parser_desc::parse_ref_target; use lsp_types::GotoDefinitionResponse; +use rowan::TextSize; pub fn goto_doc_see( semantic_model: &SemanticModel, + compilation: &LuaCompilation, content_token: LuaGeneralToken, + position_offset: TextSize, ) -> Option { let text = content_token.get_text(); let name_parts = text.split('#').collect::>(); match name_parts.len() { - 1 => { - let name = &name_parts[0]; - return goto_type(semantic_model, &name); - } - 2 => { + 0 => {} + // Legacy handler for format like `@see type#member` + 2 if !name_parts[1].is_empty() && !name_parts[1].starts_with([' ', '\t']) => { let type_name = &name_parts[0]; let member_name = &name_parts[1]; return goto_type_member(semantic_model, &type_name, &member_name); } - _ => {} - } + _ => { + let path = parse_ref_target( + semantic_model.get_document().get_text(), + content_token.get_range(), + position_offset, + )?; - None -} - -fn goto_type(semantic_model: &SemanticModel, type_name: &str) -> Option { - let file_id = semantic_model.get_file_id(); - let type_decl = semantic_model - .get_db() - .get_type_index() - .find_type_decl(file_id, type_name)?; - let locations = type_decl.get_locations(); - let mut result = Vec::new(); - for location in locations { - let document = semantic_model.get_document_by_file_id(location.file_id)?; - let lsp_location = document.to_lsp_location(location.range)?; - result.push(lsp_location); + return goto_path(semantic_model, compilation, &path, content_token.syntax()); + } } - Some(GotoDefinitionResponse::Array(result)) + None } fn goto_type_member( diff --git a/crates/emmylua_ls/src/handlers/definition/mod.rs b/crates/emmylua_ls/src/handlers/definition/mod.rs index 7d839f53..ac36eaad 100644 --- a/crates/emmylua_ls/src/handlers/definition/mod.rs +++ b/crates/emmylua_ls/src/handlers/definition/mod.rs @@ -94,7 +94,12 @@ pub fn definition( } else if token.kind() == LuaTokenKind::TkDocSeeContent.into() { let general_token = LuaGeneralToken::cast(token.clone())?; if let Some(_) = general_token.get_parent::() { - return goto_doc_see(&semantic_model, general_token); + return goto_doc_see( + &semantic_model, + &analysis.compilation, + general_token, + position_offset, + ); } } else if token.kind() == LuaTokenKind::TkDocDetail.into() { let parent = token.parent()?; diff --git a/crates/emmylua_ls/src/handlers/hover/hover_builder.rs b/crates/emmylua_ls/src/handlers/hover/hover_builder.rs index c0f9ba49..ddb5eea7 100644 --- a/crates/emmylua_ls/src/handlers/hover/hover_builder.rs +++ b/crates/emmylua_ls/src/handlers/hover/hover_builder.rs @@ -23,7 +23,7 @@ pub struct HoverBuilder<'a> { pub annotation_description: Vec, /// Type expansion, often used for alias types pub type_expansion: Option>, - /// see + /// For `@see` and unknown tags tags tag_content: Option>, pub is_completion: bool, @@ -239,6 +239,9 @@ impl<'a> HoverBuilder<'a> { } if let Some(tag_content) = &self.tag_content { + if !tag_content.is_empty() { + content.push_str("\n---\n"); + } for (tag_name, description) in tag_content { content.push_str(&format!("\n@*{}* {}\n", tag_name, description)); } diff --git a/crates/emmylua_ls/src/handlers/hover/mod.rs b/crates/emmylua_ls/src/handlers/hover/mod.rs index ae60b5e3..9b781d76 100644 --- a/crates/emmylua_ls/src/handlers/hover/mod.rs +++ b/crates/emmylua_ls/src/handlers/hover/mod.rs @@ -6,10 +6,14 @@ mod hover_humanize; mod keyword_hover; mod std_hover; +use super::RegisterCapabilities; +use crate::context::ServerContextSnapshot; +use crate::util::{find_ref_at, resolve_ref_single}; pub use build_hover::build_hover_content_for_completion; use build_hover::build_semantic_info_hover; use emmylua_code_analysis::{EmmyLuaAnalysis, FileId, WorkspaceId}; -use emmylua_parser::{LuaAstNode, LuaDocDescription, LuaKind, LuaTokenKind}; +use emmylua_parser::{LuaAstNode, LuaDocDescription, LuaTokenKind}; +use emmylua_parser_desc::parse_ref_target; pub use find_origin::{find_all_same_named_members, find_member_origin_owner}; pub use hover_builder::HoverBuilder; pub use hover_humanize::infer_prefix_global_name; @@ -22,10 +26,6 @@ use rowan::TokenAtOffset; pub use std_hover::{hover_std_description, is_std}; use tokio_util::sync::CancellationToken; -use super::RegisterCapabilities; -use crate::context::ServerContextSnapshot; -use crate::util::{find_ref_at, resolve_ref_single}; - pub async fn on_hover( context: ServerContextSnapshot, params: HoverParams, @@ -82,7 +82,7 @@ pub fn hover(analysis: &EmmyLuaAnalysis, file_id: FileId, position: Position) -> range: document.to_lsp_range(keywords.text_range()), }); } - detail if detail.kind() == LuaKind::Token(LuaTokenKind::TkDocDetail) => { + detail if detail.kind() == LuaTokenKind::TkDocDetail.into() => { let parent = detail.parent()?; let description = LuaDocDescription::cast(parent)?; let document = semantic_model.get_document(); @@ -111,6 +111,25 @@ pub fn hover(analysis: &EmmyLuaAnalysis, file_id: FileId, position: Position) -> path.last()?.1, ) } + doc_see if doc_see.kind() == LuaTokenKind::TkDocSeeContent.into() => { + let document = semantic_model.get_document(); + + let path = + parse_ref_target(document.get_text(), doc_see.text_range(), position_offset)?; + + let db = analysis.compilation.get_db(); + let semantic_info = resolve_ref_single(db, file_id, &path, &doc_see)?; + + build_semantic_info_hover( + &analysis.compilation, + &semantic_model, + db, + &document, + doc_see, + semantic_info, + path.last()?.1, + ) + } _ => { let semantic_info = semantic_model.get_semantic_info(token.clone().into())?; let db = semantic_model.get_db(); diff --git a/crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs b/crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs index ff2858bc..3d631352 100644 --- a/crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs +++ b/crates/emmylua_ls/src/handlers/semantic_token/build_semantic_tokens.rs @@ -216,13 +216,20 @@ fn build_tokens_semantic_token( SemanticTokenModifier::DOCUMENTATION, ); } - LuaTokenKind::TKDocPath | LuaTokenKind::TkDocSeeContent => { + LuaTokenKind::TKDocPath => { builder.push_with_modifier( token, SemanticTokenType::STRING, SemanticTokenModifier::DOCUMENTATION, ); } + LuaTokenKind::TkDocSeeContent => { + builder.push_with_modifier( + token, + SemanticTokenType::VARIABLE, + SemanticTokenModifier::DOCUMENTATION, + ); + } LuaTokenKind::TkDocRegion | LuaTokenKind::TkDocEndRegion => { builder.push(token, SemanticTokenType::COMMENT); } diff --git a/crates/emmylua_ls/src/handlers/test/completion_test.rs b/crates/emmylua_ls/src/handlers/test/completion_test.rs index ead99586..b78909ea 100644 --- a/crates/emmylua_ls/src/handlers/test/completion_test.rs +++ b/crates/emmylua_ls/src/handlers/test/completion_test.rs @@ -2082,4 +2082,37 @@ mod tests { )); Ok(()) } + + #[gtest] + fn test_see_completion() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + ws.def( + r#" + ---@class Meep + "#, + ); + check!(ws.check_completion( + r#" + --- @see M + "#, + vec![ + VirtualCompletionItem { + label: "Meep".to_string(), + kind: CompletionItemKind::CLASS, + ..Default::default() + }, + VirtualCompletionItem { + label: "virtual_0".to_string(), + kind: CompletionItemKind::FILE, + ..Default::default() + }, + VirtualCompletionItem { + label: "virtual_1".to_string(), + kind: CompletionItemKind::FILE, + ..Default::default() + }, + ], + )); + Ok(()) + } } diff --git a/crates/emmylua_ls/src/handlers/test/definition_test.rs b/crates/emmylua_ls/src/handlers/test/definition_test.rs index 41e296ef..448e57e0 100644 --- a/crates/emmylua_ls/src/handlers/test/definition_test.rs +++ b/crates/emmylua_ls/src/handlers/test/definition_test.rs @@ -466,4 +466,56 @@ mod tests { )); Ok(()) } + + #[gtest] + fn test_goto_see() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + let mut emmyrc = Emmyrc::default(); + emmyrc.doc.syntax = DocSyntax::Myst; + ws.analysis.update_config(emmyrc.into()); + + ws.def_file( + "a.lua", + r#" + ---@class Meep + "#, + ); + + check!(ws.check_definition( + r#" + --- @see Meep + "#, + vec![Expected { + file: "a.lua".to_string(), + line: 1, + }], + )); + + check!(ws.check_definition( + r#" + --- @see Meep {lua:obj}`Meep` + "#, + vec![Expected { + file: "a.lua".to_string(), + line: 1, + }], + )); + + check!(ws.check_definition( + r#" + --- @class Foo + --- @field bar int + local Foo = {} + + --- @see bar + Foo.xxx = 0 + "#, + vec![Expected { + file: "".to_string(), + line: 2, + }], + )); + + Ok(()) + } } diff --git a/crates/emmylua_ls/src/handlers/test/hover_test.rs b/crates/emmylua_ls/src/handlers/test/hover_test.rs index 279f686c..fb5a769b 100644 --- a/crates/emmylua_ls/src/handlers/test/hover_test.rs +++ b/crates/emmylua_ls/src/handlers/test/hover_test.rs @@ -365,4 +365,52 @@ mod tests { Ok(()) } + + #[gtest] + fn test_see_tag() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_hover( + r#" + --- Description + --- + --- @see a.b.c + local function test() end + "#, + VirtualHoverResult { + value: "```lua\nlocal function test()\n```\n\n---\n\nDescription\n\n---\n\n@*see* a.b.c".to_string(), + }, + )); + + check!(ws.check_hover( + r#" + --- Description + --- + --- @see a.b.c see description + local function test() end + "#, + VirtualHoverResult { + value: "```lua\nlocal function test()\n```\n\n---\n\nDescription\n\n---\n\n@*see* a.b.c see description".to_string(), + }, + )); + + Ok(()) + } + + #[gtest] + fn test_other_tag() -> Result<()> { + let mut ws = ProviderVirtualWorkspace::new(); + check!(ws.check_hover( + r#" + --- Description + --- + --- @xyz content + local function test() end + "#, + VirtualHoverResult { + value: "```lua\nlocal function test()\n```\n\n---\n\nDescription\n\n---\n\n@*xyz* content".to_string(), + }, + )); + + Ok(()) + } } diff --git a/crates/emmylua_parser/src/grammar/doc/test.rs b/crates/emmylua_parser/src/grammar/doc/test.rs index 37afd033..c5798c44 100644 --- a/crates/emmylua_parser/src/grammar/doc/test.rs +++ b/crates/emmylua_parser/src/grammar/doc/test.rs @@ -1365,6 +1365,33 @@ Syntax(Chunk)@0..33 assert_ast_eq!(code, result); } + #[test] + fn test_see_doc_with_description() { + let code = r#" + ---@see aaa.[1].[{"boo": Bar}].x#bbb description + "#; + + let result = r##" +Syntax(Chunk)@0..66 + Syntax(Block)@0..66 + Token(TkEndOfLine)@0..1 "\n" + Token(TkWhitespace)@1..9 " " + Syntax(Comment)@9..57 + Token(TkDocStart)@9..13 "---@" + Syntax(DocTagSee)@13..45 + Token(TkTagSee)@13..16 "see" + Token(TkWhitespace)@16..17 " " + Token(TkDocSeeContent)@17..45 "aaa.[1].[{\"boo\": Bar} ..." + Token(TkWhitespace)@45..46 " " + Syntax(DocDescription)@46..57 + Token(TkDocDetail)@46..57 "description" + Token(TkEndOfLine)@57..58 "\n" + Token(TkWhitespace)@58..66 " " + "##; + + assert_ast_eq!(code, result); + } + #[test] fn test_version_doc() { let code = r#" diff --git a/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs b/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs index c32f049c..4151409b 100644 --- a/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs +++ b/crates/emmylua_parser/src/lexer/lua_doc_lexer.rs @@ -403,7 +403,30 @@ impl LuaDocLexer<'_> { LuaTokenKind::TkWhitespace } _ => { - reader.eat_while(|_| true); + let mut brace_level = 0usize; + while !reader.is_eof() { + match reader.current_char() { + '[' | '(' | '<' | '{' => { + brace_level += 1; + reader.bump(); + } + ']' | ')' | '>' | '}' => { + brace_level = brace_level.saturating_sub(1); + reader.bump(); + } + ch @ '"' | ch @ '\'' => { + reader.bump(); + reader.eat_while(|c| c != ch); + if reader.current_char() == ch { + reader.bump(); + } + } + '-' | '_' | '.' | '#' | 'a'..='z' | 'A'..='Z' | '0'..='9' => reader.bump(), + _ if brace_level > 0 => reader.bump(), + _ => break, + } + } + LuaTokenKind::TkDocSeeContent } }