Fork of daniellemaywood.uk/gleam — Wasm codegen work
21 kB
596 lines
1use crate::{
2 diagnostic::{Diagnostic, Level},
3 io::{CommandExecutor, FileSystemReader, FileSystemWriter},
4 language_server::{
5 engine::{self, LanguageServerEngine},
6 feedback::{Feedback, FeedbackBookKeeper},
7 files::FileSystemProxy,
8 router::Router,
9 src_span_to_lsp_range, DownloadDependencies, MakeLocker,
10 },
11 line_numbers::LineNumbers,
12 Result,
13};
14use debug_ignore::DebugIgnore;
15use lsp::{
16 notification::{DidChangeWatchedFiles, DidOpenTextDocument},
17 request::GotoDefinition,
18 HoverProviderCapability, Position, Range, TextEdit, Url,
19};
20use lsp_types::{
21 self as lsp,
22 notification::{DidChangeTextDocument, DidCloseTextDocument, DidSaveTextDocument},
23 request::{Completion, Formatting, HoverRequest},
24 InitializeParams, PublishDiagnosticsParams,
25};
26use serde_json::Value as Json;
27use std::collections::HashMap;
28
29use camino::Utf8PathBuf;
30
31use super::progress::ConnectionProgressReporter;
32
33/// This class is responsible for handling the language server protocol and
34/// delegating the work to the engine.
35///
36/// - Configuring watching of the `gleam.toml` file.
37/// - Decoding requests.
38/// - Encoding responses.
39/// - Sending diagnostics and messages to the client.
40/// - Tracking the state of diagnostics and messages.
41/// - Performing the initialisation handshake.
42///
43#[derive(Debug)]
44pub struct LanguageServer<'a, IO> {
45 initialise_params: InitializeParams,
46 connection: DebugIgnore<&'a lsp_server::Connection>,
47 outside_of_project_feedback: FeedbackBookKeeper,
48 router: Router<IO, ConnectionProgressReporter<'a>>,
49 io: FileSystemProxy<IO>,
50}
51
52impl<'a, IO> LanguageServer<'a, IO>
53where
54 IO: FileSystemReader
55 + FileSystemWriter
56 + CommandExecutor
57 + DownloadDependencies
58 + MakeLocker
59 + Clone,
60{
61 pub fn new(connection: &'a lsp_server::Connection, io: IO) -> Result<Self> {
62 let initialise_params = initialisation_handshake(connection);
63 let reporter = ConnectionProgressReporter::new(connection, &initialise_params);
64 let io = FileSystemProxy::new(io);
65 let router = Router::new(reporter, io.clone());
66 Ok(Self {
67 connection: connection.into(),
68 initialise_params,
69 outside_of_project_feedback: FeedbackBookKeeper::default(),
70 router,
71 io,
72 })
73 }
74
75 pub fn run(&mut self) -> Result<()> {
76 self.start_watching_gleam_toml();
77
78 // Enter the message loop, handling each message that comes in from the client
79 for message in &self.connection.receiver {
80 match self.handle_message(message) {
81 Next::Continue => (),
82 Next::Break => break,
83 }
84 }
85
86 Ok(())
87 }
88
89 fn handle_message(&mut self, message: lsp_server::Message) -> Next {
90 match message {
91 lsp_server::Message::Request(request) if self.handle_shutdown(&request) => Next::Break,
92
93 lsp_server::Message::Request(request) => {
94 self.handle_request(request);
95 Next::Continue
96 }
97
98 lsp_server::Message::Notification(notification) => {
99 self.handle_notification(notification);
100 Next::Continue
101 }
102
103 lsp_server::Message::Response(_) => Next::Continue,
104 }
105 }
106
107 fn handle_shutdown(&mut self, request: &lsp_server::Request) -> bool {
108 self.connection
109 .handle_shutdown(request)
110 .expect("LSP shutdown")
111 }
112
113 fn handle_request(&mut self, request: lsp_server::Request) {
114 let id = request.id.clone();
115 let (payload, feedback) = match request.method.as_str() {
116 "textDocument/formatting" => {
117 let params = cast_request::<Formatting>(request);
118 self.format(params)
119 }
120
121 "textDocument/hover" => {
122 let params = cast_request::<HoverRequest>(request);
123 self.hover(params)
124 }
125
126 "textDocument/definition" => {
127 let params = cast_request::<GotoDefinition>(request);
128 self.goto_definition(params)
129 }
130
131 "textDocument/completion" => {
132 let params = cast_request::<Completion>(request);
133 self.completion(params)
134 }
135
136 _ => panic!("Unsupported LSP request"),
137 };
138
139 self.publish_feedback(feedback);
140
141 let response = lsp_server::Response {
142 id,
143 error: None,
144 result: Some(payload),
145 };
146 self.connection
147 .sender
148 .send(lsp_server::Message::Response(response))
149 .expect("channel send LSP response")
150 }
151
152 fn handle_notification(&mut self, notification: lsp_server::Notification) {
153 let feedback = match notification.method.as_str() {
154 "textDocument/didOpen" => {
155 let params = cast_notification::<DidOpenTextDocument>(notification);
156 self.text_document_did_open(params)
157 }
158
159 "textDocument/didSave" => {
160 let params = cast_notification::<DidSaveTextDocument>(notification);
161 self.text_document_did_save(params)
162 }
163
164 "textDocument/didClose" => {
165 let params = cast_notification::<DidCloseTextDocument>(notification);
166 self.text_document_did_close(params)
167 }
168
169 "textDocument/didChange" => {
170 let params = cast_notification::<DidChangeTextDocument>(notification);
171 self.text_document_did_change(params)
172 }
173
174 "workspace/didChangeWatchedFiles" => {
175 let params = cast_notification::<DidChangeWatchedFiles>(notification);
176 self.watched_files_changed(params)
177 }
178
179 _ => return,
180 };
181
182 self.publish_feedback(feedback);
183 }
184
185 fn publish_feedback(&self, feedback: Feedback) {
186 self.publish_diagnostics(feedback.diagnostics);
187 self.publish_messages(feedback.messages);
188 }
189
190 fn publish_diagnostics(&self, diagnostics: HashMap<Utf8PathBuf, Vec<Diagnostic>>) {
191 for (path, diagnostics) in diagnostics {
192 let diagnostics = diagnostics
193 .into_iter()
194 .flat_map(diagnostic_to_lsp)
195 .collect::<Vec<_>>();
196 let uri = path_to_uri(path);
197
198 // Publish the diagnostics
199 let diagnostic_params = PublishDiagnosticsParams {
200 uri,
201 diagnostics,
202 version: None,
203 };
204 let notification = lsp_server::Notification {
205 method: "textDocument/publishDiagnostics".into(),
206 params: serde_json::to_value(diagnostic_params)
207 .expect("textDocument/publishDiagnostics to json"),
208 };
209 self.connection
210 .sender
211 .send(lsp_server::Message::Notification(notification))
212 .expect("send textDocument/publishDiagnostics");
213 }
214 }
215
216 fn start_watching_gleam_toml(&mut self) {
217 let supports_watch_files = self
218 .initialise_params
219 .capabilities
220 .workspace
221 .as_ref()
222 .and_then(|w| w.did_change_watched_files)
223 .map(|wf| wf.dynamic_registration == Some(true))
224 .unwrap_or(false);
225
226 if !supports_watch_files {
227 tracing::warn!("lsp_client_cannot_watch_gleam_toml");
228 return;
229 }
230
231 // Register gleam.toml as a watched file so we get a notification when
232 // it changes and thus know that we need to rebuild the entire project.
233 let watch_config = lsp::Registration {
234 id: "watch-gleam-toml".into(),
235 method: "workspace/didChangeWatchedFiles".into(),
236 register_options: Some(
237 serde_json::value::to_value(lsp::DidChangeWatchedFilesRegistrationOptions {
238 watchers: vec![lsp::FileSystemWatcher {
239 glob_pattern: "**/gleam.toml".into(),
240 kind: Some(lsp::WatchKind::Change),
241 }],
242 })
243 .expect("workspace/didChangeWatchedFiles to json"),
244 ),
245 };
246 let request = lsp_server::Request {
247 id: 1.into(),
248 method: "client/registerCapability".into(),
249 params: serde_json::value::to_value(lsp::RegistrationParams {
250 registrations: vec![watch_config],
251 })
252 .expect("client/registerCapability to json"),
253 };
254 self.connection
255 .sender
256 .send(lsp_server::Message::Request(request))
257 .expect("send client/registerCapability");
258 }
259
260 fn publish_messages(&self, messages: Vec<Diagnostic>) {
261 for message in messages {
262 let params = lsp::ShowMessageParams {
263 typ: match message.level {
264 Level::Error => lsp::MessageType::ERROR,
265 Level::Warning => lsp::MessageType::WARNING,
266 },
267 message: message.text,
268 };
269 let notification = lsp_server::Notification {
270 method: "window/showMessage".into(),
271 params: serde_json::to_value(params).expect("window/showMessage to json"),
272 };
273 self.connection
274 .sender
275 .send(lsp_server::Message::Notification(notification))
276 .expect("send window/showMessage");
277 }
278 }
279
280 fn respond_with_engine<T, Handler>(
281 &mut self,
282 path: Utf8PathBuf,
283 handler: Handler,
284 ) -> (Json, Feedback)
285 where
286 T: serde::Serialize,
287 Handler: FnOnce(
288 &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>,
289 ) -> engine::Response<T>,
290 {
291 match self.router.project_for_path(&path) {
292 Ok(Some(project)) => {
293 let engine::Response {
294 result,
295 warnings,
296 compilation,
297 } = handler(&mut project.engine);
298 match result {
299 Ok(value) => {
300 let feedback = project.feedback.response(compilation, warnings);
301 let json = serde_json::to_value(value).expect("response to json");
302 (json, feedback)
303 }
304 Err(e) => {
305 let feedback = project.feedback.build_with_error(e, compilation, warnings);
306 (Json::Null, feedback)
307 }
308 }
309 }
310
311 Ok(None) => (Json::Null, Feedback::default()),
312
313 Err(error) => (Json::Null, self.outside_of_project_feedback.error(error)),
314 }
315 }
316
317 fn notified_with_engine(
318 &mut self,
319 path: Utf8PathBuf,
320 handler: impl FnOnce(
321 &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>,
322 ) -> engine::Response<()>,
323 ) -> Feedback {
324 self.respond_with_engine(path, handler).1
325 }
326
327 fn format(&mut self, params: lsp::DocumentFormattingParams) -> (Json, Feedback) {
328 let path = path(¶ms.text_document.uri);
329 let mut new_text = String::new();
330 let mut error_response = |error| {
331 let feedback = match self.router.project_for_path(&path) {
332 Ok(Some(project)) => project.feedback.error(error),
333 Ok(None) | Err(_) => self.outside_of_project_feedback.error(error),
334 };
335 (Json::Null, feedback)
336 };
337
338 let src = match self.io.read(&path) {
339 Ok(src) => src.into(),
340 Err(error) => return error_response(error),
341 };
342
343 if let Err(error) = crate::format::pretty(&mut new_text, &src, &path) {
344 return error_response(error);
345 }
346
347 let line_count = src.lines().count() as u32;
348
349 let edit = TextEdit {
350 range: Range::new(Position::new(0, 0), Position::new(line_count, 0)),
351 new_text,
352 };
353 let json = serde_json::to_value(vec![edit]).expect("to JSON value");
354
355 (json, Feedback::default())
356 }
357
358 fn hover(&mut self, params: lsp::HoverParams) -> (Json, Feedback) {
359 let path = path(¶ms.text_document_position_params.text_document.uri);
360 self.respond_with_engine(path, |engine| engine.hover(params))
361 }
362
363 fn goto_definition(&mut self, params: lsp::GotoDefinitionParams) -> (Json, Feedback) {
364 let path = path(¶ms.text_document_position_params.text_document.uri);
365 self.respond_with_engine(path, |engine| engine.goto_definition(params))
366 }
367
368 fn completion(&mut self, params: lsp::CompletionParams) -> (Json, Feedback) {
369 let path = path(¶ms.text_document_position.text_document.uri);
370 self.respond_with_engine(path, |engine| {
371 engine.completion(params.text_document_position)
372 })
373 }
374
375 /// A file opened in the editor may be unsaved, so store a copy of the
376 /// new content in memory and compile.
377 fn text_document_did_open(&mut self, params: lsp::DidOpenTextDocumentParams) -> Feedback {
378 let path = path(¶ms.text_document.uri);
379 if let Err(error) = self.io.write_mem_cache(&path, ¶ms.text_document.text) {
380 return self.outside_of_project_feedback.error(error);
381 }
382
383 self.notified_with_engine(path, |engine| engine.compile_please())
384 }
385
386 fn text_document_did_save(&mut self, params: lsp::DidSaveTextDocumentParams) -> Feedback {
387 let path = path(¶ms.text_document.uri);
388
389 // The file is in sync with the file system, discard our cache of the changes
390 if let Err(error) = self.io.delete_mem_cache(&path) {
391 return self.outside_of_project_feedback.error(error);
392 }
393
394 // The files on disc have changed, so compile the project with the new changes
395 self.notified_with_engine(path, |engine| engine.compile_please())
396 }
397
398 fn text_document_did_close(&mut self, params: lsp::DidCloseTextDocumentParams) -> Feedback {
399 let path = path(¶ms.text_document.uri);
400
401 // The file is in sync with the file system, discard our cache of the changes
402 if let Err(error) = self.io.delete_mem_cache(&path) {
403 return self.outside_of_project_feedback.error(error);
404 }
405
406 Feedback::default()
407 }
408
409 /// A file has changed in the editor, so store a copy of the new content in
410 /// memory and compile.
411 fn text_document_did_change(&mut self, params: lsp::DidChangeTextDocumentParams) -> Feedback {
412 let path = path(¶ms.text_document.uri);
413
414 let changes = match params.content_changes.into_iter().last() {
415 Some(changes) => changes,
416 None => return Feedback::default(),
417 };
418
419 if let Err(error) = self.io.write_mem_cache(&path, changes.text.as_str()) {
420 return self.outside_of_project_feedback.error(error);
421 }
422
423 // The files on disc have changed, so compile the project with the new changes
424 self.notified_with_engine(path, |engine| engine.compile_please())
425 }
426
427 fn watched_files_changed(&mut self, params: lsp::DidChangeWatchedFilesParams) -> Feedback {
428 let changes = match params.changes.into_iter().last() {
429 Some(changes) => changes,
430 None => return Feedback::default(),
431 };
432
433 let path = path(&changes.uri);
434 self.router.delete_engine_for_path(&path);
435 self.notified_with_engine(path, LanguageServerEngine::compile_please)
436 }
437}
438
439fn initialisation_handshake(connection: &lsp_server::Connection) -> InitializeParams {
440 let server_capabilities = lsp::ServerCapabilities {
441 text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
442 lsp::TextDocumentSyncOptions {
443 open_close: Some(true),
444 change: Some(lsp::TextDocumentSyncKind::FULL),
445 will_save: None,
446 will_save_wait_until: None,
447 save: Some(lsp::TextDocumentSyncSaveOptions::SaveOptions(
448 lsp::SaveOptions {
449 include_text: Some(false),
450 },
451 )),
452 },
453 )),
454 selection_range_provider: None,
455 hover_provider: Some(HoverProviderCapability::Simple(true)),
456 completion_provider: Some(lsp::CompletionOptions {
457 resolve_provider: None,
458 trigger_characters: None,
459 all_commit_characters: None,
460 work_done_progress_options: lsp::WorkDoneProgressOptions {
461 work_done_progress: None,
462 },
463 }),
464 signature_help_provider: None,
465 definition_provider: Some(lsp::OneOf::Left(true)),
466 type_definition_provider: None,
467 implementation_provider: None,
468 references_provider: None,
469 document_highlight_provider: None,
470 document_symbol_provider: None,
471 workspace_symbol_provider: None,
472 code_action_provider: None,
473 code_lens_provider: None,
474 document_formatting_provider: Some(lsp::OneOf::Left(true)),
475 document_range_formatting_provider: None,
476 document_on_type_formatting_provider: None,
477 rename_provider: None,
478 document_link_provider: None,
479 color_provider: None,
480 folding_range_provider: None,
481 declaration_provider: None,
482 execute_command_provider: None,
483 workspace: None,
484 call_hierarchy_provider: None,
485 semantic_tokens_provider: None,
486 moniker_provider: None,
487 linked_editing_range_provider: None,
488 experimental: None,
489 };
490 let server_capabilities_json =
491 serde_json::to_value(server_capabilities).expect("server_capabilities_serde");
492 let initialise_params_json = connection
493 .initialize(server_capabilities_json)
494 .expect("LSP initialize");
495 let initialise_params: InitializeParams =
496 serde_json::from_value(initialise_params_json).expect("LSP InitializeParams from json");
497 initialise_params
498}
499
500#[derive(Debug, Clone, Copy)]
501enum Next {
502 Continue,
503 Break,
504}
505
506fn cast_request<R>(request: lsp_server::Request) -> R::Params
507where
508 R: lsp::request::Request,
509 R::Params: serde::de::DeserializeOwned,
510{
511 let (_, params) = request.extract(R::METHOD).expect("cast request");
512 params
513}
514
515fn cast_notification<N>(notification: lsp_server::Notification) -> N::Params
516where
517 N: lsp::notification::Notification,
518 N::Params: serde::de::DeserializeOwned,
519{
520 notification
521 .extract::<N::Params>(N::METHOD)
522 .expect("cast notification")
523}
524
525fn diagnostic_to_lsp(diagnostic: Diagnostic) -> Vec<lsp::Diagnostic> {
526 let severity = match diagnostic.level {
527 Level::Error => lsp::DiagnosticSeverity::ERROR,
528 Level::Warning => lsp::DiagnosticSeverity::WARNING,
529 };
530 let hint = diagnostic.hint;
531 let mut text = diagnostic.title;
532
533 if let Some(label) = diagnostic
534 .location
535 .as_ref()
536 .and_then(|location| location.label.text.as_deref())
537 {
538 text.push_str("\n\n");
539 text.push_str(label);
540 if !label.ends_with(['.', '?']) {
541 text.push('.');
542 }
543 }
544
545 if !diagnostic.text.is_empty() {
546 text.push_str("\n\n");
547 text.push_str(&diagnostic.text);
548 }
549
550 // TODO: Redesign the diagnostic type so that we can be sure there is always
551 // a location. Locationless diagnostics would be handled separately.
552 let location = diagnostic
553 .location
554 .expect("Diagnostic given to LSP without location");
555 let line_numbers = LineNumbers::new(&location.src);
556
557 let main = lsp::Diagnostic {
558 range: src_span_to_lsp_range(location.label.span, &line_numbers),
559 severity: Some(severity),
560 code: None,
561 code_description: None,
562 source: None,
563 message: text,
564 related_information: None,
565 tags: None,
566 data: None,
567 };
568
569 match hint {
570 Some(hint) => {
571 let hint = lsp::Diagnostic {
572 severity: Some(lsp::DiagnosticSeverity::HINT),
573 message: hint,
574 ..main.clone()
575 };
576 vec![main, hint]
577 }
578 None => vec![main],
579 }
580}
581
582fn path_to_uri(path: Utf8PathBuf) -> Url {
583 let mut file: String = "file://".into();
584 file.push_str(&path.as_os_str().to_string_lossy());
585 Url::parse(&file).expect("path_to_uri URL parse")
586}
587
588fn path(uri: &Url) -> Utf8PathBuf {
589 // The to_file_path method is available on these platforms
590 #[cfg(any(unix, windows, target_os = "redox", target_os = "wasi"))]
591 return Utf8PathBuf::from_path_buf(uri.to_file_path().expect("URL file"))
592 .expect("Non Utf8 Path");
593
594 #[cfg(not(any(unix, windows, target_os = "redox", target_os = "wasi")))]
595 return Utf8PathBuf::from_path_buf(uri.path().into()).expect("Non Utf8 Path");
596}