Hover
Displaying type information and documentation when the user hovers over code.
Implementation Status
| Feature | Status |
|---|---|
| Function signatures | ✅ Implemented |
| Type definitions | ✅ Implemented |
| Variable types | ⚠ Not Implemented |
| Doc comments | ⚠ Not Implemented |
| Expression types | ⚠ Not Implemented |
Overview
Hover is a request from client to server. The server returns information to display in a tooltip.
textDocument/hover
Client ─────────────────────────► Server
◄───────────────────────── (Hover)
Current Implementation
Limitation: Hover only works on function and type definitions at the top level. It does not resolve variable references, expressions, field accesses, or any identifiers inside function bodies. Hovering over a variable usage or a function call returns nothing.
The current hover implementation finds items (functions, types) at the cursor position:
impl OriLanguageServer {
fn get_hover_info(&self, uri: &Url, position: Position) -> Option<Hover> {
let doc = self.documents.get(uri)?;
let module = doc.module.as_ref()?;
let offset = position_to_offset(&doc.text, position);
for item in &module.items {
if let Some(info) = self.hover_for_item(item, offset) {
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: info,
}),
range: None,
});
}
}
None
}
fn hover_for_item(&self, item: &Item, offset: usize) -> Option<String> {
match item {
Item::Function(fd) => {
if fd.span.contains(&offset) {
Some(self.function_signature(fd))
} else {
None
}
}
Item::TypeDef(td) => {
if td.span.contains(&offset) {
Some(self.type_signature(td))
} else {
None
}
}
_ => None,
}
}
}
Future Design: Enhanced Hover
The planned enhanced implementation would provide richer hover info:
// Planned: features/hover.rs
pub fn hover(
docs: &DocumentManager,
params: HoverParams,
) -> Option<Hover> {
let uri = ¶ms.text_document.uri;
let pos = params.position;
let doc = docs.get(uri)?;
let analysis = doc.analyze();
// Find the AST node at the cursor position
let ast = analysis.ast.as_ref()?;
let offset = position_to_offset(&doc.text, pos);
let node = ast.node_at_offset(offset)?;
// Generate hover content based on node type
let content = generate_hover_content(node, &analysis)?;
Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: content,
}),
range: Some(span_to_range(&doc.text, node.span())),
})
}
Node-Specific Hover
fn generate_hover_content(node: &Node, analysis: &AnalysisResult) -> Option<String> {
let types = analysis.types.as_ref()?;
match node {
// Variable reference: show type
Node::Identifier(name, span) => {
let ty = types.type_of_binding(name, *span)?;
Some(format!("```ori\n{}: {}\n```", name, ty))
}
// Function call: show signature
Node::Call { func, .. } => {
let sig = types.function_signature(func)?;
Some(format!("```ori\n{}\n```", sig))
}
// Function definition: show full signature with doc
Node::FunctionDef { name, params, return_type, doc, .. } => {
let mut content = format!("```ori\n@{} ", name);
// Parameters
content.push('(');
for (i, param) in params.iter().enumerate() {
if i > 0 { content.push_str(", "); }
content.push_str(&format!("{}: {}", param.name, param.ty));
}
content.push(')');
// Return type
if let Some(ret) = return_type {
content.push_str(&format!(" -> {}", ret));
}
content.push_str("\n```");
// Doc comment
if let Some(doc) = doc {
content.push_str("\n\n---\n\n");
content.push_str(doc);
}
Some(content)
}
// Type reference: show definition
Node::TypeRef(name, span) => {
let def = types.type_definition(name)?;
Some(format!("```ori\ntype {} = {}\n```", name, def))
}
// Field access: show field type
Node::FieldAccess { field, base_type, .. } => {
let field_type = types.field_type(base_type, field)?;
Some(format!("```ori\n{}: {}\n```", field, field_type))
}
// Literal: show inferred type
Node::Literal(lit) => {
let ty = types.type_of_literal(lit)?;
Some(format!("```ori\n{}\n```", ty))
}
// Pattern in match arm
Node::Pattern(pat) => {
let bindings = types.pattern_bindings(pat)?;
let mut content = String::from("Pattern bindings:\n```ori\n");
for (name, ty) in bindings {
content.push_str(&format!("{}: {}\n", name, ty));
}
content.push_str("```");
Some(content)
}
_ => None,
}
}
Hover Content Examples
Variable
Hovering over x in let y = x + 1:
```ori
x: int
### Function Call
Hovering over `fetch` in `fetch(url: endpoint)`:
```markdown
```ori
@fetch (url: str) -> Result<str, HttpError> uses Http
### Function Definition
Hovering over `@calculate`:
```markdown
```ori
@calculate (a: int, b: int) -> int
Calculates the sum of two integers.
@param a First operand @param b Second operand
### Type Reference
Hovering over `User` in `let u: User`:
```markdown
```ori
type User = {
id: int,
name: str,
email: str,
}
### Method Call
Hovering over `map` in `items.map(...)`:
```markdown
```ori
@map<U> (self, transform: (T) -> U) -> [U]
### Generic Type
Hovering over `Option<int>`:
```markdown
```ori
type Option<T> = Some(T) | None
Specialized as: Option<int>
## Finding the Node
LSP provides a position (line, column). We need to find which AST node is there:
```rust
impl Module {
pub fn node_at_offset(&self, offset: usize) -> Option<&Node> {
self.walk()
.filter(|n| n.span().contains(offset))
.min_by_key(|n| n.span().len()) // Innermost node
}
}
impl Span {
pub fn contains(&self, offset: usize) -> bool {
offset >= self.start && offset < self.end
}
pub fn len(&self) -> usize {
self.end - self.start
}
}
Priority for Overlapping Nodes
When multiple nodes overlap, prefer:
- Identifiers/names over enclosing expressions
- Innermost node (smallest span)
fn best_hover_node<'a>(candidates: impl Iterator<Item = &'a Node>) -> Option<&'a Node> {
candidates
.filter(|n| is_hoverable(n))
.min_by_key(|n| {
// Prefer identifiers, then by span size
let priority = match n {
Node::Identifier(..) => 0,
Node::TypeRef(..) => 0,
Node::FieldAccess { .. } => 1,
_ => 2,
};
(priority, n.span().len())
})
}
fn is_hoverable(node: &Node) -> bool {
matches!(node,
Node::Identifier(..) |
Node::TypeRef(..) |
Node::FunctionDef { .. } |
Node::Call { .. } |
Node::FieldAccess { .. } |
Node::Literal(..) |
Node::Pattern(..)
)
}
Graceful Degradation
When full type info isn’t available:
fn generate_hover_content(node: &Node, analysis: &AnalysisResult) -> Option<String> {
// Try to get full type information
if let Some(types) = &analysis.types {
if let Some(content) = generate_typed_hover(node, types) {
return Some(content);
}
}
// Fallback: show what we know from syntax alone
generate_syntax_hover(node)
}
fn generate_syntax_hover(node: &Node) -> Option<String> {
match node {
Node::FunctionDef { name, params, return_type, .. } => {
// Show signature without inferred types
let params_str = params.iter()
.map(|p| match &p.ty {
Some(t) => format!("{}: {}", p.name, t),
None => p.name.clone(),
})
.collect::<Vec<_>>()
.join(", ");
let ret = return_type.as_ref().map(|t| format!(" -> {}", t)).unwrap_or_default();
Some(format!("```ori\n@{} ({}){}\n```", name, params_str, ret))
}
Node::Identifier(name, _) => {
// Just show the name
Some(format!("`{}`", name))
}
_ => None,
}
}
WASM API
#[wasm_bindgen]
impl WasmLanguageServer {
pub fn hover(&self, uri: &str, line: u32, character: u32) -> Option<String> {
let uri = Url::parse(uri).ok()?;
let params = HoverParams {
text_document: TextDocumentIdentifier { uri },
position: Position { line, character },
work_done_progress_params: Default::default(),
};
let result = hover(&self.inner.documents, params)?;
Some(serde_json::to_string(&result).unwrap())
}
}
Monaco Integration
monaco.languages.registerHoverProvider('ori', {
provideHover(model, position) {
const uri = model.uri.toString();
const hoverJson = server.hover(
uri,
position.lineNumber - 1, // LSP is 0-indexed
position.column - 1
);
if (!hoverJson) return null;
const hover = JSON.parse(hoverJson);
return {
contents: [{
value: hover.contents.value,
isTrusted: true, // Allow markdown
}],
range: hover.range ? {
startLineNumber: hover.range.start.line + 1,
startColumn: hover.range.start.character + 1,
endLineNumber: hover.range.end.line + 1,
endColumn: hover.range.end.character + 1,
} : undefined,
};
}
});
Testing
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hover_variable() {
let code = "let x: int = 42\nlet y = x + 1";
let server = test_server(code);
// Hover over 'x' on line 2
let hover = server.hover(pos(1, 8)).unwrap();
assert!(hover.contents.value.contains("x: int"));
}
#[test]
fn test_hover_function_call() {
let code = "@add (a: int, b: int) -> int = a + b\nlet sum = add(a: 1, b: 2)";
let server = test_server(code);
// Hover over 'add' call
let hover = server.hover(pos(1, 10)).unwrap();
assert!(hover.contents.value.contains("@add"));
assert!(hover.contents.value.contains("a: int"));
assert!(hover.contents.value.contains("-> int"));
}
#[test]
fn test_hover_no_type_info() {
let code = "let x = unknown_function()"; // Type error
let server = test_server(code);
// Should still get some hover for 'x'
let hover = server.hover(pos(0, 4));
// May return None or fallback content
// Depends on error recovery strategy
}
}