Skip to content

Commit 8d8e94a

Browse files
committed
✨ feat(mq-lsp): implement LspError enum and integrate error handling in the server
1 parent 31797f3 commit 8d8e94a

File tree

5 files changed

+158
-60
lines changed

5 files changed

+158
-60
lines changed

crates/mq-check/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use types::TypeScheme;
4141
pub type Result<T> = std::result::Result<T, TypeError>;
4242

4343
/// Type checking errors
44-
#[derive(Debug, Error, Diagnostic)]
44+
#[derive(Debug, Error, Clone, Diagnostic)]
4545
#[allow(unused_assignments)]
4646
pub enum TypeError {
4747
#[error("Type mismatch: expected {expected}, found {found}")]

crates/mq-lsp/src/error.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use tower_lsp_server::ls_types;
2+
3+
pub type SyntaxError = (std::string::String, mq_lang::Range);
4+
5+
#[derive(Debug, Clone)]
6+
pub enum LspError {
7+
SyntaxError(SyntaxError),
8+
TypeError(mq_check::TypeError),
9+
}
10+
11+
impl From<&LspError> for ls_types::Diagnostic {
12+
fn from(error: &LspError) -> Self {
13+
match error {
14+
LspError::SyntaxError((message, range)) => ls_types::Diagnostic::new_simple(
15+
ls_types::Range::new(
16+
ls_types::Position {
17+
line: range.start.line - 1,
18+
character: (range.start.column - 1) as u32,
19+
},
20+
ls_types::Position {
21+
line: range.end.line - 1,
22+
character: (range.end.column - 1) as u32,
23+
},
24+
),
25+
message.to_string(),
26+
),
27+
LspError::TypeError(type_error) => match type_error.location() {
28+
Some((line, column)) => ls_types::Diagnostic::new_simple(
29+
ls_types::Range::new(
30+
ls_types::Position {
31+
line,
32+
character: column as u32,
33+
},
34+
ls_types::Position {
35+
line,
36+
character: column as u32,
37+
},
38+
),
39+
type_error.to_string(),
40+
),
41+
None => ls_types::Diagnostic::new_simple(
42+
ls_types::Range::new(
43+
ls_types::Position { line: 0, character: 0 },
44+
ls_types::Position { line: 0, character: 0 },
45+
),
46+
type_error.to_string(),
47+
),
48+
},
49+
}
50+
}
51+
}

crates/mq-lsp/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
pub mod capabilities;
1919
pub mod completions;
2020
pub mod document_symbol;
21+
pub mod error;
2122
pub mod execute_command;
2223
pub mod goto_definition;
2324
pub mod hover;

crates/mq-lsp/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::server::LspConfig;
77
pub mod capabilities;
88
pub mod completions;
99
pub mod document_symbol;
10+
pub mod error;
1011
pub mod execute_command;
1112
pub mod goto_definition;
1213
pub mod hover;

crates/mq-lsp/src/server.rs

Lines changed: 104 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use dashmap::DashMap;
77
use tower_lsp_server::ls_types::DocumentRangeFormattingParams;
88
use url::Url;
99

10+
use crate::error::LspError;
1011
use crate::{
1112
capabilities, completions, document_symbol, execute_command, goto_definition, hover, references, semantic_tokens,
1213
};
@@ -25,7 +26,7 @@ struct Backend {
2526
client: Client,
2627
hir: Arc<RwLock<mq_hir::Hir>>,
2728
source_map: RwLock<BiMap<String, mq_hir::SourceId>>,
28-
error_map: DashMap<String, Vec<(std::string::String, mq_lang::Range)>>,
29+
error_map: DashMap<String, Vec<LspError>>,
2930
text_map: DashMap<String, Arc<String>>,
3031
config: LspConfig,
3132
}
@@ -230,7 +231,7 @@ impl LanguageServer for Backend {
230231
params: DocumentRangeFormattingParams,
231232
) -> jsonrpc::Result<Option<Vec<ls_types::TextEdit>>> {
232233
if let Some(errors) = self.error_map.get(&params.text_document.uri.to_string())
233-
&& !errors.is_empty()
234+
&& !(*errors).is_empty()
234235
{
235236
return Ok(None);
236237
}
@@ -326,9 +327,21 @@ impl Backend {
326327
let (source_id, _) = self.hir.write().unwrap().add_nodes(uri.clone(), &nodes);
327328

328329
let uri_string = uri.to_string();
330+
let mut errors = errors
331+
.error_ranges(&text)
332+
.into_iter()
333+
.map(|(message, range)| LspError::SyntaxError((message, range)))
334+
.collect::<Vec<_>>();
335+
336+
if errors.is_empty() && self.config.enable_type_checking {
337+
let mut checker = mq_check::TypeChecker::with_options(self.config.type_checker_options);
338+
let type_errors = checker.check(&self.hir.read().unwrap());
339+
errors.extend(type_errors.into_iter().map(LspError::TypeError));
340+
}
341+
329342
self.source_map.write().unwrap().insert(uri_string.clone(), source_id);
330343
self.text_map.insert(uri_string.clone(), text.to_string().into());
331-
self.error_map.insert(uri_string, errors.error_ranges(&text));
344+
self.error_map.insert(uri_string, errors);
332345
}
333346

334347
async fn diagnostics(&self, uri: Url, version: Option<i32>) {
@@ -340,46 +353,8 @@ impl Backend {
340353

341354
// Add parsing errors if they exist
342355
if let Some(errors) = file_errors {
343-
diagnostics.extend(errors.iter().map(|(message, item)| {
344-
ls_types::Diagnostic::new_simple(
345-
ls_types::Range::new(
346-
ls_types::Position {
347-
line: item.start.line - 1,
348-
character: (item.start.column - 1) as u32,
349-
},
350-
ls_types::Position {
351-
line: item.end.line - 1,
352-
character: (item.end.column - 1) as u32,
353-
},
354-
),
355-
message.to_string(),
356-
)
357-
}));
358-
359-
if errors.is_empty() && self.config.enable_type_checking {
360-
let mut checker = mq_check::TypeChecker::with_options(self.config.type_checker_options);
361-
let type_errors = checker.check(&self.hir.read().unwrap());
362-
363-
diagnostics.extend(type_errors.iter().filter_map(|type_error| {
364-
type_error.location().map(|location| {
365-
let mut diagnostic = ls_types::Diagnostic::new_simple(
366-
ls_types::Range::new(
367-
ls_types::Position {
368-
line: location.0,
369-
character: location.1 as u32,
370-
},
371-
ls_types::Position {
372-
line: location.0,
373-
character: location.1 as u32,
374-
},
375-
),
376-
type_error.to_string(),
377-
);
378-
diagnostic.severity = Some(ls_types::DiagnosticSeverity::ERROR);
379-
diagnostic
380-
})
381-
}));
382-
}
356+
let errors: Vec<ls_types::Diagnostic> = (*errors).iter().map(Into::into).collect::<Vec<_>>();
357+
diagnostics.extend(errors);
383358
}
384359

385360
{
@@ -580,7 +555,14 @@ mod tests {
580555

581556
let (_, errors) = mq_lang::parse_recovery(text);
582557
backend.text_map.insert(uri.to_string(), text.to_string().into());
583-
backend.error_map.insert(uri.to_string(), errors.error_ranges(text));
558+
backend.error_map.insert(
559+
uri.to_string(),
560+
errors
561+
.error_ranges(text)
562+
.into_iter()
563+
.map(|(message, range)| LspError::SyntaxError((message, range)))
564+
.collect(),
565+
);
584566

585567
let result = backend
586568
.formatting(ls_types::DocumentFormattingParams {
@@ -931,7 +913,14 @@ mod tests {
931913
let text = "def main(): invalid_syntax";
932914
let (_, errors) = mq_lang::parse_recovery(text);
933915
backend.text_map.insert(uri.to_string(), text.to_string().into());
934-
backend.error_map.insert(uri.to_string(), errors.error_ranges(text));
916+
backend.error_map.insert(
917+
uri.to_string(),
918+
errors
919+
.error_ranges(text)
920+
.into_iter()
921+
.map(|(message, range)| LspError::SyntaxError((message, range)))
922+
.collect(),
923+
);
935924

936925
backend
937926
.source_map
@@ -1032,7 +1021,14 @@ mod tests {
10321021
let text = "def main() 1;"; // Missing colon
10331022
let (_, errors) = mq_lang::parse_recovery(text);
10341023
backend.text_map.insert(uri.to_string(), text.to_string().into());
1035-
backend.error_map.insert(uri.to_string(), errors.error_ranges(text));
1024+
backend.error_map.insert(
1025+
uri.to_string(),
1026+
errors
1027+
.error_ranges(text)
1028+
.into_iter()
1029+
.map(|(message, range)| LspError::SyntaxError((message, range)))
1030+
.collect(),
1031+
);
10361032

10371033
// We can't directly test client.publish_diagnostics was called with correct diagnostics,
10381034
// but we can verify the code doesn't panic
@@ -1060,7 +1056,14 @@ mod tests {
10601056

10611057
backend.source_map.write().unwrap().insert(uri.to_string(), source_id);
10621058
backend.text_map.insert(uri.to_string(), code.to_string().into());
1063-
backend.error_map.insert(uri.to_string(), errors.error_ranges(code));
1059+
backend.error_map.insert(
1060+
uri.to_string(),
1061+
errors
1062+
.error_ranges(code)
1063+
.into_iter()
1064+
.map(|(message, range)| LspError::SyntaxError((message, range)))
1065+
.collect(),
1066+
);
10641067

10651068
// Check unused functions are detected
10661069
{
@@ -1094,13 +1097,13 @@ mod tests {
10941097
backend.text_map.insert(uri.to_string(), text.to_string().into());
10951098
backend.error_map.insert(
10961099
uri.to_string(),
1097-
vec![(
1100+
vec![LspError::SyntaxError((
10981101
"Syntax error".to_string(),
10991102
mq_lang::Range {
11001103
start: mq_lang::Position { line: 1, column: 10 },
11011104
end: mq_lang::Position { line: 1, column: 11 },
11021105
},
1103-
)],
1106+
))],
11041107
);
11051108

11061109
let result = backend
@@ -1139,7 +1142,14 @@ mod tests {
11391142

11401143
backend.source_map.write().unwrap().insert(uri.to_string(), source_id);
11411144
backend.text_map.insert(uri.to_string(), code.to_string().into());
1142-
backend.error_map.insert(uri.to_string(), errors.error_ranges(code));
1145+
backend.error_map.insert(
1146+
uri.to_string(),
1147+
errors
1148+
.error_ranges(code)
1149+
.into_iter()
1150+
.map(|(message, range)| LspError::SyntaxError((message, range)))
1151+
.collect(),
1152+
);
11431153

11441154
// Check unreachable code warnings are detected
11451155
{
@@ -1254,13 +1264,13 @@ mod tests {
12541264
backend.text_map.insert(uri.to_string(), text.to_string().into());
12551265
backend.error_map.insert(
12561266
uri.to_string(),
1257-
vec![(
1267+
vec![LspError::SyntaxError((
12581268
"Syntax error".to_string(),
12591269
mq_lang::Range {
12601270
start: mq_lang::Position { line: 0, column: 0 },
12611271
end: mq_lang::Position { line: 0, column: 10 },
12621272
},
1263-
)],
1273+
))],
12641274
);
12651275

12661276
let params = DocumentRangeFormattingParams {
@@ -1327,7 +1337,14 @@ mod tests {
13271337

13281338
backend.source_map.write().unwrap().insert(uri.to_string(), source_id);
13291339
backend.text_map.insert(uri.to_string(), code.to_string().into());
1330-
backend.error_map.insert(uri.to_string(), errors.error_ranges(code));
1340+
backend.error_map.insert(
1341+
uri.to_string(),
1342+
errors
1343+
.error_ranges(code)
1344+
.into_iter()
1345+
.map(|(message, range)| LspError::SyntaxError((message, range)))
1346+
.collect(),
1347+
);
13311348

13321349
// No type errors should be emitted since type checking is disabled
13331350
// (diagnostics() calls publish_diagnostics internally, we just verify it doesn't panic)
@@ -1355,7 +1372,14 @@ mod tests {
13551372

13561373
backend.source_map.write().unwrap().insert(uri.to_string(), source_id);
13571374
backend.text_map.insert(uri.to_string(), code.to_string().into());
1358-
backend.error_map.insert(uri.to_string(), errors.error_ranges(code));
1375+
backend.error_map.insert(
1376+
uri.to_string(),
1377+
errors
1378+
.error_ranges(code)
1379+
.into_iter()
1380+
.map(|(message, range)| LspError::SyntaxError((message, range)))
1381+
.collect(),
1382+
);
13591383

13601384
assert!(backend.error_map.get(&uri.to_string()).unwrap().is_empty());
13611385

@@ -1384,7 +1408,14 @@ mod tests {
13841408

13851409
backend.source_map.write().unwrap().insert(uri.to_string(), source_id);
13861410
backend.text_map.insert(uri.to_string(), code.to_string().into());
1387-
backend.error_map.insert(uri.to_string(), errors.error_ranges(code));
1411+
backend.error_map.insert(
1412+
uri.to_string(),
1413+
errors
1414+
.error_ranges(code)
1415+
.into_iter()
1416+
.map(|(message, range)| LspError::SyntaxError((message, range)))
1417+
.collect(),
1418+
);
13881419

13891420
// Parse errors should be empty so type checking runs
13901421
assert!(backend.error_map.get(&uri.to_string()).unwrap().is_empty());
@@ -1417,13 +1448,13 @@ mod tests {
14171448
backend.text_map.insert(uri.to_string(), text.to_string().into());
14181449
backend.error_map.insert(
14191450
uri.to_string(),
1420-
vec![(
1451+
vec![LspError::SyntaxError((
14211452
"Syntax error".to_string(),
14221453
mq_lang::Range {
14231454
start: mq_lang::Position { line: 1, column: 1 },
14241455
end: mq_lang::Position { line: 1, column: 5 },
14251456
},
1426-
)],
1457+
))],
14271458
);
14281459

14291460
// Parse errors are present, so type checking should be skipped
@@ -1461,7 +1492,14 @@ mod tests {
14611492

14621493
backend.source_map.write().unwrap().insert(uri.to_string(), source_id);
14631494
backend.text_map.insert(uri.to_string(), code.to_string().into());
1464-
backend.error_map.insert(uri.to_string(), errors.error_ranges(code));
1495+
backend.error_map.insert(
1496+
uri.to_string(),
1497+
errors
1498+
.error_ranges(code)
1499+
.into_iter()
1500+
.map(|(message, range)| LspError::SyntaxError((message, range)))
1501+
.collect(),
1502+
);
14651503

14661504
assert!(backend.error_map.get(&uri.to_string()).unwrap().is_empty());
14671505

@@ -1496,7 +1534,14 @@ mod tests {
14961534

14971535
backend.source_map.write().unwrap().insert(uri.to_string(), source_id);
14981536
backend.text_map.insert(uri.to_string(), code.to_string().into());
1499-
backend.error_map.insert(uri.to_string(), errors.error_ranges(code));
1537+
backend.error_map.insert(
1538+
uri.to_string(),
1539+
errors
1540+
.error_ranges(code)
1541+
.into_iter()
1542+
.map(|(message, range)| LspError::SyntaxError((message, range)))
1543+
.collect(),
1544+
);
15001545

15011546
assert!(backend.error_map.get(&uri.to_string()).unwrap().is_empty());
15021547

0 commit comments

Comments
 (0)