From db82ca1a1c72562bbdacb278c3318ebe14a057ea Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Tue, 17 Feb 2026 08:32:01 -0500 Subject: [PATCH] fix: improve LSP hover to show function info when cursor is on `fn` keyword When hovering on declaration keywords (fn, type, effect, let, trait), look ahead to find the declaration name and show that symbol's full info from the symbol table instead of generic keyword documentation. Co-Authored-By: Claude Opus 4.6 --- src/lsp.rs | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/src/lsp.rs b/src/lsp.rs index bcbaf4f..f16f3e9 100644 --- a/src/lsp.rs +++ b/src/lsp.rs @@ -356,12 +356,57 @@ impl LspServer { } } - // Fall back to hardcoded info - - // Extract the word at the cursor position + // Fall back: try looking up the declaration name when hovering on keywords let word = self.get_word_at_position(source, position)?; - // Look up rich documentation for known symbols + // When hovering on a keyword like 'fn', 'type', 'effect', 'let', 'trait', + // look ahead to find the declaration name and show that symbol's info + if let Some(ref table) = doc.symbol_table { + let decl_name = match word.as_str() { + "fn" | "type" | "effect" | "let" | "trait" | "handler" | "impl" => { + let offset = self.position_to_offset(source, position); + self.find_next_ident(source, offset + word.len()) + } + _ => None, + }; + if let Some(name) = decl_name { + // Look up the declaration name in the symbol table + for sym in table.global_symbols() { + if sym.name == name { + let signature = sym.type_signature.as_ref() + .map(|s| s.as_str()) + .unwrap_or(&sym.name); + let kind_str = match sym.kind { + SymbolKind::Function => "function", + SymbolKind::Variable => "variable", + SymbolKind::Parameter => "parameter", + SymbolKind::Type => "type", + SymbolKind::TypeParameter => "type parameter", + SymbolKind::Variant => "variant", + SymbolKind::Effect => "effect", + SymbolKind::EffectOperation => "effect operation", + SymbolKind::Field => "field", + SymbolKind::Module => "module", + }; + let doc_str = sym.documentation.as_ref() + .map(|d| format!("\n\n{}", d)) + .unwrap_or_default(); + let formatted_sig = format_signature_for_hover(signature); + let property_docs = extract_property_docs(signature); + + return Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format!("```lux\n{}\n```\n\n*{}*{}{}", formatted_sig, kind_str, property_docs, doc_str), + }), + range: None, + }); + } + } + } + } + + // Final fallback: rich documentation for keywords let info = self.get_rich_symbol_info(&word) .or_else(|| self.get_symbol_info(&word).map(|(s, d)| (s.to_string(), d.to_string()))); @@ -402,6 +447,26 @@ impl LspServer { } } + /// Find the next identifier in source after the given offset (skipping whitespace) + fn find_next_ident(&self, source: &str, start: usize) -> Option { + let chars: Vec = source.chars().collect(); + let mut pos = start; + // Skip whitespace + while pos < chars.len() && (chars[pos] == ' ' || chars[pos] == '\t' || chars[pos] == '\n' || chars[pos] == '\r') { + pos += 1; + } + // Collect identifier + let ident_start = pos; + while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') { + pos += 1; + } + if pos > ident_start { + Some(chars[ident_start..pos].iter().collect()) + } else { + None + } + } + fn get_symbol_info(&self, word: &str) -> Option<(&'static str, &'static str)> { match word { // Keywords