Fork of daniellemaywood.uk/gleam — Wasm codegen work
26 kB
718 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 The Gleam contributors
3
4use super::{
5 DownloadDependencies, MakeLocker,
6 engine::{self, LanguageServerEngine},
7 feedback::{Feedback, FeedbackBookKeeper},
8 files::FileSystemProxy,
9 messages::{Message, MessageBuffer, Next, Notification, Request},
10 progress::ConnectionProgressReporter,
11 router::Router,
12 src_span_to_lsp_range,
13};
14use camino::{Utf8Path, Utf8PathBuf};
15use debug_ignore::DebugIgnore;
16use gleam_core::{
17 Result,
18 diagnostic::{Diagnostic, ExtraLabel, Level},
19 io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter},
20 line_numbers::LineNumbers,
21};
22use lsp_server::ResponseError;
23use lsp_types::{
24 self as lsp, InitializeParams, Position, PublishDiagnosticsParams, Range, RenameOptions,
25 TextEdit, Uri as Url,
26};
27use serde_json::Value as Json;
28use std::collections::{HashMap, HashSet};
29
30/// This class is responsible for handling the language server protocol and
31/// delegating the work to the engine.
32///
33/// - Configuring watching of the `gleam.toml` file.
34/// - Decoding requests.
35/// - Encoding responses.
36/// - Sending diagnostics and messages to the client.
37/// - Tracking the state of diagnostics and messages.
38/// - Performing the initialisation handshake.
39///
40#[derive(Debug)]
41pub struct LanguageServer<'a, IO> {
42 initialise_params: InitializeParams,
43 connection: DebugIgnore<&'a lsp_server::Connection>,
44 outside_of_project_feedback: FeedbackBookKeeper,
45 router: Router<IO, ConnectionProgressReporter<'a>>,
46 changed_projects: HashSet<Utf8PathBuf>,
47 io: FileSystemProxy<IO>,
48}
49
50impl<'a, IO> LanguageServer<'a, IO>
51where
52 IO: FileSystemReader
53 + FileSystemWriter
54 + BeamCompiler
55 + CommandExecutor
56 + DownloadDependencies
57 + MakeLocker
58 + Clone,
59{
60 pub fn new(connection: &'a lsp_server::Connection, io: IO) -> Result<Self> {
61 let initialise_params = initialisation_handshake(connection);
62 let reporter = ConnectionProgressReporter::new(connection, &initialise_params);
63 let io = FileSystemProxy::new(io);
64 let router = Router::new(reporter, io.clone());
65 Ok(Self {
66 connection: connection.into(),
67 initialise_params,
68 changed_projects: HashSet::new(),
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 let mut buffer = MessageBuffer::new();
78
79 loop {
80 match buffer.receive(*self.connection) {
81 Next::Stop => break,
82 Next::MorePlease => (),
83 Next::Handle(messages) => {
84 for message in messages {
85 self.handle_message(message);
86 }
87 }
88 }
89 }
90
91 Ok(())
92 }
93
94 fn handle_message(&mut self, message: Message) {
95 match message {
96 Message::Request(id, request) => self.handle_request(id, request),
97 Message::Notification(notification) => self.handle_notification(notification),
98 }
99 }
100
101 fn handle_request(&mut self, id: lsp_server::RequestId, request: Request) {
102 let (outcome, feedback) = match request {
103 Request::Format(param) => self.format(param),
104 Request::Hover(param) => self.hover(param),
105 Request::GoToDefinition(param) => self.goto_definition(param),
106 Request::Completion(param) => self.completion(param),
107 Request::CodeAction(param) => self.code_action(param),
108 Request::SignatureHelp(param) => self.signature_help(param),
109 Request::DocumentSymbol(param) => self.document_symbol(param),
110 Request::FoldingRange(param) => self.folding_range(param),
111 Request::PrepareRename(param) => self.prepare_rename(param),
112 Request::Rename(param) => self.rename(param),
113 Request::GoToTypeDefinition(param) => self.goto_type_definition(param),
114 Request::FindReferences(param) => self.find_references(param),
115 Request::DocumentHighlight(param) => self.document_highlight(param),
116 };
117
118 self.publish_feedback(feedback);
119
120 let response = match outcome {
121 Ok(payload) => lsp_server::Response {
122 id,
123 error: None,
124 result: Some(payload),
125 },
126 Err(error) => lsp_server::Response {
127 id,
128 error: Some(error),
129 result: None,
130 },
131 };
132
133 self.connection
134 .sender
135 .send(lsp_server::Message::Response(response))
136 .expect("channel send LSP response")
137 }
138
139 fn handle_notification(&mut self, notification: Notification) {
140 let feedback = match notification {
141 Notification::CompilePlease => self.compile_please(),
142 Notification::SourceFileOpened { path, text } => self.source_file_opened(path, text),
143 Notification::SourceFileClosed { path } => self.source_file_closed(path),
144 Notification::SourceFileSaved { path } => self.discard_in_memory_cache(path),
145 Notification::SourceFileChangedInMemory { path, text } => {
146 self.cache_file_in_memory(path, text)
147 }
148 Notification::ConfigFileChanged { path } => self.watched_files_changed(path),
149 };
150 self.publish_feedback(feedback);
151 }
152
153 fn publish_feedback(&self, feedback: Feedback) {
154 self.publish_diagnostics(feedback.diagnostics);
155 self.publish_messages(feedback.messages);
156 }
157
158 fn publish_diagnostics(&self, diagnostics: HashMap<Utf8PathBuf, Vec<Diagnostic>>) {
159 for (path, diagnostics) in diagnostics {
160 let diagnostics = diagnostics
161 .into_iter()
162 .flat_map(diagnostic_to_lsp)
163 .collect::<Vec<_>>();
164 let uri = path_to_uri(path);
165
166 // Publish the diagnostics
167 let diagnostic_params = PublishDiagnosticsParams {
168 uri,
169 diagnostics,
170 version: None,
171 };
172 let notification = lsp_server::Notification {
173 method: "textDocument/publishDiagnostics".into(),
174 params: serde_json::to_value(diagnostic_params)
175 .expect("textDocument/publishDiagnostics to json"),
176 };
177 self.connection
178 .sender
179 .send(lsp_server::Message::Notification(notification))
180 .expect("send textDocument/publishDiagnostics");
181 }
182 }
183
184 fn start_watching_gleam_toml(&mut self) {
185 let supports_watch_files = self
186 .initialise_params
187 .capabilities
188 .workspace
189 .as_ref()
190 .and_then(|w| w.did_change_watched_files)
191 .map(|wf| wf.dynamic_registration.unwrap_or(false))
192 .unwrap_or(false);
193
194 if !supports_watch_files {
195 tracing::warn!("lsp_client_cannot_watch_gleam_toml");
196 return;
197 }
198
199 // Register gleam.toml as a watched file so we get a notification when
200 // it changes and thus know that we need to rebuild the entire project.
201 let watch_config = lsp::Registration {
202 id: "watch-gleam-toml".into(),
203 method: "workspace/didChangeWatchedFiles".into(),
204 register_options: Some(
205 serde_json::value::to_value(lsp::DidChangeWatchedFilesRegistrationOptions {
206 watchers: vec![lsp::FileSystemWatcher {
207 glob_pattern: "**/gleam.toml".to_string().into(),
208 kind: Some(lsp::WatchKind::Change),
209 }],
210 })
211 .expect("workspace/didChangeWatchedFiles to json"),
212 ),
213 };
214 let request = lsp_server::Request {
215 id: 1.into(),
216 method: "client/registerCapability".into(),
217 params: serde_json::value::to_value(lsp::RegistrationParams {
218 registrations: vec![watch_config],
219 })
220 .expect("client/registerCapability to json"),
221 };
222 self.connection
223 .sender
224 .send(lsp_server::Message::Request(request))
225 .expect("send client/registerCapability");
226 }
227
228 fn publish_messages(&self, messages: Vec<Diagnostic>) {
229 for message in messages {
230 let params = lsp::ShowMessageParams {
231 kind: match message.level {
232 Level::Error => lsp::MessageType::Error,
233 Level::Warning => lsp::MessageType::Warning,
234 },
235 message: message.text,
236 };
237 let notification = lsp_server::Notification {
238 method: "window/showMessage".into(),
239 params: serde_json::to_value(params).expect("window/showMessage to json"),
240 };
241 self.connection
242 .sender
243 .send(lsp_server::Message::Notification(notification))
244 .expect("send window/showMessage");
245 }
246 }
247
248 fn respond_with_engine<T, Handler>(
249 &mut self,
250 path: Utf8PathBuf,
251 handler: Handler,
252 ) -> (Result<Json, ResponseError>, Feedback)
253 where
254 T: serde::Serialize,
255 Handler: FnOnce(
256 &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>,
257 ) -> engine::Response<T>,
258 {
259 self.fallible_respond_with_engine(path, |engine| {
260 let response = handler(engine);
261 engine::Response {
262 result: response.result.map(Ok),
263 warnings: response.warnings,
264 compilation: response.compilation,
265 }
266 })
267 }
268
269 fn fallible_respond_with_engine<T, Handler>(
270 &mut self,
271 path: Utf8PathBuf,
272 handler: Handler,
273 ) -> (Result<Json, ResponseError>, Feedback)
274 where
275 T: serde::Serialize,
276 Handler: FnOnce(
277 &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>,
278 ) -> engine::Response<Result<T, ResponseError>>,
279 {
280 match self.router.project_for_path(path) {
281 Ok(Some(project)) => {
282 let engine::Response {
283 result,
284 warnings,
285 compilation,
286 } = handler(&mut project.engine);
287 match result {
288 Ok(Ok(value)) => {
289 let feedback = project.feedback.response(compilation, warnings);
290 let json = serde_json::to_value(value).expect("response to json");
291 (Ok(json), feedback)
292 }
293 Ok(Err(error)) => {
294 let feedback = project.feedback.response(compilation, warnings);
295 (Err(error), feedback)
296 }
297 Err(e) => {
298 let feedback = project.feedback.build_with_error(e, compilation, warnings);
299 (Ok(Json::Null), feedback)
300 }
301 }
302 }
303
304 Ok(None) => (Ok(Json::Null), Feedback::default()),
305
306 Err(error) => (
307 Ok(Json::Null),
308 self.outside_of_project_feedback.error(error),
309 ),
310 }
311 }
312
313 fn path_error_response(
314 &mut self,
315 path: Utf8PathBuf,
316 error: gleam_core::Error,
317 ) -> (Result<Json, ResponseError>, Feedback) {
318 let feedback = match self.router.project_for_path(path) {
319 Ok(Some(project)) => project.feedback.error(error),
320 Ok(None) | Err(_) => self.outside_of_project_feedback.error(error),
321 };
322 (Ok(Json::Null), feedback)
323 }
324
325 fn format(
326 &mut self,
327 params: lsp::DocumentFormattingParams,
328 ) -> (Result<Json, ResponseError>, Feedback) {
329 let path = super::path(¶ms.text_document.uri);
330 let mut new_text = String::new();
331
332 let src = match self.io.read(&path) {
333 Ok(src) => src.into(),
334 Err(error) => return self.path_error_response(path, error),
335 };
336
337 if let Err(error) = gleam_core::format::pretty(&mut new_text, &src, &path) {
338 return self.path_error_response(path, error);
339 }
340
341 let line_count = src.lines().count() as u32;
342
343 let edit = TextEdit {
344 range: Range::new(Position::new(0, 0), Position::new(line_count, 0)),
345 new_text,
346 };
347 let json = serde_json::to_value(vec![edit]).expect("to JSON value");
348
349 (Ok(json), Feedback::default())
350 }
351
352 fn hover(&mut self, params: lsp::HoverParams) -> (Result<Json, ResponseError>, Feedback) {
353 let path = super::path(¶ms.text_document_position_params.text_document.uri);
354 self.respond_with_engine(path, |engine| engine.hover(params))
355 }
356
357 fn goto_definition(
358 &mut self,
359 params: lsp::DefinitionParams,
360 ) -> (Result<Json, ResponseError>, Feedback) {
361 let path = super::path(¶ms.text_document_position_params.text_document.uri);
362 self.respond_with_engine(path, |engine| engine.goto_definition(params))
363 }
364
365 fn goto_type_definition(
366 &mut self,
367 params: lsp_types::TypeDefinitionParams,
368 ) -> (Result<Json, ResponseError>, Feedback) {
369 let path = super::path(¶ms.text_document_position_params.text_document.uri);
370 self.respond_with_engine(path, |engine| engine.goto_type_definition(params))
371 }
372
373 fn completion(
374 &mut self,
375 params: lsp::CompletionParams,
376 ) -> (Result<Json, ResponseError>, Feedback) {
377 let path = super::path(¶ms.text_document_position_params.text_document.uri);
378
379 let src = match self.io.read(&path) {
380 Ok(src) => src.into(),
381 Err(error) => return self.path_error_response(path, error),
382 };
383 self.respond_with_engine(path, |engine| {
384 engine.completion(params.text_document_position_params, src)
385 })
386 }
387
388 fn signature_help(
389 &mut self,
390 params: lsp_types::SignatureHelpParams,
391 ) -> (Result<Json, ResponseError>, Feedback) {
392 let path = super::path(¶ms.text_document_position_params.text_document.uri);
393 self.respond_with_engine(path, |engine| engine.signature_help(params))
394 }
395
396 fn code_action(
397 &mut self,
398 params: lsp::CodeActionParams,
399 ) -> (Result<Json, ResponseError>, Feedback) {
400 let path = super::path(¶ms.text_document.uri);
401 self.respond_with_engine(path, |engine| engine.code_actions(params))
402 }
403
404 fn document_symbol(
405 &mut self,
406 params: lsp::DocumentSymbolParams,
407 ) -> (Result<Json, ResponseError>, Feedback) {
408 let path = super::path(¶ms.text_document.uri);
409 self.respond_with_engine(path, |engine| engine.document_symbol(params))
410 }
411
412 fn folding_range(
413 &mut self,
414 params: lsp::FoldingRangeParams,
415 ) -> (Result<Json, ResponseError>, Feedback) {
416 let path = super::path(¶ms.text_document.uri);
417 self.respond_with_engine(path, |engine| engine.folding_range(params))
418 }
419
420 fn prepare_rename(
421 &mut self,
422 params: lsp::PrepareRenameParams,
423 ) -> (Result<Json, ResponseError>, Feedback) {
424 let path = super::path(¶ms.text_document_position_params.text_document.uri);
425 self.respond_with_engine(path, |engine| engine.prepare_rename(params))
426 }
427
428 fn rename(&mut self, params: lsp::RenameParams) -> (Result<Json, ResponseError>, Feedback) {
429 let path = super::path(¶ms.text_document_position_params.text_document.uri);
430 self.fallible_respond_with_engine(
431 path,
432 |engine: &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>| {
433 engine.rename(params)
434 },
435 )
436 }
437
438 fn find_references(
439 &mut self,
440 params: lsp_types::ReferenceParams,
441 ) -> (Result<Json, ResponseError>, Feedback) {
442 let path = super::path(¶ms.text_document_position_params.text_document.uri);
443 self.respond_with_engine(path, |engine| engine.find_references(params))
444 }
445
446 fn document_highlight(
447 &mut self,
448 params: lsp_types::DocumentHighlightParams,
449 ) -> (Result<Json, ResponseError>, Feedback) {
450 let path = super::path(¶ms.text_document_position_params.text_document.uri);
451 self.respond_with_engine(path, |engine| engine.document_highlight(params))
452 }
453
454 fn cache_file_in_memory(&mut self, path: Utf8PathBuf, text: String) -> Feedback {
455 self.project_changed(&path);
456 if let Err(error) = self.io.write_mem_cache(&path, &text) {
457 return self.outside_of_project_feedback.error(error);
458 }
459 Feedback::none()
460 }
461
462 fn discard_in_memory_cache(&mut self, path: Utf8PathBuf) -> Feedback {
463 self.project_changed(&path);
464 if let Err(error) = self.io.delete_mem_cache(&path) {
465 return self.outside_of_project_feedback.error(error);
466 }
467 Feedback::none()
468 }
469
470 fn watched_files_changed(&mut self, path: Utf8PathBuf) -> Feedback {
471 self.router.delete_engine_for_path(&path);
472 Feedback::none()
473 }
474
475 fn compile_please(&mut self) -> Feedback {
476 let mut accumulator = Feedback::none();
477 let projects = std::mem::take(&mut self.changed_projects);
478 for path in projects {
479 let (_, feedback) = self.respond_with_engine(path, |this| this.compile_please());
480 accumulator.append_feedback(feedback);
481 }
482 accumulator
483 }
484
485 fn project_changed(&mut self, path: &Utf8Path) {
486 let project_path = self.router.project_path(path);
487 if let Some(project_path) = project_path {
488 _ = self.changed_projects.insert(project_path);
489 }
490 }
491
492 fn source_file_opened(&mut self, path: Utf8PathBuf, text: String) -> Feedback {
493 let mut feedback = self.cache_file_in_memory(path.clone(), text);
494 if let Ok(Some(project)) = self.router.project_for_path(path.clone()) {
495 feedback.append_feedback(project.feedback.open_file(path));
496 };
497 feedback
498 }
499
500 fn source_file_closed(&mut self, path: Utf8PathBuf) -> Feedback {
501 let mut feedback = self.discard_in_memory_cache(path.clone());
502 if let Ok(Some(project)) = self.router.project_for_path(path.clone()) {
503 feedback.append_feedback(project.feedback.close_file(&path));
504 };
505 feedback
506 }
507}
508
509fn initialisation_handshake(connection: &lsp_server::Connection) -> InitializeParams {
510 let server_capabilities = lsp::ServerCapabilities {
511 text_document_sync: Some(
512 lsp::TextDocumentSyncOptions {
513 open_close: Some(true),
514 change: Some(lsp::TextDocumentSyncKind::Full),
515 will_save: None,
516 will_save_wait_until: None,
517 save: Some(
518 lsp::SaveOptions {
519 include_text: Some(false),
520 }
521 .into(),
522 ),
523 }
524 .into(),
525 ),
526 selection_range_provider: None,
527 hover_provider: Some(true.into()),
528 completion_provider: Some(lsp::CompletionOptions {
529 resolve_provider: None,
530 trigger_characters: Some(vec![".".into()]),
531 all_commit_characters: None,
532 work_done_progress_options: lsp::WorkDoneProgressOptions {
533 work_done_progress: None,
534 },
535 completion_item: None,
536 }),
537 signature_help_provider: Some(lsp::SignatureHelpOptions {
538 trigger_characters: Some(vec!["(".into(), ",".into(), ":".into()]),
539 retrigger_characters: None,
540 work_done_progress_options: lsp::WorkDoneProgressOptions {
541 work_done_progress: None,
542 },
543 }),
544 definition_provider: Some(true.into()),
545 type_definition_provider: Some(true.into()),
546 implementation_provider: None,
547 references_provider: Some(true.into()),
548 document_highlight_provider: Some(true.into()),
549 document_symbol_provider: Some(true.into()),
550 workspace_symbol_provider: None,
551 code_action_provider: Some(true.into()),
552 code_lens_provider: None,
553 document_formatting_provider: Some(true.into()),
554 document_range_formatting_provider: None,
555 document_on_type_formatting_provider: None,
556 rename_provider: Some(
557 RenameOptions {
558 prepare_provider: Some(true),
559 work_done_progress_options: lsp::WorkDoneProgressOptions {
560 work_done_progress: None,
561 },
562 }
563 .into(),
564 ),
565 document_link_provider: None,
566 color_provider: None,
567 folding_range_provider: Some(true.into()),
568 declaration_provider: None,
569 execute_command_provider: None,
570 workspace: None,
571 call_hierarchy_provider: None,
572 semantic_tokens_provider: None,
573 moniker_provider: None,
574 linked_editing_range_provider: None,
575 experimental: None,
576 position_encoding: None,
577 inline_value_provider: None,
578 inlay_hint_provider: None,
579 diagnostic_provider: None,
580 type_hierarchy_provider: None,
581 notebook_document_sync: None,
582 inline_completion_provider: None,
583 };
584 let server_capabilities_json =
585 serde_json::to_value(server_capabilities).expect("server_capabilities_serde");
586 let initialise_params_json = connection
587 .initialize(server_capabilities_json)
588 .expect("LSP initialize");
589 let initialise_params: InitializeParams =
590 serde_json::from_value(initialise_params_json).expect("LSP InitializeParams from json");
591 initialise_params
592}
593
594fn diagnostic_to_lsp(diagnostic: Diagnostic) -> Vec<lsp::Diagnostic> {
595 let severity = match diagnostic.level {
596 Level::Error => lsp::DiagnosticSeverity::Error,
597 Level::Warning => lsp::DiagnosticSeverity::Warning,
598 };
599 let hint = diagnostic.hint;
600 let mut text = diagnostic.title;
601
602 if let Some(label) = diagnostic
603 .location
604 .as_ref()
605 .and_then(|location| location.label.text.as_deref())
606 {
607 text.push_str("\n\n");
608 text.push_str(label);
609 if !label.ends_with(['.', '?']) {
610 text.push('.');
611 }
612 }
613
614 if !diagnostic.text.is_empty() {
615 text.push_str("\n\n");
616 text.push_str(&diagnostic.text);
617 }
618
619 // TODO: Redesign the diagnostic type so that we can be sure there is always
620 // a location. Locationless diagnostics would be handled separately.
621 let location = diagnostic
622 .location
623 .expect("Diagnostic given to LSP without location");
624 let line_numbers = LineNumbers::new(&location.src);
625 let path = path_to_uri(location.path);
626 let range = src_span_to_lsp_range(location.label.span, &line_numbers);
627
628 let main = lsp::Diagnostic {
629 range,
630 severity: Some(severity),
631 code: None,
632 code_description: None,
633 source: None,
634 message: text,
635 related_information: related_information(
636 &hint,
637 &location.extra_labels,
638 &path,
639 &line_numbers,
640 range,
641 ),
642 tags: None,
643 data: None,
644 };
645
646 match hint {
647 Some(hint) => {
648 let hint = lsp::Diagnostic {
649 severity: Some(lsp::DiagnosticSeverity::Hint),
650 message: hint,
651 // Some editors require this kind of "link" to group diagnostics.
652 // For example, in Zed "go to next diagnostic" would move you from
653 // the warning to the hint in the same location without this.
654 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
655 location: lsp::Location { uri: path, range },
656 message: String::new(),
657 }]),
658 ..main.clone()
659 };
660 vec![main, hint]
661 }
662 None => vec![main],
663 }
664}
665
666fn related_information(
667 hint: &Option<String>,
668 extra_labels: &[ExtraLabel],
669 path: &Url,
670 line_numbers: &LineNumbers,
671 range: Range,
672) -> Option<Vec<lsp::DiagnosticRelatedInformation>> {
673 let mut related_info = Vec::with_capacity(extra_labels.len() + 1);
674
675 // The hint is included as a dedicated diagnostic _and_ the related information
676 // to maximize compatibility
677 if let Some(hint) = hint {
678 let hint = lsp::DiagnosticRelatedInformation {
679 message: hint.clone(),
680 location: lsp::Location {
681 uri: path.clone(),
682 range,
683 },
684 };
685 related_info.push(hint);
686 }
687
688 let additional_info = extra_labels.iter().map(|extra| {
689 let message = extra.label.text.clone().unwrap_or_default();
690 let location = match &extra.src_info {
691 Some((src, path)) => {
692 let line_numbers = LineNumbers::new(src);
693 lsp::Location {
694 uri: path_to_uri(path.clone()),
695 range: src_span_to_lsp_range(extra.label.span, &line_numbers),
696 }
697 }
698 _ => lsp::Location {
699 uri: path.clone(),
700 range: src_span_to_lsp_range(extra.label.span, line_numbers),
701 },
702 };
703 lsp::DiagnosticRelatedInformation { location, message }
704 });
705 related_info.extend(additional_info);
706
707 if related_info.is_empty() {
708 None
709 } else {
710 Some(related_info)
711 }
712}
713
714fn path_to_uri(path: Utf8PathBuf) -> Url {
715 let mut file: String = "file://".into();
716 file.push_str(&path.as_os_str().to_string_lossy());
717 Url::parse(&file).expect("path_to_uri URL parse")
718}