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