1mod errors;
2mod type_hint_collector;
3mod utils;
4mod variable_declaration_finder;
5use crate::ast::expressions::{
6 DatexExpressionData, VariableAccess, VariableAssignment,
7 VariableDeclaration,
8};
9use crate::collections::HashMap;
10use crate::compiler::precompiler::precompiled_ast::RichAst;
11use crate::compiler::workspace::CompilerWorkspace;
12use crate::lsp::errors::SpannedLSPCompilerError;
13use crate::lsp::variable_declaration_finder::VariableDeclarationFinder;
14use crate::runtime::Runtime;
15use crate::stdlib::borrow::Cow;
16use crate::stdlib::cell::RefCell;
17use crate::values::core_values::r#type::Type;
18use crate::visitor::expression::ExpressionVisitor;
19use realhydroper_lsp::jsonrpc::{Error, ErrorCode};
20use realhydroper_lsp::{Client, LanguageServer, Server};
21use realhydroper_lsp::{LspService, lsp_types::*};
22
23#[cfg(feature = "lsp_wasm")]
24use futures::io::{AsyncRead, AsyncWrite};
25#[cfg(not(feature = "lsp_wasm"))]
26use tokio::io::{AsyncRead, AsyncWrite};
27
28pub struct LanguageServerBackend {
29 pub client: Client,
30 pub compiler_workspace: RefCell<CompilerWorkspace>,
31 pub spanned_compiler_errors:
32 RefCell<HashMap<Url, Vec<SpannedLSPCompilerError>>>,
33}
34
35impl LanguageServerBackend {
36 pub fn new(client: Client, compiler_workspace: CompilerWorkspace) -> Self {
37 Self {
38 client,
39 compiler_workspace: RefCell::new(compiler_workspace),
40 spanned_compiler_errors: RefCell::new(HashMap::new()),
41 }
42 }
43}
44
45#[realhydroper_lsp::async_trait(?Send)]
46impl LanguageServer for LanguageServerBackend {
47 async fn initialize(
48 &self,
49 _: InitializeParams,
50 ) -> realhydroper_lsp::jsonrpc::Result<InitializeResult> {
51 Ok(InitializeResult {
52 capabilities: ServerCapabilities {
53 hover_provider: Some(HoverProviderCapability::Simple(true)),
54 completion_provider: Some(CompletionOptions::default()),
55 text_document_sync: Some(TextDocumentSyncCapability::Kind(
56 TextDocumentSyncKind::FULL,
57 )),
58 diagnostic_provider: Some(
59 DiagnosticServerCapabilities::Options(DiagnosticOptions {
60 inter_file_dependencies: true,
61 workspace_diagnostics: false,
62 identifier: None,
63 work_done_progress_options: WorkDoneProgressOptions {
64 work_done_progress: None,
65 },
66 }),
67 ),
68 inlay_hint_provider: Some(OneOf::Left(true)),
69 document_link_provider: Some(DocumentLinkOptions {
70 resolve_provider: Some(true),
71 work_done_progress_options: Default::default(),
72 }),
73 definition_provider: Some(OneOf::Left(true)),
74 ..Default::default()
75 },
76 ..Default::default()
77 })
78 }
79
80 async fn initialized(&self, _: InitializedParams) {
81 self.client
82 .log_message(MessageType::INFO, "server initialized!")
83 .await;
84 }
85
86 async fn shutdown(&self) -> realhydroper_lsp::jsonrpc::Result<()> {
87 Ok(())
88 }
89
90 async fn did_open(&self, params: DidOpenTextDocumentParams) {
91 self.client
92 .log_message(
93 MessageType::INFO,
94 format!("File opened: {}", params.text_document.uri),
95 )
96 .await;
97
98 self.update_file_contents(
99 params.text_document.uri,
100 params.text_document.text,
101 )
102 .await;
103 }
104
105 async fn did_change(&self, params: DidChangeTextDocumentParams) {
106 self.client
107 .log_message(
108 MessageType::INFO,
109 format!("File changed: {}", params.text_document.uri),
110 )
111 .await;
112 let new_content = params
113 .content_changes
114 .into_iter()
115 .next()
116 .map(|change| change.text)
117 .unwrap_or_default();
118 self.update_file_contents(params.text_document.uri, new_content)
119 .await;
120 }
121
122 async fn completion(
123 &self,
124 params: CompletionParams,
125 ) -> realhydroper_lsp::jsonrpc::Result<Option<CompletionResponse>> {
126 self.client
127 .log_message(MessageType::INFO, "completion!")
128 .await;
129
130 let position = params.text_document_position;
131
132 let prefix = self.get_previous_text_at_position(&position);
135 self.client
136 .log_message(
137 MessageType::INFO,
138 format!("Completion prefix: {}", prefix),
139 )
140 .await;
141
142 let variables = self.find_variable_starting_with(&prefix);
143
144 let items: Vec<CompletionItem> = variables
145 .iter()
146 .map(|var| CompletionItem {
147 label: var.name.clone(),
148 kind: Some(CompletionItemKind::VARIABLE),
149 detail: Some(format!(
150 "{} {}: {}",
151 var.shape,
152 var.name,
153 var.var_type.as_ref().unwrap()
154 )),
155 documentation: None,
156 ..Default::default()
157 })
158 .collect();
159
160 Ok(Some(CompletionResponse::Array(items)))
161 }
162
163 async fn hover(
164 &self,
165 params: HoverParams,
166 ) -> realhydroper_lsp::jsonrpc::Result<Option<Hover>> {
167 let expression = self
168 .get_expression_at_position(¶ms.text_document_position_params);
169
170 if let Some(expression) = expression {
171 Ok(match expression.data {
172 DatexExpressionData::VariableDeclaration(
174 VariableDeclaration {
175 name, id: Some(id), ..
176 },
177 )
178 | DatexExpressionData::VariableAssignment(
179 VariableAssignment {
180 name, id: Some(id), ..
181 },
182 )
183 | DatexExpressionData::VariableAccess(VariableAccess {
184 id,
185 name,
186 }) => {
187 let variable_metadata =
188 self.get_variable_by_id(id).unwrap();
189 Some(self.get_language_string_hover(&format!(
190 "{} {}: {}",
191 variable_metadata.shape,
192 name,
193 variable_metadata.var_type.unwrap_or(Type::unknown())
194 )))
195 }
196
197 DatexExpressionData::Integer(integer) => Some(
199 self.get_language_string_hover(&format!("{}", integer)),
200 ),
201 DatexExpressionData::TypedInteger(typed_integer) => {
202 Some(self.get_language_string_hover(&format!(
203 "{}",
204 typed_integer
205 )))
206 }
207 DatexExpressionData::Decimal(decimal) => Some(
208 self.get_language_string_hover(&format!("{}", decimal)),
209 ),
210 DatexExpressionData::TypedDecimal(typed_decimal) => {
211 Some(self.get_language_string_hover(&format!(
212 "{}",
213 typed_decimal
214 )))
215 }
216 DatexExpressionData::Boolean(boolean) => Some(
217 self.get_language_string_hover(&format!("{}", boolean)),
218 ),
219 DatexExpressionData::Text(text) => Some(
220 self.get_language_string_hover(&format!("\"{}\"", text)),
221 ),
222 DatexExpressionData::Endpoint(endpoint) => Some(
223 self.get_language_string_hover(&format!("{}", endpoint)),
224 ),
225 DatexExpressionData::Null => {
226 Some(self.get_language_string_hover("null"))
227 }
228
229 _ => None,
230 })
231 } else {
232 Err(realhydroper_lsp::jsonrpc::Error {
233 code: ErrorCode::ParseError,
234 message: Cow::from("No AST available"),
235 data: None,
236 })
237 }
238 }
239
240 async fn inlay_hint(
241 &self,
242 params: InlayHintParams,
243 ) -> realhydroper_lsp::jsonrpc::Result<Option<Vec<InlayHint>>> {
244 let type_hints = self
246 .get_type_hints(params.text_document.uri)
247 .unwrap()
248 .into_iter()
249 .map(|hint| InlayHint {
250 position: hint.0,
251 label: InlayHintLabel::String(format!(": {}", hint.1.unwrap())),
252 kind: Some(InlayHintKind::TYPE),
253 text_edits: None,
254 tooltip: None,
255 padding_left: Some(true),
256 padding_right: None,
257 data: None,
258 })
259 .collect();
260
261 Ok(Some(type_hints))
262 }
263
264 async fn goto_definition(
265 &self,
266 params: GotoDefinitionParams,
267 ) -> realhydroper_lsp::jsonrpc::Result<Option<GotoDefinitionResponse>> {
268 let expression = self
269 .get_expression_at_position(¶ms.text_document_position_params);
270 if let Some(expression) = expression {
271 match expression.data {
272 DatexExpressionData::VariableAccess(VariableAccess {
273 id,
274 name,
275 }) => {
276 let uri =
277 params.text_document_position_params.text_document.uri;
278 let mut workspace = self.compiler_workspace.borrow_mut();
279 let file = workspace.get_file_mut(&uri).unwrap();
280 if let Some(RichAst { ast, .. }) = &mut file.rich_ast {
281 let mut finder = VariableDeclarationFinder::new(id);
282 finder.visit_datex_expression(ast);
283 Ok(finder.variable_declaration_position.map(
284 |position| {
285 GotoDefinitionResponse::Scalar(Location {
286 uri,
287 range: self
288 .convert_byte_range_to_document_range(
289 &position,
290 &file.content,
291 ),
292 })
293 },
294 ))
295 } else {
296 Ok(None)
297 }
298 }
299 _ => Ok(None),
300 }
301 } else {
302 Err(Error::internal_error())
303 }
304 }
305
306 async fn document_link(
307 &self,
308 params: DocumentLinkParams,
309 ) -> realhydroper_lsp::jsonrpc::Result<Option<Vec<DocumentLink>>> {
310 Ok(Some(vec![]))
312 }
313
314 async fn diagnostic(
316 &self,
317 params: DocumentDiagnosticParams,
318 ) -> realhydroper_lsp::jsonrpc::Result<DocumentDiagnosticReportResult> {
319 self.client
320 .log_message(MessageType::INFO, "diagnostics!")
321 .await;
322
323 let uri = params.text_document.uri;
324 let diagnostics = self.get_diagnostics_for_file(&uri);
325 let report = FullDocumentDiagnosticReport {
326 result_id: None,
327 items: diagnostics,
328 };
329
330 Ok(DocumentDiagnosticReportResult::Report(
331 DocumentDiagnosticReport::Full(
332 RelatedFullDocumentDiagnosticReport {
333 related_documents: None,
334 full_document_diagnostic_report: report,
335 },
336 ),
337 ))
338 }
339}
340
341impl LanguageServerBackend {
342 fn get_language_string_hover(&self, text: &str) -> Hover {
343 let contents = HoverContents::Scalar(MarkedString::LanguageString(
344 LanguageString {
345 language: "datex".to_string(),
346 value: text.to_string(),
347 },
348 ));
349 Hover {
350 contents,
351 range: None,
352 }
353 }
354
355 fn get_diagnostics_for_file(&self, url: &Url) -> Vec<Diagnostic> {
356 let mut diagnostics = Vec::new();
357 let errors = self.spanned_compiler_errors.borrow();
358 if let Some(file_errors) = errors.get(url) {
359 for spanned_error in file_errors {
360 let diagnostic = Diagnostic {
361 range: spanned_error.span,
362 severity: Some(DiagnosticSeverity::ERROR),
363 code: None,
364 code_description: None,
365 source: Some("datex".to_string()),
366 message: format!("{}", spanned_error.error),
367 related_information: None,
368 tags: None,
369 data: None,
370 };
371 diagnostics.push(diagnostic);
372 }
373 }
374 diagnostics
375 }
376}
377
378pub fn create_lsp<I, O>(
379 runtime: Runtime,
380 input: I,
381 output: O,
382) -> impl core::future::Future<Output = ()>
383where
384 I: AsyncRead + Unpin,
385 O: AsyncWrite,
386{
387 let compiler_workspace = CompilerWorkspace::new(runtime);
388 let (service, socket) = LspService::new(|client| {
389 LanguageServerBackend::new(client, compiler_workspace)
390 });
391 Server::new(input, output, socket).serve(service)
392}
393
394#[cfg(test)]
395mod tests {
396 use core::str::FromStr;
397
398 use crate::runtime::{AsyncContext, RuntimeConfig};
399 use crate::values::core_values::endpoint::Endpoint;
400
401 use super::*;
402 use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
403 use tokio::task::LocalSet;
404 use tokio::time::{Duration, timeout};
405
406 #[tokio::test(flavor = "current_thread")]
407 async fn test_lsp_initialization() {
408 let local = LocalSet::new();
410
411 local
412 .run_until(async {
413 let runtime = Runtime::new(
414 RuntimeConfig::new_with_endpoint(
415 Endpoint::from_str("@lspler").unwrap(),
416 ),
417 AsyncContext::new(),
418 );
419
420 let (mut client_read, server_write) = duplex(1024);
421 let (server_read, mut client_write) = duplex(1024);
422
423 let lsp_future = create_lsp(runtime, server_read, server_write);
424 let lsp_handle = tokio::task::spawn_local(lsp_future);
425
426 let init_body = r#"{
428 "jsonrpc": "2.0",
429 "id": 1,
430 "method": "initialize",
431 "params": {
432 "capabilities": {},
433 "rootUri": null,
434 "workspaceFolders": null
435 }
436 }"#;
437
438 let init_request = format!(
439 "Content-Length: {}\r\n\r\n{}",
440 init_body.len(),
441 init_body
442 );
443
444 client_write
445 .write_all(init_request.as_bytes())
446 .await
447 .unwrap();
448
449 let mut buffer = vec![0; 1024];
451 let n = timeout(
452 Duration::from_secs(2),
453 client_read.read(&mut buffer),
454 )
455 .await
456 .unwrap()
457 .unwrap();
458
459 let response = String::from_utf8_lossy(&buffer[..n]);
460 assert!(response.contains(r#""id":1"#));
461 lsp_handle.abort();
462 })
463 .await;
464 }
465}