Skip to content

Commit d2a5bfe

Browse files
Jonathan D.A. Jewellclaude
andcommitted
feat(lsp): implement Phase 2 LSP features (completion, hover, definition, diagnostics, stdlib)
Implemented all Phase 2 LSP features in priority order 1,2,3,5,4: 1. Completion Handler (completion.rs) - 40+ WokeLang keywords (to, give, remember, when, etc.) - 8 stdlib modules (math, string, array, io, json, time, net, chan) - Local symbol completion from TypeEnv - Stdlib function completion after "std.module." prefix - Smart context detection for keyword vs module vs function completion 2. Hover Handler (hover.rs) - Markdown-formatted keyword documentation with syntax examples - Type information from TypeChecker integration - Covers all WokeLang keywords with usage examples 3. Go-to-Definition (definition.rs) - AST traversal for function, constant, and type definitions - Recursive statement search for variable declarations - Handles nested scopes (conditionals, loops, consent blocks) - Fixed struct field access for Statement variants 4. Enhanced Diagnostics (diagnostics.rs) - Error pipeline: Lexer → Parser → Linter - Converts all error types to LSP Diagnostic format - Real-time error reporting on document changes 5. Stdlib Metadata (stdlib_metadata.rs) - Complete function signatures for all 8 stdlib modules - Math: abs, sqrt, pow, sin, cos, tan, floor, ceil, round, min, max - String: upper, lower, trim, concat, length, split, join, substring, contains, replace - Array: map, filter, fold, length, push, pop, head, tail, reverse, sort - IO: readFile, writeFile, appendFile, fileExists, deleteFile - JSON: parse, stringify - Time: now, sleep, format - Net: httpGet, httpPost - Chan: make, send, recv, tryRecv, close TypeChecker Integration: - Made TypeEnv::bindings public for symbol lookup - Added get_all_symbols() and get_symbol_type() API methods - Proper integration with LSP handlers Fixes: - Corrected Statement variant struct field access (VarDecl, Conditional, Loop, etc.) - Removed unused imports (Range, Expr, FunctionDef) - All features compile and build successfully Phase 2 Complete: LSP now provides rich IDE features including auto-completion, hover documentation, go-to-definition, and real-time diagnostics. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 580b858 commit d2a5bfe

7 files changed

Lines changed: 559 additions & 29 deletions

File tree

src/lsp/document.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
//! Document state management and caching
44
55
use once_cell::sync::OnceCell;
6-
use std::ops::Range;
76
use tower_lsp::lsp_types::Url;
87

98
use crate::ast::Program;

src/lsp/handlers/completion.rs

Lines changed: 125 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,134 @@
55
use tower_lsp::jsonrpc::Result;
66
use tower_lsp::lsp_types::*;
77

8+
use crate::lsp::stdlib_metadata::get_stdlib_completions;
89
use crate::lsp::Backend;
910

11+
/// WokeLang keywords for completion
12+
const KEYWORDS: &[&str] = &[
13+
"to",
14+
"give",
15+
"back",
16+
"remember",
17+
"when",
18+
"is",
19+
"otherwise",
20+
"repeat",
21+
"times",
22+
"only",
23+
"if",
24+
"okay",
25+
"attempt",
26+
"safely",
27+
"handle",
28+
"reassure",
29+
"complain",
30+
"type",
31+
"use",
32+
"renamed",
33+
"as",
34+
"hello",
35+
"goodbye",
36+
"worker",
37+
"side",
38+
"quest",
39+
"spawn",
40+
"superpower",
41+
"thanks",
42+
"for",
43+
"each",
44+
"in",
45+
"measured",
46+
"then",
47+
"and",
48+
"or",
49+
"not",
50+
"true",
51+
"false",
52+
];
53+
54+
/// Standard library modules
55+
const STDLIB_MODULES: &[&str] = &[
56+
"math", "string", "array", "io", "json", "time", "net", "chan",
57+
];
58+
1059
/// Handle completion request
1160
pub async fn completion(
12-
_backend: &Backend,
13-
_params: CompletionParams,
61+
backend: &Backend,
62+
params: CompletionParams,
1463
) -> Result<Option<CompletionResponse>> {
15-
// TODO: Implement completion
16-
Ok(None)
64+
let uri = params.text_document_position.text_document.uri;
65+
let position = params.text_document_position.position;
66+
67+
// Get document
68+
let doc = match backend.document_map.get(&uri) {
69+
Some(d) => d,
70+
None => return Ok(None),
71+
};
72+
73+
// Get word at cursor
74+
let word = doc
75+
.word_at_position(position.line, position.character)
76+
.unwrap_or_default();
77+
78+
let mut items = Vec::new();
79+
80+
// Check if we're after "std.module." for function completion
81+
if word.starts_with("std.") && word.matches('.').count() >= 2 {
82+
// Extract module name (e.g., "std.math." -> "math")
83+
let parts: Vec<&str> = word.split('.').collect();
84+
if parts.len() >= 2 {
85+
let module = parts[1];
86+
items.extend(get_stdlib_completions(module));
87+
}
88+
}
89+
// Check if we're after "std." for module completion
90+
else if word.starts_with("std.") || word == "std" {
91+
// Complete stdlib modules
92+
for module in STDLIB_MODULES {
93+
items.push(CompletionItem {
94+
label: module.to_string(),
95+
kind: Some(CompletionItemKind::MODULE),
96+
detail: Some(format!("Standard library module: {}", module)),
97+
documentation: Some(Documentation::String(format!(
98+
"WokeLang standard library module for {} operations",
99+
module
100+
))),
101+
sort_text: Some(format!("0_{}", module)),
102+
..Default::default()
103+
});
104+
}
105+
} else {
106+
// Add keywords
107+
for keyword in KEYWORDS {
108+
items.push(CompletionItem {
109+
label: keyword.to_string(),
110+
kind: Some(CompletionItemKind::KEYWORD),
111+
detail: Some("Keyword".to_string()),
112+
sort_text: Some(format!("1_{}", keyword)),
113+
..Default::default()
114+
});
115+
}
116+
117+
// Add local symbols from type environment
118+
if let Some(type_env) = doc.type_env() {
119+
for (name, ty) in &type_env.bindings {
120+
let kind = match ty {
121+
crate::typechecker::TypeInfo::Function(_, _) => CompletionItemKind::FUNCTION,
122+
_ => CompletionItemKind::VARIABLE,
123+
};
124+
125+
items.push(CompletionItem {
126+
label: name.clone(),
127+
kind: Some(kind),
128+
detail: Some(format!("{}", ty)),
129+
documentation: Some(Documentation::String(format!("Type: {}", ty))),
130+
sort_text: Some(format!("2_{}", name)),
131+
..Default::default()
132+
});
133+
}
134+
}
135+
}
136+
137+
Ok(Some(CompletionResponse::Array(items)))
17138
}

src/lsp/handlers/definition.rs

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,122 @@
55
use tower_lsp::jsonrpc::Result;
66
use tower_lsp::lsp_types::*;
77

8+
use crate::ast::{Statement, TopLevelItem};
9+
use crate::lsp::utils::span_to_range;
810
use crate::lsp::Backend;
911

1012
/// Handle go-to-definition request
1113
pub async fn goto_definition(
12-
_backend: &Backend,
13-
_params: GotoDefinitionParams,
14+
backend: &Backend,
15+
params: GotoDefinitionParams,
1416
) -> Result<Option<GotoDefinitionResponse>> {
15-
// TODO: Implement go-to-definition
17+
let uri = params.text_document_position_params.text_document.uri;
18+
let position = params.text_document_position_params.position;
19+
20+
// Get document
21+
let doc = match backend.document_map.get(&uri) {
22+
Some(d) => d,
23+
None => return Ok(None),
24+
};
25+
26+
// Get word at cursor
27+
let word = match doc.word_at_position(position.line, position.character) {
28+
Some(w) => w,
29+
None => return Ok(None),
30+
};
31+
32+
// Get AST
33+
let ast = match doc.ast() {
34+
Ok(a) => a,
35+
Err(_) => return Ok(None),
36+
};
37+
38+
// Search for definition
39+
for item in &ast.items {
40+
match item {
41+
TopLevelItem::Function(func) => {
42+
// Check if this is the function we're looking for
43+
if func.name == word {
44+
let range = span_to_range(&func.span, &doc.source);
45+
return Ok(Some(GotoDefinitionResponse::Scalar(Location {
46+
uri: uri.clone(),
47+
range,
48+
})));
49+
}
50+
51+
// Check function body for variable declarations
52+
if let Some(location) = search_statements(&func.body, &word, &doc.source, &uri) {
53+
return Ok(Some(GotoDefinitionResponse::Scalar(location)));
54+
}
55+
}
56+
TopLevelItem::ConstDef(const_def) => {
57+
if const_def.name == word {
58+
let range = span_to_range(&const_def.span, &doc.source);
59+
return Ok(Some(GotoDefinitionResponse::Scalar(Location {
60+
uri: uri.clone(),
61+
range,
62+
})));
63+
}
64+
}
65+
TopLevelItem::TypeDef(type_def) => {
66+
if type_def.name == word {
67+
let range = span_to_range(&type_def.span, &doc.source);
68+
return Ok(Some(GotoDefinitionResponse::Scalar(Location {
69+
uri: uri.clone(),
70+
range,
71+
})));
72+
}
73+
}
74+
_ => {}
75+
}
76+
}
77+
1678
Ok(None)
1779
}
80+
81+
/// Search statements for variable declarations
82+
fn search_statements(
83+
statements: &[Statement],
84+
target: &str,
85+
source: &str,
86+
uri: &Url,
87+
) -> Option<Location> {
88+
for stmt in statements {
89+
match stmt {
90+
Statement::VarDecl(var_decl) if var_decl.name == *target => {
91+
let range = span_to_range(&var_decl.span, source);
92+
return Some(Location {
93+
uri: uri.clone(),
94+
range,
95+
});
96+
}
97+
Statement::Conditional(cond) => {
98+
if let Some(loc) = search_statements(&cond.then_branch, target, source, uri) {
99+
return Some(loc);
100+
}
101+
if let Some(ref else_stmts) = cond.else_branch {
102+
if let Some(loc) = search_statements(else_stmts, target, source, uri) {
103+
return Some(loc);
104+
}
105+
}
106+
}
107+
Statement::Loop(loop_stmt) => {
108+
if let Some(loc) = search_statements(&loop_stmt.body, target, source, uri) {
109+
return Some(loc);
110+
}
111+
}
112+
Statement::AttemptBlock(attempt) => {
113+
if let Some(loc) = search_statements(&attempt.body, target, source, uri) {
114+
return Some(loc);
115+
}
116+
}
117+
Statement::ConsentBlock(consent) => {
118+
if let Some(loc) = search_statements(&consent.body, target, source, uri) {
119+
return Some(loc);
120+
}
121+
}
122+
_ => {}
123+
}
124+
}
125+
None
126+
}

src/lsp/handlers/diagnostics.rs

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
55
use tower_lsp::lsp_types::*;
66

7+
use crate::linter::{Linter, Severity};
8+
use crate::lsp::utils::span_to_range;
79
use crate::lsp::Backend;
810

911
/// Publish diagnostics for a document
@@ -34,7 +36,7 @@ fn collect_diagnostics(backend: &Backend, uri: &Url) -> Vec<Diagnostic> {
3436
end: Position::new(0, 1),
3537
},
3638
severity: Some(DiagnosticSeverity::ERROR),
37-
code: None,
39+
code: Some(NumberOrString::String("lex-error".to_string())),
3840
source: Some("wokelang-lexer".to_string()),
3941
message: err.clone(),
4042
..Default::default()
@@ -43,26 +45,48 @@ fn collect_diagnostics(backend: &Backend, uri: &Url) -> Vec<Diagnostic> {
4345
}
4446

4547
// Try to parse
46-
if let Err(err) = doc.ast() {
48+
let ast = match doc.ast() {
49+
Ok(a) => a,
50+
Err(err) => {
51+
diagnostics.push(Diagnostic {
52+
range: Range {
53+
start: Position::new(0, 0),
54+
end: Position::new(0, 1),
55+
},
56+
severity: Some(DiagnosticSeverity::ERROR),
57+
code: Some(NumberOrString::String("parse-error".to_string())),
58+
source: Some("wokelang-parser".to_string()),
59+
message: err.clone(),
60+
..Default::default()
61+
});
62+
return diagnostics; // Can't lint without AST
63+
}
64+
};
65+
66+
// Typecheck (errors are silent, we just want to populate the type env)
67+
let _ = doc.type_env();
68+
69+
// Run linter
70+
let mut linter = Linter::new();
71+
let lint_diagnostics = linter.lint(ast);
72+
73+
// Convert linter diagnostics to LSP diagnostics
74+
for lint_diag in lint_diagnostics {
75+
let severity = match lint_diag.severity {
76+
Severity::Error => DiagnosticSeverity::ERROR,
77+
Severity::Warning => DiagnosticSeverity::WARNING,
78+
Severity::Info => DiagnosticSeverity::INFORMATION,
79+
};
80+
4781
diagnostics.push(Diagnostic {
48-
range: Range {
49-
start: Position::new(0, 0),
50-
end: Position::new(0, 1),
51-
},
52-
severity: Some(DiagnosticSeverity::ERROR),
53-
code: None,
54-
source: Some("wokelang-parser".to_string()),
55-
message: err.clone(),
82+
range: span_to_range(&lint_diag.span, &doc.source),
83+
severity: Some(severity),
84+
code: Some(NumberOrString::String(lint_diag.code)),
85+
source: Some("wokelang-linter".to_string()),
86+
message: lint_diag.message,
5687
..Default::default()
5788
});
58-
return diagnostics; // Can't lint without AST
5989
}
6090

61-
// Typecheck (if it fails, type_env will be None, but we don't report it as error
62-
// since parse errors are already reported)
63-
let _ = doc.type_env();
64-
65-
// TODO: Add linter diagnostics
66-
6791
diagnostics
6892
}

0 commit comments

Comments
 (0)