Something went wrong. Try again.
fork of Gleam compiler for Lua backend forked from gleam.run/gleam
Something went wrong. Try again.
28 kB · 766 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767// SPDX-License-Identifier: Apache-2.0// SPDX-FileCopyrightText: 2023 The Gleam contributors
use super::{ DownloadDependencies, MakeLocker, engine::{self, LanguageServerEngine}, feedback::{Feedback, FeedbackBookKeeper}, files::FileSystemProxy, messages::{Message, MessageBuffer, Next, Notification, Request}, progress::ConnectionProgressReporter, router::Router, src_span_to_lsp_range,};use camino::{Utf8Path, Utf8PathBuf};use debug_ignore::DebugIgnore;use gleam_core::{ Result, diagnostic::{Diagnostic, ExtraLabel, Level}, io::{BeamCompilerIO, CommandExecutor, FileSystemReader, FileSystemWriter},};use itertools::Itertools;use lsp_server::ResponseError;use lsp_types::{ self as lsp, FileOperationFilter, FileOperationOptions, FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions, InitializeParams, Position, PublishDiagnosticsParams, Range, RenameFilesParams, RenameOptions, TextEdit, Uri as Url, WorkspaceOptions,};use serde_json::Value as Json;use src_span::LineNumbers;use std::collections::{HashMap, HashSet};
/// This class is responsible for handling the language server protocol and/// delegating the work to the engine.////// - Configuring watching of the `gleam.toml` file./// - Decoding requests./// - Encoding responses./// - Sending diagnostics and messages to the client./// - Tracking the state of diagnostics and messages./// - Performing the initialisation handshake.///#[derive(Debug)]pub struct LanguageServer<'a, IO> { initialise_params: InitializeParams, connection: DebugIgnore<&'a lsp_server::Connection>, outside_of_project_feedback: FeedbackBookKeeper, router: Router<IO, ConnectionProgressReporter<'a>>, changed_projects: HashSet<Utf8PathBuf>, io: FileSystemProxy<IO>,}
impl<'a, IO> LanguageServer<'a, IO>where IO: FileSystemReader + FileSystemWriter + BeamCompilerIO + CommandExecutor + DownloadDependencies + MakeLocker + Clone,{ pub fn new(connection: &'a lsp_server::Connection, io: IO) -> Result<Self> { let initialise_params = initialisation_handshake(connection); let reporter = ConnectionProgressReporter::new(connection, &initialise_params); let io = FileSystemProxy::new(io); let router = Router::new(reporter, io.clone()); Ok(Self { connection: connection.into(), initialise_params, changed_projects: HashSet::new(), outside_of_project_feedback: FeedbackBookKeeper::default(), router, io, }) }
pub fn run(&mut self) -> Result<()> { self.start_watching_gleam_toml(); let mut buffer = MessageBuffer::new();
loop { match buffer.receive(*self.connection) { Next::Stop => break, Next::MorePlease => (), Next::Handle(messages) => { for message in messages { self.handle_message(message); } } } }
Ok(()) }
fn handle_message(&mut self, message: Message) { match message { Message::Request(id, request) => self.handle_request(id, request), Message::Notification(notification) => self.handle_notification(notification), } }
fn handle_request(&mut self, id: lsp_server::RequestId, request: Request) { let (outcome, feedback) = match request { Request::Format(param) => self.format(param), Request::Hover(param) => self.hover(param), Request::GoToDefinition(param) => self.goto_definition(param), Request::Completion(param) => self.completion(param), Request::CodeAction(param) => self.code_action(param), Request::SignatureHelp(param) => self.signature_help(param), Request::DocumentSymbol(param) => self.document_symbol(param), Request::FoldingRange(param) => self.folding_range(param), Request::PrepareRename(param) => self.prepare_rename(param), Request::Rename(param) => self.rename(param), Request::GoToTypeDefinition(param) => self.goto_type_definition(param), Request::FindReferences(param) => self.find_references(param), Request::DocumentHighlight(param) => self.document_highlight(param), Request::RenameFiles(param) => self.rename_files(param), };
self.publish_feedback(feedback);
let response = match outcome { Ok(payload) => lsp_server::Response { id, error: None, result: Some(payload), }, Err(error) => lsp_server::Response { id, error: Some(error), result: None, }, };
self.connection .sender .send(lsp_server::Message::Response(response)) .expect("channel send LSP response"); }
fn handle_notification(&mut self, notification: Notification) { let feedback = match notification { Notification::CompilePlease => self.compile_please(), Notification::SourceFileOpened { path, text } => self.source_file_opened(path, text), Notification::SourceFileClosed { path } => self.source_file_closed(path), Notification::SourceFileSaved { path } => self.discard_in_memory_cache(path), Notification::SourceFileChangedInMemory { path, text } => { self.cache_file_in_memory(path, text) } Notification::ConfigFileChanged { path } => self.watched_files_changed(path), }; self.publish_feedback(feedback); }
fn publish_feedback(&self, feedback: Feedback) { self.publish_diagnostics(feedback.diagnostics); self.publish_messages(feedback.messages); }
fn publish_diagnostics(&self, diagnostics: HashMap<Utf8PathBuf, Vec<Diagnostic>>) { for (path, diagnostics) in diagnostics { let diagnostics = diagnostics .into_iter() .flat_map(diagnostic_to_lsp) .collect::<Vec<_>>(); let uri = path_to_uri(path);
// Publish the diagnostics let diagnostic_params = PublishDiagnosticsParams { uri, diagnostics, version: None, }; let notification = lsp_server::Notification { method: "textDocument/publishDiagnostics".into(), params: serde_json::to_value(diagnostic_params) .expect("textDocument/publishDiagnostics to json"), }; self.connection .sender .send(lsp_server::Message::Notification(notification)) .expect("send textDocument/publishDiagnostics"); } }
fn start_watching_gleam_toml(&mut self) { let supports_watch_files = self .initialise_params .capabilities .workspace .as_ref() .and_then(|w| w.did_change_watched_files) .map(|wf| wf.dynamic_registration.unwrap_or(false)) .unwrap_or(false);
if !supports_watch_files { tracing::warn!("lsp_client_cannot_watch_gleam_toml"); return; }
// Register gleam.toml as a watched file so we get a notification when // it changes and thus know that we need to rebuild the entire project. let watch_config = lsp::Registration { id: "watch-gleam-toml".into(), method: "workspace/didChangeWatchedFiles".into(), register_options: Some( serde_json::value::to_value(lsp::DidChangeWatchedFilesRegistrationOptions { watchers: vec![lsp::FileSystemWatcher { glob_pattern: "**/gleam.toml".to_string().into(), kind: Some(lsp::WatchKind::Change), }], }) .expect("workspace/didChangeWatchedFiles to json"), ), }; let request = lsp_server::Request { id: 1.into(), method: "client/registerCapability".into(), params: serde_json::value::to_value(lsp::RegistrationParams { registrations: vec![watch_config], }) .expect("client/registerCapability to json"), }; self.connection .sender .send(lsp_server::Message::Request(request)) .expect("send client/registerCapability"); }
fn publish_messages(&self, messages: Vec<Diagnostic>) { for message in messages { let params = lsp::ShowMessageParams { kind: match message.level { Level::Error => lsp::MessageType::Error, Level::Warning => lsp::MessageType::Warning, }, message: message.text, }; let notification = lsp_server::Notification { method: "window/showMessage".into(), params: serde_json::to_value(params).expect("window/showMessage to json"), }; self.connection .sender .send(lsp_server::Message::Notification(notification)) .expect("send window/showMessage"); } }
fn respond_with_engine<T, Handler>( &mut self, path: Utf8PathBuf, handler: Handler, ) -> (Result<Json, ResponseError>, Feedback) where T: serde::Serialize, Handler: FnOnce( &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>, ) -> engine::Response<T>, { self.fallible_respond_with_engine(path, |engine| { let response = handler(engine); engine::Response { result: response.result.map(Ok), warnings: response.warnings, compilation: response.compilation, } }) }
fn fallible_respond_with_engine<T, Handler>( &mut self, path: Utf8PathBuf, handler: Handler, ) -> (Result<Json, ResponseError>, Feedback) where T: serde::Serialize, Handler: FnOnce( &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>, ) -> engine::Response<Result<T, ResponseError>>, { match self.router.project_for_path(path) { Ok(Some(project)) => { let engine::Response { result, warnings, compilation, } = handler(&mut project.engine); match result { Ok(Ok(value)) => { let feedback = project.feedback.response(compilation, warnings); let json = serde_json::to_value(value).expect("response to json"); (Ok(json), feedback) } Ok(Err(error)) => { let feedback = project.feedback.response(compilation, warnings); (Err(error), feedback) } Err(e) => { let feedback = project.feedback.build_with_error(e, compilation, warnings); (Ok(Json::Null), feedback) } } }
Ok(None) => (Ok(Json::Null), Feedback::default()),
Err(error) => ( Ok(Json::Null), self.outside_of_project_feedback.error(error), ), } }
fn path_error_response( &mut self, path: Utf8PathBuf, error: gleam_core::Error, ) -> (Result<Json, ResponseError>, Feedback) { let feedback = match self.router.project_for_path(path) { Ok(Some(project)) => project.feedback.error(error), Ok(None) | Err(_) => self.outside_of_project_feedback.error(error), }; (Ok(Json::Null), feedback) }
fn format( &mut self, params: lsp::DocumentFormattingParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document.uri); let mut new_text = String::new();
let src = match self.io.read(&path) { Ok(src) => src.into(), Err(error) => return self.path_error_response(path, error), };
if let Err(error) = gleam_format::pretty(&mut new_text, &src, &path) { return self.path_error_response(path, error); }
let line_count = src.lines().count() as u32;
let edit = TextEdit { range: Range::new(Position::new(0, 0), Position::new(line_count, 0)), new_text, }; let json = serde_json::to_value(vec![edit]).expect("to JSON value");
(Ok(json), Feedback::default()) }
fn hover(&mut self, params: lsp::HoverParams) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri); self.respond_with_engine(path, |engine| engine.hover(params)) }
fn goto_definition( &mut self, params: lsp::DefinitionParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri); self.respond_with_engine(path, |engine| engine.goto_definition(params)) }
fn goto_type_definition( &mut self, params: lsp_types::TypeDefinitionParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri); self.respond_with_engine(path, |engine| engine.goto_type_definition(params)) }
fn completion( &mut self, params: lsp::CompletionParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri);
let src = match self.io.read(&path) { Ok(src) => src.into(), Err(error) => return self.path_error_response(path, error), }; self.respond_with_engine(path, |engine| { engine.completion(params.text_document_position_params, src) }) }
fn signature_help( &mut self, params: lsp_types::SignatureHelpParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri); self.respond_with_engine(path, |engine| engine.signature_help(params)) }
fn code_action( &mut self, params: lsp::CodeActionParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document.uri); self.respond_with_engine(path, |engine| engine.code_actions(params)) }
fn document_symbol( &mut self, params: lsp::DocumentSymbolParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document.uri); self.respond_with_engine(path, |engine| engine.document_symbol(params)) }
fn folding_range( &mut self, params: lsp::FoldingRangeParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document.uri); self.respond_with_engine(path, |engine| engine.folding_range(params)) }
fn prepare_rename( &mut self, params: lsp::PrepareRenameParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri); self.respond_with_engine(path, |engine| engine.prepare_rename(params)) }
fn rename(&mut self, params: lsp::RenameParams) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri); self.fallible_respond_with_engine( path, |engine: &mut LanguageServerEngine<IO, ConnectionProgressReporter<'a>>| { engine.rename(params) }, ) }
fn rename_files( &mut self, params: RenameFilesParams, ) -> (Result<Json, ResponseError>, Feedback) { let renames = params .files .into_iter() .map(|file| { ( Url::parse(&file.old_uri).expect("Uri should be valid"), Url::parse(&file.new_uri).expect("Uri should be valid"), ) }) .collect_vec();
let Some((_, first_renamed_file)) = renames.first() else { return (Ok(serde_json::json!(null)), Feedback::none()); };
self.respond_with_engine(super::path(first_renamed_file), |engine| { engine.rename_files(renames) }) }
fn find_references( &mut self, params: lsp_types::ReferenceParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri); self.respond_with_engine(path, |engine| engine.find_references(params)) }
fn document_highlight( &mut self, params: lsp_types::DocumentHighlightParams, ) -> (Result<Json, ResponseError>, Feedback) { let path = super::path(¶ms.text_document_position_params.text_document.uri); self.respond_with_engine(path, |engine| engine.document_highlight(params)) }
fn cache_file_in_memory(&mut self, path: Utf8PathBuf, text: String) -> Feedback { self.project_changed(&path); if let Err(error) = self.io.write_mem_cache(&path, &text) { return self.outside_of_project_feedback.error(error); } Feedback::none() }
fn discard_in_memory_cache(&mut self, path: Utf8PathBuf) -> Feedback { self.project_changed(&path); if let Err(error) = self.io.delete_mem_cache(&path) { return self.outside_of_project_feedback.error(error); } Feedback::none() }
fn watched_files_changed(&mut self, path: Utf8PathBuf) -> Feedback { self.router.delete_engine_for_path(&path); Feedback::none() }
fn compile_please(&mut self) -> Feedback { let mut accumulator = Feedback::none(); let projects = std::mem::take(&mut self.changed_projects); for path in projects { let (_, feedback) = self.respond_with_engine(path, |this| this.compile_please()); accumulator.append_feedback(feedback); } accumulator }
fn project_changed(&mut self, path: &Utf8Path) { let project_path = self.router.project_path(path); if let Some(project_path) = project_path { _ = self.changed_projects.insert(project_path); } }
fn source_file_opened(&mut self, path: Utf8PathBuf, text: String) -> Feedback { let mut feedback = self.cache_file_in_memory(path.clone(), text); if let Ok(Some(project)) = self.router.project_for_path(path.clone()) { feedback.append_feedback(project.feedback.open_file(path)); } feedback }
fn source_file_closed(&mut self, path: Utf8PathBuf) -> Feedback { let mut feedback = self.discard_in_memory_cache(path.clone()); if let Ok(Some(project)) = self.router.project_for_path(path.clone()) { feedback.append_feedback(project.feedback.close_file(&path)); } feedback }}
fn initialisation_handshake(connection: &lsp_server::Connection) -> InitializeParams { let server_capabilities = lsp::ServerCapabilities { text_document_sync: Some( lsp::TextDocumentSyncOptions { open_close: Some(true), change: Some(lsp::TextDocumentSyncKind::Full), will_save: None, will_save_wait_until: None, save: Some( lsp::SaveOptions { include_text: Some(false), } .into(), ), } .into(), ), selection_range_provider: None, hover_provider: Some(true.into()), completion_provider: Some(lsp::CompletionOptions { resolve_provider: None, trigger_characters: Some(vec![".".into()]), all_commit_characters: None, work_done_progress_options: lsp::WorkDoneProgressOptions { work_done_progress: None, }, completion_item: None, }), signature_help_provider: Some(lsp::SignatureHelpOptions { trigger_characters: Some(vec!["(".into(), ",".into(), ":".into()]), retrigger_characters: None, work_done_progress_options: lsp::WorkDoneProgressOptions { work_done_progress: None, }, }), definition_provider: Some(true.into()), type_definition_provider: Some(true.into()), implementation_provider: None, references_provider: Some(true.into()), document_highlight_provider: Some(true.into()), document_symbol_provider: Some(true.into()), workspace_symbol_provider: None, code_action_provider: Some(true.into()), code_lens_provider: None, document_formatting_provider: Some(true.into()), document_range_formatting_provider: None, document_on_type_formatting_provider: None, rename_provider: Some( RenameOptions { prepare_provider: Some(true), work_done_progress_options: lsp::WorkDoneProgressOptions { work_done_progress: None, }, } .into(), ), document_link_provider: None, color_provider: None, folding_range_provider: Some(true.into()), declaration_provider: None, execute_command_provider: None, workspace: Some(WorkspaceOptions { workspace_folders: None, file_operations: Some(FileOperationOptions { did_create: None, will_create: None, will_rename: Some(FileOperationRegistrationOptions { filters: vec![FileOperationFilter { scheme: Some("file".into()), pattern: FileOperationPattern { glob: "**/*.gleam".into(), matches: Some(FileOperationPatternKind::File), options: None, }, }], }), did_rename: None, did_delete: None, will_delete: None, }), text_document_content: None, }), call_hierarchy_provider: None, semantic_tokens_provider: None, moniker_provider: None, linked_editing_range_provider: None, experimental: None, position_encoding: None, inline_value_provider: None, inlay_hint_provider: None, diagnostic_provider: None, type_hierarchy_provider: None, notebook_document_sync: None, inline_completion_provider: None, }; let server_capabilities_json = serde_json::to_value(server_capabilities).expect("server_capabilities_serde"); let initialise_params_json = connection .initialize(server_capabilities_json) .expect("LSP initialize"); let initialise_params: InitializeParams = serde_json::from_value(initialise_params_json).expect("LSP InitializeParams from json"); initialise_params}
fn diagnostic_to_lsp(diagnostic: Diagnostic) -> Vec<lsp::Diagnostic> { let severity = match diagnostic.level { Level::Error => lsp::DiagnosticSeverity::Error, Level::Warning => lsp::DiagnosticSeverity::Warning, }; let hint = diagnostic.hint; let mut text = diagnostic.title;
if let Some(label) = diagnostic .location .as_ref() .and_then(|location| location.label.text.as_deref()) { text.push_str("\n\n"); text.push_str(label); if !label.ends_with(['.', '?']) { text.push('.'); } }
if !diagnostic.text.is_empty() { text.push_str("\n\n"); text.push_str(&diagnostic.text); }
// TODO: Redesign the diagnostic type so that we can be sure there is always // a location. Locationless diagnostics would be handled separately. let location = diagnostic .location .expect("Diagnostic given to LSP without location"); let line_numbers = LineNumbers::new(&location.src); let path = path_to_uri(location.path); let range = src_span_to_lsp_range(location.label.span, &line_numbers);
let main = lsp::Diagnostic { range, severity: Some(severity), code: None, code_description: None, source: None, message: text, related_information: related_information( &hint, &location.extra_labels, &path, &line_numbers, range, ), tags: None, data: None, };
match hint { Some(hint) => { let hint = lsp::Diagnostic { severity: Some(lsp::DiagnosticSeverity::Hint), message: hint, // Some editors require this kind of "link" to group diagnostics. // For example, in Zed "go to next diagnostic" would move you from // the warning to the hint in the same location without this. related_information: Some(vec![lsp::DiagnosticRelatedInformation { location: lsp::Location { uri: path, range }, message: String::new(), }]), ..main.clone() }; vec![main, hint] } None => vec![main], }}
fn related_information( hint: &Option<String>, extra_labels: &[ExtraLabel], path: &Url, line_numbers: &LineNumbers, range: Range,) -> Option<Vec<lsp::DiagnosticRelatedInformation>> { let mut related_info = Vec::with_capacity(extra_labels.len() + 1);
// The hint is included as a dedicated diagnostic _and_ the related information // to maximize compatibility if let Some(hint) = hint { let hint = lsp::DiagnosticRelatedInformation { message: hint.clone(), location: lsp::Location { uri: path.clone(), range, }, }; related_info.push(hint); }
let additional_info = extra_labels.iter().map(|extra| { let message = extra.label.text.clone().unwrap_or_default(); let location = match &extra.src_info { Some((src, path)) => { let line_numbers = LineNumbers::new(src); lsp::Location { uri: path_to_uri(path.clone()), range: src_span_to_lsp_range(extra.label.span, &line_numbers), } } _ => lsp::Location { uri: path.clone(), range: src_span_to_lsp_range(extra.label.span, line_numbers), }, }; lsp::DiagnosticRelatedInformation { location, message } }); related_info.extend(additional_info);
if related_info.is_empty() { None } else { Some(related_info) }}
fn path_to_uri(path: Utf8PathBuf) -> Url { let mut file: String = "file://".into(); file.push_str(&path.as_os_str().to_string_lossy()); Url::parse(&file).expect("path_to_uri URL parse")}