feat: enhance LSP with inlay hints, parameter hints, and improved hover
Add inlay type hints for let bindings, parameter name hints at call sites, behavioral property documentation in hover, and long signature wrapping. - Inlay hints: show inferred types for let bindings without annotations - Parameter hints: show param names at call sites for multi-arg functions - Hover: wrap long signatures, show behavioral property docs (pure, total, etc.) - Rich docs: detailed hover for keywords like pure, total, idempotent, run, with - TypeChecker: expose get_inferred_type() for LSP consumption - Symbol table: include behavioral properties in function type signatures Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
377
src/lsp.rs
377
src/lsp.rs
@@ -19,7 +19,7 @@ use crate::formatter::{format as format_source, FormatConfig};
|
||||
use lsp_server::{Connection, ExtractError, Message, Request, RequestId, Response};
|
||||
use lsp_types::{
|
||||
notification::{DidChangeTextDocument, DidOpenTextDocument, Notification},
|
||||
request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting},
|
||||
request::{Completion, GotoDefinition, HoverRequest, References, DocumentSymbolRequest, Rename, SignatureHelpRequest, Formatting, InlayHintRequest},
|
||||
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
|
||||
Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
|
||||
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
|
||||
@@ -28,7 +28,8 @@ use lsp_types::{
|
||||
TextDocumentSyncKind, Url, ReferenceParams, Location, DocumentSymbolParams,
|
||||
DocumentSymbolResponse, SymbolInformation, RenameParams, WorkspaceEdit, TextEdit,
|
||||
SignatureHelpParams, SignatureHelp, SignatureInformation, ParameterInformation,
|
||||
SignatureHelpOptions, DocumentFormattingParams, TextDocumentIdentifier,
|
||||
SignatureHelpOptions, DocumentFormattingParams,
|
||||
InlayHint, InlayHintKind, InlayHintLabel, InlayHintParams,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
@@ -88,6 +89,7 @@ impl LspServer {
|
||||
work_done_progress_options: Default::default(),
|
||||
}),
|
||||
document_formatting_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
inlay_hint_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
@@ -191,7 +193,7 @@ impl LspServer {
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
let _req = match cast_request::<Formatting>(req) {
|
||||
let req = match cast_request::<Formatting>(req) {
|
||||
Ok((id, params)) => {
|
||||
let result = self.handle_formatting(params);
|
||||
let resp = Response::new_ok(id, result);
|
||||
@@ -201,6 +203,16 @@ impl LspServer {
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
let _req = match cast_request::<InlayHintRequest>(req) {
|
||||
Ok((id, params)) => {
|
||||
let result = self.handle_inlay_hints(params);
|
||||
let resp = Response::new_ok(id, result);
|
||||
self.connection.sender.send(Message::Response(resp))?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(req) => req,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -328,10 +340,16 @@ impl LspServer {
|
||||
.map(|d| format!("\n\n{}", d))
|
||||
.unwrap_or_default();
|
||||
|
||||
// Format signature: wrap long signatures onto multiple lines
|
||||
let formatted_sig = format_signature_for_hover(signature);
|
||||
|
||||
// Add behavioral property documentation if present
|
||||
let property_docs = extract_property_docs(signature);
|
||||
|
||||
return Some(Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n*{}*{}", signature, kind_str, doc_str),
|
||||
value: format!("```lux\n{}\n```\n\n*{}*{}{}", formatted_sig, kind_str, property_docs, doc_str),
|
||||
}),
|
||||
range: None,
|
||||
});
|
||||
@@ -343,19 +361,20 @@ impl LspServer {
|
||||
// Extract the word at the cursor position
|
||||
let word = self.get_word_at_position(source, position)?;
|
||||
|
||||
// Look up documentation for known symbols
|
||||
let info = self.get_symbol_info(&word);
|
||||
// Look up rich documentation for known symbols
|
||||
let info = self.get_rich_symbol_info(&word)
|
||||
.or_else(|| self.get_symbol_info(&word).map(|(s, d)| (s.to_string(), d.to_string())));
|
||||
|
||||
if let Some((signature, doc)) = info {
|
||||
let formatted_sig = format_signature_for_hover(&signature);
|
||||
Some(Hover {
|
||||
contents: HoverContents::Markup(MarkupContent {
|
||||
kind: MarkupKind::Markdown,
|
||||
value: format!("```lux\n{}\n```\n\n{}", signature, doc),
|
||||
value: format!("```lux\n{}\n```\n\n{}", formatted_sig, doc),
|
||||
}),
|
||||
range: None,
|
||||
})
|
||||
} else {
|
||||
// Return generic info for unknown symbols
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -439,6 +458,84 @@ impl LspServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rich documentation for behavioral properties and keywords
|
||||
fn get_rich_symbol_info(&self, word: &str) -> Option<(String, String)> {
|
||||
match word {
|
||||
"pure" => Some((
|
||||
"is pure".to_string(),
|
||||
"**Behavioral Property: Pure**\n\n\
|
||||
A pure function has no side effects and always produces the same output for the same inputs. \
|
||||
The compiler can safely memoize calls, reorder them, or eliminate duplicates.\n\n\
|
||||
```lux\nfn add(a: Int, b: Int): Int is pure = a + b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- No effect operations (Console, File, Http, etc.)\n\
|
||||
- Referential transparency: `f(x)` can be replaced with its result\n\
|
||||
- Enables memoization and common subexpression elimination".to_string(),
|
||||
)),
|
||||
"total" => Some((
|
||||
"is total".to_string(),
|
||||
"**Behavioral Property: Total**\n\n\
|
||||
A total function always terminates and never throws exceptions. \
|
||||
The compiler verifies termination through structural recursion analysis.\n\n\
|
||||
```lux\nfn factorial(n: Int): Int is total =\n if n <= 0 then 1\n else n * factorial(n - 1)\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Always produces a result (no infinite loops)\n\
|
||||
- Cannot use the `Fail` effect\n\
|
||||
- Recursive calls must be structurally decreasing".to_string(),
|
||||
)),
|
||||
"idempotent" => Some((
|
||||
"is idempotent".to_string(),
|
||||
"**Behavioral Property: Idempotent**\n\n\
|
||||
An idempotent function satisfies `f(f(x)) == f(x)` for all inputs. \
|
||||
Applying it multiple times has the same effect as applying it once.\n\n\
|
||||
```lux\nfn abs(x: Int): Int is idempotent =\n if x < 0 then 0 - x else x\n\n\
|
||||
fn clamp(x: Int): Int is idempotent =\n if x < 0 then 0\n else if x > 100 then 100\n else x\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- `f(f(x)) == f(x)` for all valid inputs\n\
|
||||
- Safe to retry without changing outcome\n\
|
||||
- Compiler can deduplicate consecutive calls".to_string(),
|
||||
)),
|
||||
"deterministic" => Some((
|
||||
"is deterministic".to_string(),
|
||||
"**Behavioral Property: Deterministic**\n\n\
|
||||
A deterministic function always produces the same output for the same inputs, \
|
||||
with no dependence on randomness, time, or external state.\n\n\
|
||||
```lux\nfn multiply(a: Int, b: Int): Int is deterministic = a * b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Cannot use `Random` or `Time` effects\n\
|
||||
- Same inputs always produce same outputs\n\
|
||||
- Results can be cached across runs".to_string(),
|
||||
)),
|
||||
"commutative" => Some((
|
||||
"is commutative".to_string(),
|
||||
"**Behavioral Property: Commutative**\n\n\
|
||||
A commutative function satisfies `f(a, b) == f(b, a)`. \
|
||||
The order of arguments doesn't affect the result.\n\n\
|
||||
```lux\nfn add(a: Int, b: Int): Int is commutative = a + b\nfn max(a: Int, b: Int): Int is commutative =\n if a > b then a else b\n```\n\n\
|
||||
**Guarantees:**\n\
|
||||
- Must have exactly 2 parameters\n\
|
||||
- `f(a, b) == f(b, a)` for all inputs\n\
|
||||
- Compiler can normalize argument order for optimization".to_string(),
|
||||
)),
|
||||
"run" => Some((
|
||||
"run expr with { handlers }".to_string(),
|
||||
"**Effect Handler**\n\n\
|
||||
Execute an effectful expression with explicit effect handlers. \
|
||||
Must be bound to a variable at top level.\n\n\
|
||||
```lux\nlet result = run myFunction() with {\n Console = { /* handler */ }\n}\n```\n\n\
|
||||
Handlers intercept effect operations and provide implementations.".to_string(),
|
||||
)),
|
||||
"with" => Some((
|
||||
"with {Effect1, Effect2}".to_string(),
|
||||
"**Effect Declaration / Handler Block**\n\n\
|
||||
Declares which effects a function may perform, or provides handlers in a `run` expression.\n\n\
|
||||
```lux\n// In function signature:\nfn greet(name: String): Unit with {Console} =\n Console.print(\"Hello, \" + name)\n\n\
|
||||
// In run expression:\nlet _ = run greet(\"world\") with {}\n```".to_string(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_completion(&self, params: CompletionParams) -> Option<CompletionResponse> {
|
||||
let uri = params.text_document_position.text_document.uri;
|
||||
let position = params.text_document_position.position;
|
||||
@@ -1022,6 +1119,90 @@ impl LspServer {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_inlay_hints(&self, params: InlayHintParams) -> Option<Vec<InlayHint>> {
|
||||
let uri = params.text_document.uri;
|
||||
let doc = self.documents.get(&uri)?;
|
||||
let source = &doc.text;
|
||||
|
||||
// Parse the document to get AST
|
||||
let program = Parser::parse_source(source).ok()?;
|
||||
|
||||
// Type-check to get inferred types
|
||||
let mut checker = TypeChecker::new();
|
||||
let _ = checker.check_program(&program);
|
||||
|
||||
let mut hints = Vec::new();
|
||||
|
||||
// Collect parameter names for known functions (from symbol table)
|
||||
let param_names = self.collect_function_params(&program);
|
||||
|
||||
for decl in &program.declarations {
|
||||
match decl {
|
||||
crate::ast::Declaration::Let(l) => {
|
||||
// Show inferred type for let bindings without explicit type annotations
|
||||
if l.typ.is_none() {
|
||||
if let Some(inferred_type) = checker.get_inferred_type(&l.name.name) {
|
||||
let type_str = format!(": {}", inferred_type);
|
||||
let pos = offset_to_position(source, l.name.span.end);
|
||||
hints.push(InlayHint {
|
||||
position: pos,
|
||||
label: InlayHintLabel::String(type_str),
|
||||
kind: Some(InlayHintKind::TYPE),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: Some(false),
|
||||
padding_right: Some(true),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Walk into the value expression for call-site parameter hints
|
||||
collect_call_site_hints(source, &l.value, ¶m_names, &mut hints);
|
||||
}
|
||||
crate::ast::Declaration::Function(f) => {
|
||||
// Walk into the function body for call-site parameter hints
|
||||
collect_call_site_hints(source, &f.body, ¶m_names, &mut hints);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if hints.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hints)
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect parameter names for all functions defined in the program
|
||||
fn collect_function_params(&self, program: &crate::ast::Program) -> HashMap<String, Vec<String>> {
|
||||
let mut params = HashMap::new();
|
||||
for decl in &program.declarations {
|
||||
if let crate::ast::Declaration::Function(f) = decl {
|
||||
let names: Vec<String> = f.params.iter()
|
||||
.map(|p| p.name.name.clone())
|
||||
.collect();
|
||||
params.insert(f.name.name.clone(), names);
|
||||
}
|
||||
}
|
||||
// Add builtin function parameter names
|
||||
params.insert("map".into(), vec!["list".into(), "f".into()]);
|
||||
params.insert("filter".into(), vec!["list".into(), "predicate".into()]);
|
||||
params.insert("fold".into(), vec!["list".into(), "init".into(), "f".into()]);
|
||||
params.insert("concat".into(), vec!["a".into(), "b".into()]);
|
||||
params.insert("range".into(), vec!["start".into(), "end".into()]);
|
||||
params.insert("get".into(), vec!["list".into(), "index".into()]);
|
||||
params.insert("take".into(), vec!["list".into(), "n".into()]);
|
||||
params.insert("drop".into(), vec!["list".into(), "n".into()]);
|
||||
params.insert("split".into(), vec!["s".into(), "delimiter".into()]);
|
||||
params.insert("join".into(), vec!["list".into(), "delimiter".into()]);
|
||||
params.insert("replace".into(), vec!["s".into(), "old".into(), "new".into()]);
|
||||
params.insert("substring".into(), vec!["s".into(), "start".into(), "end".into()]);
|
||||
params.insert("contains".into(), vec!["s".into(), "substr".into()]);
|
||||
params.insert("getOrElse".into(), vec!["opt".into(), "default".into()]);
|
||||
params
|
||||
}
|
||||
|
||||
fn handle_formatting(&self, params: DocumentFormattingParams) -> Option<Vec<TextEdit>> {
|
||||
let uri = params.text_document.uri;
|
||||
let doc = self.documents.get(&uri)?;
|
||||
@@ -1061,6 +1242,186 @@ impl LspServer {
|
||||
}
|
||||
|
||||
/// Convert byte offsets to LSP Position
|
||||
/// Format a function signature for hover display, wrapping long lines
|
||||
fn format_signature_for_hover(sig: &str) -> String {
|
||||
// If it fits in ~60 chars, keep it on one line
|
||||
if sig.len() <= 60 {
|
||||
return sig.to_string();
|
||||
}
|
||||
|
||||
// Try to break at parameter list for function signatures
|
||||
if let Some(paren_start) = sig.find('(') {
|
||||
if let Some(paren_end) = sig.rfind(')') {
|
||||
let prefix = &sig[..paren_start + 1];
|
||||
let params = &sig[paren_start + 1..paren_end];
|
||||
let suffix = &sig[paren_end..];
|
||||
|
||||
// Split parameters and format each on its own line
|
||||
let param_parts: Vec<&str> = params.split(", ").collect();
|
||||
if param_parts.len() > 1 {
|
||||
let indent = " ";
|
||||
let formatted_params = param_parts.join(&format!(",\n{}", indent));
|
||||
return format!("{}\n{}{}\n{}", prefix, indent, formatted_params, suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sig.to_string()
|
||||
}
|
||||
|
||||
/// Extract behavioral property documentation from a signature string
|
||||
fn extract_property_docs(sig: &str) -> String {
|
||||
let properties = [
|
||||
("is pure", "**pure** — no side effects, same output for same inputs"),
|
||||
("is total", "**total** — always terminates, no exceptions"),
|
||||
("is idempotent", "**idempotent** — `f(f(x)) == f(x)`"),
|
||||
("is deterministic", "**deterministic** — no randomness or time dependence"),
|
||||
("is commutative", "**commutative** — `f(a, b) == f(b, a)`"),
|
||||
];
|
||||
|
||||
let mut found = Vec::new();
|
||||
for (keyword, description) in &properties {
|
||||
if sig.contains(keyword) {
|
||||
found.push(*description);
|
||||
}
|
||||
}
|
||||
|
||||
if found.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("\n\n{}", found.join(" \n"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect parameter name hints at call sites
|
||||
fn collect_call_site_hints(
|
||||
source: &str,
|
||||
expr: &crate::ast::Expr,
|
||||
param_names: &HashMap<String, Vec<String>>,
|
||||
hints: &mut Vec<InlayHint>,
|
||||
) {
|
||||
use crate::ast::Expr;
|
||||
match expr {
|
||||
Expr::Call { func, args, .. } => {
|
||||
// Get the function name for parameter lookup
|
||||
let func_name = match func.as_ref() {
|
||||
Expr::Var(ident) => Some(ident.name.clone()),
|
||||
// Module.method calls like List.map
|
||||
Expr::Field { object, field, .. } => {
|
||||
if let Expr::Var(_) = object.as_ref() {
|
||||
Some(field.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(name) = func_name {
|
||||
if let Some(names) = param_names.get(&name) {
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
if let Some(param_name) = names.get(i) {
|
||||
// Skip hint if the argument is already a variable with the same name
|
||||
if let Expr::Var(ident) = arg {
|
||||
if &ident.name == param_name {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Skip hints for single-arg functions (obvious)
|
||||
if args.len() <= 1 {
|
||||
continue;
|
||||
}
|
||||
let pos = offset_to_position(source, arg.span().start);
|
||||
hints.push(InlayHint {
|
||||
position: pos,
|
||||
label: InlayHintLabel::String(format!("{}:", param_name)),
|
||||
kind: Some(InlayHintKind::PARAMETER),
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: Some(false),
|
||||
padding_right: Some(true),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into function expression and arguments
|
||||
collect_call_site_hints(source, func, param_names, hints);
|
||||
for arg in args {
|
||||
collect_call_site_hints(source, arg, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
collect_call_site_hints(source, left, param_names, hints);
|
||||
collect_call_site_hints(source, right, param_names, hints);
|
||||
}
|
||||
Expr::UnaryOp { operand, .. } => {
|
||||
collect_call_site_hints(source, operand, param_names, hints);
|
||||
}
|
||||
Expr::If { condition, then_branch, else_branch, .. } => {
|
||||
collect_call_site_hints(source, condition, param_names, hints);
|
||||
collect_call_site_hints(source, then_branch, param_names, hints);
|
||||
collect_call_site_hints(source, else_branch, param_names, hints);
|
||||
}
|
||||
Expr::Let { value, body, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
collect_call_site_hints(source, body, param_names, hints);
|
||||
}
|
||||
Expr::Block { statements, result, .. } => {
|
||||
for stmt in statements {
|
||||
match stmt {
|
||||
crate::ast::Statement::Expr(e) => {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
crate::ast::Statement::Let { value, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
}
|
||||
}
|
||||
}
|
||||
collect_call_site_hints(source, result, param_names, hints);
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
collect_call_site_hints(source, scrutinee, param_names, hints);
|
||||
for arm in arms {
|
||||
collect_call_site_hints(source, &arm.body, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Lambda { body, .. } => {
|
||||
collect_call_site_hints(source, body, param_names, hints);
|
||||
}
|
||||
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
|
||||
for e in elements {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Record { fields, .. } => {
|
||||
for (_, e) in fields {
|
||||
collect_call_site_hints(source, e, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Field { object, .. } => {
|
||||
collect_call_site_hints(source, object, param_names, hints);
|
||||
}
|
||||
Expr::Run { expr, handlers, .. } => {
|
||||
collect_call_site_hints(source, expr, param_names, hints);
|
||||
for (_, handler_expr) in handlers {
|
||||
collect_call_site_hints(source, handler_expr, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Resume { value, .. } => {
|
||||
collect_call_site_hints(source, value, param_names, hints);
|
||||
}
|
||||
Expr::EffectOp { args, .. } => {
|
||||
for arg in args {
|
||||
collect_call_site_hints(source, arg, param_names, hints);
|
||||
}
|
||||
}
|
||||
Expr::Literal { .. } | Expr::Var(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn span_to_range(source: &str, start: usize, end: usize) -> Range {
|
||||
let start_pos = offset_to_position(source, start);
|
||||
let end_pos = offset_to_position(source, end);
|
||||
|
||||
@@ -263,7 +263,21 @@ impl SymbolTable {
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "))
|
||||
};
|
||||
let type_sig = format!("fn {}({}): {}{}", f.name.name, param_types.join(", "), return_type, effects);
|
||||
let properties = if f.properties.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" is {}", f.properties.iter()
|
||||
.map(|p| match p {
|
||||
crate::ast::BehavioralProperty::Pure => "pure",
|
||||
crate::ast::BehavioralProperty::Total => "total",
|
||||
crate::ast::BehavioralProperty::Idempotent => "idempotent",
|
||||
crate::ast::BehavioralProperty::Deterministic => "deterministic",
|
||||
crate::ast::BehavioralProperty::Commutative => "commutative",
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "))
|
||||
};
|
||||
let type_sig = format!("fn {}({}): {}{}{}", f.name.name, param_types.join(", "), return_type, properties, effects);
|
||||
|
||||
let symbol = self.new_symbol(
|
||||
f.name.name.clone(),
|
||||
|
||||
@@ -759,6 +759,17 @@ impl TypeChecker {
|
||||
self.env.bindings.get(name)
|
||||
}
|
||||
|
||||
/// Get the inferred type of a binding as a display string (for LSP inlay hints)
|
||||
pub fn get_inferred_type(&self, name: &str) -> Option<String> {
|
||||
let scheme = self.env.bindings.get(name)?;
|
||||
let type_str = scheme.typ.to_string();
|
||||
// Skip unhelpful types
|
||||
if type_str == "<error>" || type_str.contains('?') {
|
||||
return None;
|
||||
}
|
||||
Some(type_str)
|
||||
}
|
||||
|
||||
/// Get auto-generated migrations from type checking
|
||||
/// Returns: type_name -> from_version -> migration_body
|
||||
pub fn get_auto_migrations(&self) -> &HashMap<String, HashMap<u32, Expr>> {
|
||||
|
||||
Reference in New Issue
Block a user