Skip to content

Commit 7f41937

Browse files
Merge pull request #190 from mimium-org/feat/completion
Feature: Completion and Signature Provider for Language Server
2 parents 3f2a42f + b914146 commit 7f41937

File tree

2 files changed

+503
-2
lines changed

2 files changed

+503
-2
lines changed

crates/bin/mimium-language-server/src/analysis.rs

Lines changed: 248 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
use mimium_lang::ast::Expr;
2+
use mimium_lang::ast::program::VisibilityMap;
13
use mimium_lang::compiler::mirgen;
2-
use mimium_lang::interner::{Symbol, TypeNodeId};
4+
use mimium_lang::interner::{ExprNodeId, Symbol, TypeNodeId};
5+
use mimium_lang::pattern::Pattern;
6+
use mimium_lang::types::Type;
37
use mimium_lang::utils::error::ReportableError;
48
use ropey::Rope;
59
use tower_lsp::lsp_types::{
@@ -14,10 +18,44 @@ pub struct AnalysisRequest {
1418
pub text: String,
1519
}
1620

21+
/// Parameter information for signature help.
22+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
23+
pub struct ParamInfo {
24+
pub name: String,
25+
pub type_str: String,
26+
pub has_default: bool,
27+
}
28+
29+
/// Signature information for a function definition.
30+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
31+
pub struct FnSignature {
32+
pub name: String,
33+
pub params: Vec<ParamInfo>,
34+
pub return_type: String,
35+
}
36+
37+
/// Kind of a completion symbol.
38+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
39+
pub enum CompletionKind {
40+
Function,
41+
Variable,
42+
}
43+
44+
/// A single symbol available for completion.
45+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46+
pub struct CompletionSymbol {
47+
pub name: String,
48+
pub kind: CompletionKind,
49+
/// Human-readable type annotation, empty when unknown.
50+
pub type_str: String,
51+
}
52+
1753
#[derive(Debug, serde::Serialize, serde::Deserialize)]
1854
pub struct AnalysisResponse {
1955
pub diagnostics: Vec<Diagnostic>,
2056
pub semantic_tokens: Vec<ImCompleteSemanticToken>,
57+
pub fn_signatures: Vec<FnSignature>,
58+
pub completion_symbols: Vec<CompletionSymbol>,
2159
}
2260

2361
pub fn analyze_source(
@@ -42,6 +80,9 @@ pub fn analyze_source(
4280
module_info,
4381
} = parse(src, url.as_str());
4482

83+
// Save visibility info before module_info is consumed by typecheck.
84+
let visibility_map = module_info.visibility_map.clone();
85+
4586
let errs = {
4687
let ast = ast.wrap_to_staged_expr();
4788
let (_, _, typeerrs) =
@@ -54,9 +95,215 @@ pub fn analyze_source(
5495
.flat_map(|item| diagnostic_from_error(item, url.clone(), &rope))
5596
.collect::<Vec<Diagnostic>>();
5697

98+
let fn_signatures = collect_fn_signatures(ast, &visibility_map);
99+
let completion_symbols = collect_completion_symbols(ast, &visibility_map);
100+
57101
AnalysisResponse {
58102
diagnostics,
59103
semantic_tokens,
104+
fn_signatures,
105+
completion_symbols,
106+
}
107+
}
108+
109+
/// Format a type for display in signature help.
110+
/// Returns an empty string for unknown types.
111+
fn format_type_for_display(ty: &Type) -> String {
112+
match ty {
113+
Type::Unknown => String::new(),
114+
other => format!("{other}"),
115+
}
116+
}
117+
118+
/// Returns `true` if the symbol with the given mangled name should be included
119+
/// in completions/signatures.
120+
///
121+
/// Top-level symbols (no `$` in mangled name) are always visible because they
122+
/// live in the same file scope. Module members (contain `$`) are only visible
123+
/// when declared `pub`.
124+
fn is_visible(mangled: &str, visibility_map: &VisibilityMap) -> bool {
125+
if !mangled.contains('$') {
126+
// Top-level: always available within the same file.
127+
return true;
128+
}
129+
// Module member: only public ones are accessible outside the module.
130+
let sym = mimium_lang::interner::ToSymbol::to_symbol(&mangled);
131+
visibility_map.get(&sym).copied().unwrap_or(false)
132+
}
133+
134+
/// Convert an internally mangled name (`mod$fn`) to mimium source syntax (`mod::fn`).
135+
fn demangle_name(name: &str) -> String {
136+
name.replace('$', "::")
137+
}
138+
139+
/// Walk the AST and collect all function signatures.
140+
fn collect_fn_signatures(root: ExprNodeId, visibility_map: &VisibilityMap) -> Vec<FnSignature> {
141+
let mut sigs = Vec::new();
142+
collect_fn_signatures_rec(root, visibility_map, &mut sigs);
143+
// Deduplicate by name, keeping the first occurrence.
144+
let mut seen = std::collections::HashSet::new();
145+
sigs.retain(|s| seen.insert(s.name.clone()));
146+
sigs
147+
}
148+
149+
/// Recursively walk the AST to find function definitions (LetRec with Lambda).
150+
fn collect_fn_signatures_rec(
151+
expr_id: ExprNodeId,
152+
visibility_map: &VisibilityMap,
153+
sigs: &mut Vec<FnSignature>,
154+
) {
155+
match expr_id.to_expr() {
156+
Expr::LetRec(typed_id, value, next) => {
157+
if let Expr::Lambda(params, ret_ty, _body) = value.to_expr() {
158+
if is_visible(typed_id.id.as_str(), visibility_map) {
159+
let name = demangle_name(typed_id.id.as_str());
160+
let params_info = params
161+
.iter()
162+
.map(|p| {
163+
let ty = p.ty.to_type();
164+
ParamInfo {
165+
name: p.id.as_str().to_string(),
166+
type_str: format_type_for_display(&ty),
167+
has_default: p.default_value.is_some(),
168+
}
169+
})
170+
.collect();
171+
let return_type = ret_ty
172+
.map(|r| format_type_for_display(&r.to_type()))
173+
.unwrap_or_default();
174+
sigs.push(FnSignature {
175+
name,
176+
params: params_info,
177+
return_type,
178+
});
179+
}
180+
}
181+
if let Some(next) = next {
182+
collect_fn_signatures_rec(next, visibility_map, sigs);
183+
}
184+
}
185+
Expr::Let(_pat, _value, Some(next)) => {
186+
collect_fn_signatures_rec(next, visibility_map, sigs);
187+
}
188+
Expr::Then(e1, e2) => {
189+
collect_fn_signatures_rec(e1, visibility_map, sigs);
190+
if let Some(e2) = e2 {
191+
collect_fn_signatures_rec(e2, visibility_map, sigs);
192+
}
193+
}
194+
Expr::Bracket(inner) => {
195+
collect_fn_signatures_rec(inner, visibility_map, sigs);
196+
}
197+
Expr::Escape(inner) => {
198+
collect_fn_signatures_rec(inner, visibility_map, sigs);
199+
}
200+
_ => {}
201+
}
202+
}
203+
204+
/// Walk the AST and collect all symbols visible at the top level or as locals.
205+
fn collect_completion_symbols(
206+
root: ExprNodeId,
207+
visibility_map: &VisibilityMap,
208+
) -> Vec<CompletionSymbol> {
209+
let mut syms = Vec::new();
210+
collect_completion_symbols_rec(root, visibility_map, &mut syms);
211+
// Deduplicate by name, keeping the first occurrence.
212+
let mut seen = std::collections::HashSet::new();
213+
syms.retain(|s| seen.insert(s.name.clone()));
214+
syms
215+
}
216+
217+
/// Recursively walk the AST collecting bindings.
218+
///
219+
/// Collects:
220+
/// - `LetRec` bindings: functions (Lambda) and plain values
221+
/// - `Let` bindings: variable patterns (including tuple destructuring)
222+
/// - Lambda parameter names from function bodies
223+
fn collect_completion_symbols_rec(
224+
expr_id: ExprNodeId,
225+
visibility_map: &VisibilityMap,
226+
syms: &mut Vec<CompletionSymbol>,
227+
) {
228+
match expr_id.to_expr() {
229+
Expr::LetRec(typed_id, value, next) => {
230+
let type_str = format_type_for_display(&typed_id.ty.to_type());
231+
let (kind, type_str) = if let Expr::Lambda(_params, ret_ty, body) = value.to_expr() {
232+
// Do not collect lambda parameters here: they are local to the
233+
// function body and not visible in the surrounding scope.
234+
// Recurse into the body to pick up any nested let-bindings.
235+
collect_completion_symbols_rec(body, visibility_map, syms);
236+
let ret = ret_ty
237+
.map(|r| format_type_for_display(&r.to_type()))
238+
.unwrap_or_default();
239+
(CompletionKind::Function, ret)
240+
} else {
241+
(CompletionKind::Variable, type_str)
242+
};
243+
if is_visible(typed_id.id.as_str(), visibility_map) {
244+
syms.push(CompletionSymbol {
245+
name: demangle_name(typed_id.id.as_str()),
246+
kind,
247+
type_str,
248+
});
249+
}
250+
if let Some(next) = next {
251+
collect_completion_symbols_rec(next, visibility_map, syms);
252+
}
253+
}
254+
Expr::Let(typed_pattern, _value, next) => {
255+
collect_pattern_symbols(
256+
&typed_pattern.pat,
257+
&format_type_for_display(&typed_pattern.ty.to_type()),
258+
syms,
259+
);
260+
if let Some(next) = next {
261+
collect_completion_symbols_rec(next, visibility_map, syms);
262+
}
263+
}
264+
Expr::Then(e1, e2) => {
265+
collect_completion_symbols_rec(e1, visibility_map, syms);
266+
if let Some(e2) = e2 {
267+
collect_completion_symbols_rec(e2, visibility_map, syms);
268+
}
269+
}
270+
Expr::Bracket(inner) | Expr::Escape(inner) => {
271+
collect_completion_symbols_rec(inner, visibility_map, syms);
272+
}
273+
_ => {}
274+
}
275+
}
276+
277+
/// Recursively extract bound names from a pattern.
278+
fn collect_pattern_symbols(pattern: &Pattern, type_str: &str, syms: &mut Vec<CompletionSymbol>) {
279+
match pattern {
280+
Pattern::Single(sym) => {
281+
syms.push(CompletionSymbol {
282+
name: sym.as_str().to_string(),
283+
kind: CompletionKind::Variable,
284+
type_str: type_str.to_string(),
285+
});
286+
}
287+
Pattern::Tuple(pats) => {
288+
for p in pats {
289+
collect_pattern_symbols(p, "", syms);
290+
}
291+
}
292+
Pattern::Record(fields) => {
293+
for (name, pat) in fields {
294+
// Prefer the field name as label; recurse if it binds further.
295+
collect_pattern_symbols(pat, "", syms);
296+
// If the sub-pattern is just Placeholder, still expose the field name.
297+
if matches!(pat, Pattern::Placeholder) {
298+
syms.push(CompletionSymbol {
299+
name: name.as_str().to_string(),
300+
kind: CompletionKind::Variable,
301+
type_str: String::new(),
302+
});
303+
}
304+
}
305+
}
306+
Pattern::Placeholder | Pattern::Error => {}
60307
}
61308
}
62309

0 commit comments

Comments
 (0)