diff --git a/compiler-core/src/ast/typed.rs b/compiler-core/src/ast/typed.rs index c3daced0cb5..0790f2bce1a 100644 --- a/compiler-core/src/ast/typed.rs +++ b/compiler-core/src/ast/typed.rs @@ -158,6 +158,35 @@ pub enum TypedExpr { } impl TypedExpr { + /// Determines if the expression is a simple literal whose inlayHints must not be showed + /// in a pipeline chain + pub fn is_simple_lit(&self) -> bool { + match self { + TypedExpr::Int { .. } + | TypedExpr::Float { .. } + | TypedExpr::String { .. } + | TypedExpr::BitArray { .. } => true, + TypedExpr::Block { .. } + | TypedExpr::Pipeline { .. } + | TypedExpr::Var { .. } + | TypedExpr::Fn { .. } + | TypedExpr::List { .. } + | TypedExpr::Call { .. } + | TypedExpr::BinOp { .. } + | TypedExpr::Case { .. } + | TypedExpr::RecordAccess { .. } + | TypedExpr::ModuleSelect { .. } + | TypedExpr::Tuple { .. } + | TypedExpr::TupleIndex { .. } + | TypedExpr::Todo { .. } + | TypedExpr::Panic { .. } + | TypedExpr::RecordUpdate { .. } + | TypedExpr::NegateBool { .. } + | TypedExpr::NegateInt { .. } + | TypedExpr::Invalid { .. } => false, + } + } + pub fn is_println(&self) -> bool { let fun = match self { TypedExpr::Call { fun, args, .. } if args.len() == 1 => fun.as_ref(), diff --git a/compiler-core/src/language_server.rs b/compiler-core/src/language_server.rs index fbc80082ca0..9b6d7d81290 100644 --- a/compiler-core/src/language_server.rs +++ b/compiler-core/src/language_server.rs @@ -1,10 +1,12 @@ mod code_action; mod compiler; mod completer; +mod configuration; mod edits; mod engine; mod feedback; mod files; +mod inlay_hints; mod messages; mod progress; mod router; @@ -39,13 +41,15 @@ pub trait DownloadDependencies { fn download_dependencies(&self, paths: &ProjectPaths) -> Result; } -pub fn src_span_to_lsp_range(location: SrcSpan, line_numbers: &LineNumbers) -> Range { - let start = line_numbers.line_and_column_number(location.start); - let end = line_numbers.line_and_column_number(location.end); +pub fn src_offset_to_lsp_position(offset: u32, line_numbers: &LineNumbers) -> Position { + let line_col = line_numbers.line_and_column_number(offset); + Position::new(line_col.line - 1, line_col.column - 1) +} +pub fn src_span_to_lsp_range(location: SrcSpan, line_numbers: &LineNumbers) -> Range { Range::new( - Position::new(start.line - 1, start.column - 1), - Position::new(end.line - 1, end.column - 1), + src_offset_to_lsp_position(location.start, line_numbers), + src_offset_to_lsp_position(location.end, line_numbers), ) } diff --git a/compiler-core/src/language_server/configuration.rs b/compiler-core/src/language_server/configuration.rs new file mode 100644 index 00000000000..aff2a4ff290 --- /dev/null +++ b/compiler-core/src/language_server/configuration.rs @@ -0,0 +1,17 @@ +use serde::Deserialize; +use std::sync::{Arc, RwLock}; + +pub type SharedConfig = Arc>; + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub struct Configuration { + pub inlay_hints: InlayHintsConfig, +} + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] +#[serde(default)] +pub struct InlayHintsConfig { + /// Whether to show type inlay hints of multiline pipelines + pub pipelines: bool, +} diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs index 2242b9432b6..cdebf2677d6 100644 --- a/compiler-core/src/language_server/engine.rs +++ b/compiler-core/src/language_server/engine.rs @@ -23,7 +23,7 @@ use ecow::EcoString; use itertools::Itertools; use lsp::CodeAction; use lsp_types::{ - self as lsp, DocumentSymbol, Hover, HoverContents, MarkedString, Position, Range, + self as lsp, DocumentSymbol, Hover, HoverContents, InlayHint, MarkedString, Position, Range, SignatureHelp, SymbolKind, SymbolTag, TextEdit, Url, }; use std::sync::Arc; @@ -35,7 +35,8 @@ use super::{ RedundantTupleInCaseSubject, }, completer::Completer, - signature_help, src_span_to_lsp_range, DownloadDependencies, MakeLocker, + configuration::SharedConfig, + inlay_hints, signature_help, src_span_to_lsp_range, DownloadDependencies, MakeLocker, }; #[derive(Debug, PartialEq, Eq)] @@ -74,6 +75,9 @@ pub struct LanguageServerEngine { /// Used to know if to show the "View on HexDocs" link /// when hovering on an imported value hex_deps: std::collections::HashSet, + + /// Configuration the user has set in their editor. + pub(crate) user_config: SharedConfig, } impl<'a, IO, Reporter> LanguageServerEngine @@ -93,6 +97,7 @@ where progress_reporter: Reporter, io: FileSystemProxy, paths: ProjectPaths, + user_config: SharedConfig, ) -> Result { let locker = io.inner().make_locker(&paths, config.target)?; @@ -129,6 +134,7 @@ where paths, error: None, hex_deps, + user_config, }) } @@ -444,6 +450,28 @@ where }) } + pub fn inlay_hints(&mut self, params: lsp::InlayHintParams) -> Response> { + self.respond(|this| { + let Ok(config) = this.user_config.read() else { + return Ok(vec![]); + }; + + if !config.inlay_hints.pipelines { + return Ok(vec![]); + } + + let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { + return Ok(vec![]); + }; + + let line_numbers = LineNumbers::new(&module.code); + + let hints = inlay_hints::get_inlay_hints(module.ast.clone(), &line_numbers); + + Ok(hints) + }) + } + fn respond(&mut self, handler: impl FnOnce(&mut Self) -> Result) -> Response { let result = handler(self); let warnings = self.take_warnings(); diff --git a/compiler-core/src/language_server/inlay_hints.rs b/compiler-core/src/language_server/inlay_hints.rs new file mode 100644 index 00000000000..0be5c0f351e --- /dev/null +++ b/compiler-core/src/language_server/inlay_hints.rs @@ -0,0 +1,109 @@ +use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel}; + +use crate::{ + ast::{ + visit::{self, Visit}, + SrcSpan, TypedAssignment, TypedExpr, TypedModule, + }, + line_numbers::LineNumbers, + type_::pretty::Printer, +}; + +use super::src_offset_to_lsp_position; + +struct InlayHintsVisitor<'a> { + hints: Vec, + line_numbers: &'a LineNumbers, +} + +impl<'a> InlayHintsVisitor<'a> { + fn new(line_numbers: &'a LineNumbers) -> InlayHintsVisitor<'a> { + InlayHintsVisitor { + hints: vec![], + line_numbers, + } + } +} + +fn default_inlay_hint(line_numbers: &LineNumbers, offset: u32, label: String) -> InlayHint { + let position = src_offset_to_lsp_position(offset, line_numbers); + + InlayHint { + position, + label: InlayHintLabel::String(label), + kind: Some(InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(true), + padding_right: None, + data: None, + } +} + +impl<'a, 'ast> Visit<'ast> for InlayHintsVisitor<'a> { + fn visit_typed_expr_pipeline( + &mut self, + _location: &'ast SrcSpan, + assignments: &'ast [TypedAssignment], + finally: &'ast TypedExpr, + ) { + let mut prev_hint: Option<(u32, Option)> = None; + for assign in assignments { + let this_line: u32 = self + .line_numbers + .line_and_column_number(assign.location.end) + .line; + + if let Some((prev_line, prev_hint)) = prev_hint { + if prev_line != this_line { + if let Some(prev_hint) = prev_hint { + self.hints.push(prev_hint); + } + } + }; + + let this_hint = default_inlay_hint( + self.line_numbers, + assign.location.end, + Printer::new().pretty_print(assign.type_().as_ref(), 0), + ); + + prev_hint = Some(( + this_line, + if assign.value.is_simple_lit() { + None + } else { + Some(this_hint) + }, + )); + + visit::visit_typed_expr(self, &assign.value); + } + + if let Some((prev_line, prev_hint)) = prev_hint { + let this_line = self + .line_numbers + .line_and_column_number(finally.location().end) + .line; + if this_line != prev_line { + if let Some(prev_hint) = prev_hint { + self.hints.push(prev_hint); + } + let hint = default_inlay_hint( + self.line_numbers, + finally.location().end, + Printer::new().pretty_print(finally.type_().as_ref(), 0), + ); + self.hints.push(hint); + } + } + + visit::visit_typed_expr(self, finally); + } +} + +pub fn get_inlay_hints(typed_module: TypedModule, line_numbers: &LineNumbers) -> Vec { + let mut visitor = InlayHintsVisitor::new(line_numbers); + visitor.visit_typed_module(&typed_module); + visitor.hints +} diff --git a/compiler-core/src/language_server/messages.rs b/compiler-core/src/language_server/messages.rs index 525b22af7bd..d117bbbc551 100644 --- a/compiler-core/src/language_server/messages.rs +++ b/compiler-core/src/language_server/messages.rs @@ -1,3 +1,4 @@ +use crate::language_server::configuration::Configuration; use camino::Utf8PathBuf; use lsp::{ notification::{DidChangeWatchedFiles, DidOpenTextDocument}, @@ -8,14 +9,15 @@ use lsp_types::{ notification::{DidChangeTextDocument, DidCloseTextDocument, DidSaveTextDocument}, request::{ CodeActionRequest, Completion, DocumentSymbolRequest, Formatting, HoverRequest, - SignatureHelpRequest, + InlayHintRequest, SignatureHelpRequest, }, }; -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; #[derive(Debug)] pub enum Message { Request(lsp_server::RequestId, Request), + Response(Response), Notification(Notification), } @@ -28,6 +30,12 @@ pub enum Request { CodeAction(lsp::CodeActionParams), SignatureHelp(lsp::SignatureHelpParams), DocumentSymbol(lsp::DocumentSymbolParams), + ShowInlayHints(lsp::InlayHintParams), +} + +#[derive(Debug)] +pub enum Response { + Configuration(Configuration), } impl Request { @@ -62,6 +70,10 @@ impl Request { let params = cast_request::(request); Some(Message::Request(id, Request::DocumentSymbol(params))) } + "textDocument/inlayHint" => { + let params = cast_request::(request); + Some(Message::Request(id, Request::ShowInlayHints(params))) + } _ => None, } } @@ -75,6 +87,8 @@ pub enum Notification { SourceFileMatchesDisc { path: Utf8PathBuf }, /// gleam.toml has changed. ConfigFileChanged { path: Utf8PathBuf }, + /// The user edited a client config option + ConfigChanged, /// It's time to compile all open projects. CompilePlease, } @@ -121,6 +135,11 @@ impl Notification { }; Some(Message::Notification(notification)) } + + "workspace/didChangeConfiguration" => { + Some(Message::Notification(Notification::ConfigChanged)) + } + _ => None, } } @@ -138,15 +157,19 @@ pub enum Next { /// - A short pause in messages is detected, indicating the programmer has /// stopped typing for a moment and would benefit from feedback. /// - A request type message is received, which requires an immediate response. -/// +#[derive(Debug)] pub struct MessageBuffer { messages: Vec, + next_request_id: i32, + response_handlers: HashMap, } impl MessageBuffer { pub fn new() -> Self { Self { messages: Vec::new(), + next_request_id: 1, + response_handlers: Default::default(), } } @@ -206,7 +229,52 @@ impl MessageBuffer { Next::MorePlease } - fn response(&mut self, _: lsp_server::Response) -> Next { + pub fn make_request( + &mut self, + method: impl Into, + params: impl serde::Serialize, + handler: Option, + ) -> lsp_server::Request { + let id = self.next_request_id; + self.next_request_id += 1; + let request = lsp_server::Request { + id: id.into(), + method: method.into(), + params: serde_json::value::to_value(params).expect("serialisation should never fail"), + }; + + if let Some(handler) = handler { + _ = self.response_handlers.insert(id.into(), handler); + } + + request + } + + fn configuration_update_received(&mut self, result: serde_json::Value) -> Next { + let parsed_update_items: Result<(Configuration,), _> = serde_json::from_value(result); + let Ok((parsed_config,)) = parsed_update_items else { + return Next::MorePlease; + }; + + let message = Message::Response(Response::Configuration(parsed_config)); + self.messages.push(message); + + Next::Handle(self.take_messages()) + } + + fn handle_response(&mut self, handler: ResponseHandler, result: serde_json::Value) -> Next { + match handler { + ResponseHandler::UpdateConfiguration => self.configuration_update_received(result), + } + } + + fn response(&mut self, response: lsp_server::Response) -> Next { + if let Some(handler) = self.response_handlers.remove(&response.id) { + if let Some(result) = response.result { + return self.handle_response(handler, result); + } + } + // We do not use or expect responses from the client currently. Next::MorePlease } @@ -251,3 +319,8 @@ where .extract::(N::METHOD) .expect("cast notification") } + +#[derive(Debug)] +pub enum ResponseHandler { + UpdateConfiguration, +} diff --git a/compiler-core/src/language_server/router.rs b/compiler-core/src/language_server/router.rs index 6f180edf80a..caaf9b24f5e 100644 --- a/compiler-core/src/language_server/router.rs +++ b/compiler-core/src/language_server/router.rs @@ -1,3 +1,4 @@ +use super::{configuration::SharedConfig, feedback::FeedbackBookKeeper}; use crate::{ build::SourceFingerprint, error::{FileIoAction, FileKind}, @@ -9,15 +10,12 @@ use crate::{ paths::ProjectPaths, Error, Result, }; +use camino::{Utf8Path, Utf8PathBuf}; use std::{ collections::{hash_map::Entry, HashMap}, time::SystemTime, }; -use camino::{Utf8Path, Utf8PathBuf}; - -use super::feedback::FeedbackBookKeeper; - /// The language server instance serves a language client, typically a text /// editor. The editor could have multiple Gleam projects open at once, so run /// an instance of the language server engine for each project. @@ -30,6 +28,7 @@ pub(crate) struct Router { io: FileSystemProxy, engines: HashMap>, progress_reporter: Reporter, + user_config: SharedConfig, } impl<'a, IO, Reporter> Router @@ -44,11 +43,16 @@ where // IO to be supplied from inside of gleam-core Reporter: ProgressReporter + Clone + 'a, { - pub fn new(progress_reporter: Reporter, io: FileSystemProxy) -> Self { + pub fn new( + progress_reporter: Reporter, + io: FileSystemProxy, + user_config: SharedConfig, + ) -> Self { Self { io, engines: HashMap::new(), progress_reporter, + user_config, } } @@ -84,8 +88,12 @@ where Ok(Some(match self.engines.entry(path.clone()) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => { - let project = - Self::new_project(path, self.io.clone(), self.progress_reporter.clone())?; + let project = Self::new_project( + path, + self.io.clone(), + self.progress_reporter.clone(), + self.user_config.clone(), + )?; entry.insert(project) } })) @@ -123,19 +131,21 @@ where path: Utf8PathBuf, io: FileSystemProxy, progress_reporter: Reporter, + user_config: SharedConfig, ) -> Result, Error> { tracing::info!(?path, "creating_new_language_server_engine"); let paths = ProjectPaths::new(path); let config_path = paths.root_config(); let modification_time = io.modification_time(&config_path)?; let toml = io.read(&config_path)?; - let config = toml::from_str(&toml).map_err(|e| Error::FileIo { + let package_config = toml::from_str(&toml).map_err(|e| Error::FileIo { action: FileIoAction::Parse, kind: FileKind::File, path: config_path, err: Some(e.to_string()), })?; - let engine = LanguageServerEngine::new(config, progress_reporter, io, paths)?; + let engine = + LanguageServerEngine::new(package_config, progress_reporter, io, paths, user_config)?; let project = Project { engine, feedback: FeedbackBookKeeper::default(), diff --git a/compiler-core/src/language_server/server.rs b/compiler-core/src/language_server/server.rs index 9d37001d9c0..4e907828fea 100644 --- a/compiler-core/src/language_server/server.rs +++ b/compiler-core/src/language_server/server.rs @@ -1,5 +1,6 @@ use super::{ - messages::{Message, MessageBuffer, Next, Notification, Request}, + configuration::SharedConfig, + messages::{Message, MessageBuffer, Next, Notification, Request, Response, ResponseHandler}, progress::ConnectionProgressReporter, }; use crate::{ @@ -19,8 +20,8 @@ use camino::{Utf8Path, Utf8PathBuf}; use debug_ignore::DebugIgnore; use itertools::Itertools; use lsp_types::{ - self as lsp, HoverProviderCapability, InitializeParams, Position, PublishDiagnosticsParams, - Range, TextEdit, Url, + self as lsp, ConfigurationItem, HoverProviderCapability, InitializeParams, Position, + PublishDiagnosticsParams, Range, TextEdit, Url, }; use serde_json::Value as Json; use std::collections::{HashMap, HashSet}; @@ -43,6 +44,8 @@ pub struct LanguageServer<'a, IO> { router: Router>, changed_projects: HashSet, io: FileSystemProxy, + message_buffer: MessageBuffer, + config: SharedConfig, } impl<'a, IO> LanguageServer<'a, IO> @@ -58,23 +61,29 @@ where 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()); + + let config = SharedConfig::default(); + let router = Router::new(reporter, io.clone(), config.clone()); + Ok(Self { connection: connection.into(), initialise_params, changed_projects: HashSet::new(), outside_of_project_feedback: FeedbackBookKeeper::default(), + message_buffer: MessageBuffer::new(), router, io, + config, }) } pub fn run(&mut self) -> Result<()> { self.start_watching_gleam_toml(); - let mut buffer = MessageBuffer::new(); + self.start_watching_config(); + let _ = self.request_configuration(); loop { - match buffer.receive(*self.connection) { + match self.message_buffer.receive(*self.connection) { Next::Stop => break, Next::MorePlease => (), Next::Handle(messages) => { @@ -92,9 +101,37 @@ where match message { Message::Request(id, request) => self.handle_request(id, request), Message::Notification(notification) => self.handle_notification(notification), + Message::Response(response) => self.handle_response(response), + } + } + + fn handle_response(&mut self, response: Response) { + match response { + Response::Configuration(updated_config) => { + { + let mut config = self.config.write().expect("cannot write config"); + *config = updated_config; + } + + let _ = self.inlay_hints_refresh(); + } } } + fn send_request( + &mut self, + method: &str, + params: impl serde::Serialize, + handler: Option, + ) { + let request = self.message_buffer.make_request(method, params, handler); + + self.connection + .sender + .send(lsp_server::Message::Request(request)) + .unwrap_or_else(|_| panic!("send {method}")); + } + fn handle_request(&mut self, id: lsp_server::RequestId, request: Request) { let (payload, feedback) = match request { Request::Format(param) => self.format(param), @@ -104,6 +141,7 @@ where Request::CodeAction(param) => self.code_action(param), Request::SignatureHelp(param) => self.signature_help(param), Request::DocumentSymbol(param) => self.document_symbol(param), + Request::ShowInlayHints(param) => self.show_inlay_hints(param), }; self.publish_feedback(feedback); @@ -127,6 +165,7 @@ where self.cache_file_in_memory(path, text) } Notification::ConfigFileChanged { path } => self.watched_files_changed(path), + Notification::ConfigChanged => self.request_configuration(), }; self.publish_feedback(feedback); } @@ -162,6 +201,35 @@ where } } + fn start_watching_config(&mut self) { + let supports_configuration = self + .initialise_params + .capabilities + .workspace + .as_ref() + .and_then(|w| w.did_change_configuration) + .map(|wf| wf.dynamic_registration == Some(true)) + .unwrap_or(false); + + if !supports_configuration { + tracing::warn!("lsp_client_cannot_watch_configuration"); + return; + } + + let watch_config = lsp::Registration { + id: "watch-user-configuration".into(), + method: "workspace/didChangeConfiguration".into(), + register_options: None, + }; + self.send_request( + "client/registerCapability", + lsp::RegistrationParams { + registrations: vec![watch_config], + }, + None, + ); + } + fn start_watching_gleam_toml(&mut self) { let supports_watch_files = self .initialise_params @@ -192,18 +260,14 @@ where .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 { + + self.send_request( + "client/registerCapability", + lsp::RegistrationParams { registrations: vec![watch_config], - }) - .expect("client/registerCapability to json"), - }; - self.connection - .sender - .send(lsp_server::Message::Request(request)) - .expect("send client/registerCapability"); + }, + None, + ); } fn publish_messages(&self, messages: Vec) { @@ -263,6 +327,54 @@ where } } + fn inlay_hints_refresh(&mut self) -> Feedback { + let supports_refresh = self + .initialise_params + .capabilities + .workspace + .as_ref() + .and_then(|capabilties| { + capabilties + .inlay_hint + .as_ref() + .and_then(|h| h.refresh_support) + }) + .unwrap_or(false); + + if supports_refresh { + self.send_request("workspace/inlayHint/refresh", (), None); + } + Feedback::default() + } + + fn request_configuration(&mut self) -> Feedback { + let supports_configuration = self + .initialise_params + .capabilities + .workspace + .as_ref() + .and_then(|w| w.configuration) + .unwrap_or(false); + + if !supports_configuration { + tracing::warn!("lsp_client_cannot_request_configuration"); + return Feedback::default(); + } + + self.send_request( + "workspace/configuration", + lsp::ConfigurationParams { + items: vec![ConfigurationItem { + scope_uri: None, + section: Some("gleam".into()), + }], + }, + Some(ResponseHandler::UpdateConfiguration), + ); + + Feedback::default() + } + fn path_error_response(&mut self, path: Utf8PathBuf, error: crate::Error) -> (Json, Feedback) { let feedback = match self.router.project_for_path(path) { Ok(Some(project)) => project.feedback.error(error), @@ -332,6 +444,11 @@ where self.respond_with_engine(path, |engine| engine.document_symbol(params)) } + fn show_inlay_hints(&mut self, params: lsp::InlayHintParams) -> (Json, Feedback) { + let path = super::path(¶ms.text_document.uri); + self.respond_with_engine(path, |engine| engine.inlay_hints(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) { @@ -430,7 +547,7 @@ fn initialisation_handshake(connection: &lsp_server::Connection) -> InitializePa experimental: None, position_encoding: None, inline_value_provider: None, - inlay_hint_provider: None, + inlay_hint_provider: Some(lsp::OneOf::Left(true)), diagnostic_provider: None, }; let server_capabilities_json = diff --git a/compiler-core/src/language_server/tests.rs b/compiler-core/src/language_server/tests.rs index 92076e8ed21..1c8c8924a75 100644 --- a/compiler-core/src/language_server/tests.rs +++ b/compiler-core/src/language_server/tests.rs @@ -4,21 +4,10 @@ mod completion; mod definition; mod document_symbols; mod hover; +mod inlay_hints; mod signature_help; -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, - time::SystemTime, -}; - -use ecow::EcoString; -use hexpm::version::{Range, Version}; - -use camino::{Utf8Path, Utf8PathBuf}; -use itertools::Itertools; -use lsp_types::{Position, TextDocumentIdentifier, TextDocumentPositionParams, Url}; - +use super::configuration::Configuration; use crate::{ config::PackageConfig, io::{ @@ -34,6 +23,16 @@ use crate::{ requirement::Requirement, Result, }; +use camino::{Utf8Path, Utf8PathBuf}; +use ecow::EcoString; +use hexpm::version::{Range, Version}; +use itertools::Itertools; +use lsp_types::{Position, TextDocumentIdentifier, TextDocumentPositionParams, Url}; +use std::{ + collections::HashMap, + sync::{Arc, Mutex, RwLock}, + time::SystemTime, +}; pub const LSP_TEST_ROOT_PACKAGE_NAME: &str = "app"; @@ -366,6 +365,7 @@ fn setup_engine( io.clone(), FileSystemProxy::new(io.clone()), io.paths.clone(), + Arc::new(RwLock::new(Configuration::default())), ) .unwrap() } @@ -523,16 +523,16 @@ impl<'a> TestProject<'a> { engine } - pub fn build_path(&self, position: Position) -> TextDocumentPositionParams { + fn build_path() -> TextDocumentIdentifier { let path = Utf8PathBuf::from(if cfg!(target_family = "windows") { r"\\?\C:\src\app.gleam" } else { "/src/app.gleam" }); - let url = Url::from_file_path(path).unwrap(); + let url = Url::from_file_path(path).expect("valid path"); - TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position) + TextDocumentIdentifier::new(url) } pub fn build_test_path( @@ -566,7 +566,7 @@ impl<'a> TestProject<'a> { let _response = engine.compile_please(); - let param = self.build_path(position); + let param = TextDocumentPositionParams::new(Self::build_path(), position); (engine, param) } diff --git a/compiler-core/src/language_server/tests/compilation.rs b/compiler-core/src/language_server/tests/compilation.rs index 3645a55f4f9..356e8c7d389 100644 --- a/compiler-core/src/language_server/tests/compilation.rs +++ b/compiler-core/src/language_server/tests/compilation.rs @@ -1,6 +1,5 @@ -use crate::language_server::engine::Compilation; - use super::*; +use crate::language_server::engine::Compilation; #[test] fn compile_please() { diff --git a/compiler-core/src/language_server/tests/inlay_hints.rs b/compiler-core/src/language_server/tests/inlay_hints.rs new file mode 100644 index 00000000000..b0562eb52d1 --- /dev/null +++ b/compiler-core/src/language_server/tests/inlay_hints.rs @@ -0,0 +1,208 @@ +use crate::language_server::{ + configuration::{Configuration, InlayHintsConfig}, + tests::{setup_engine, LanguageServerTestIO}, +}; +use lsp_types::{InlayHintParams, Position, Range}; + +#[test] +fn no_hints_when_same_line() { + let src = r#" + fn identity(x) { + x + } + + fn ret_str(_x) { + "abc" + } + + pub fn example_pipe() { + 0 |> ret_str() |> identity() + } +"#; + + let hints = inlay_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn no_hints_when_value_is_literal() { + let src = r#" + pub fn ret_str(f1) { + "abc" + |> f1() + } + + pub fn ret_int(f2) { + 42 + |> f2() + } + + pub fn ret_float(f3) { + 42.2 + |> f3() + } + + pub fn ret_bit_array(f4) { + <<1, 2>> + |> f4() + } +"#; + + let hints = inlay_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn show_many_hints() { + let src = r#" + const int_val = 0 + + fn identity(x) { + x + } + + fn ret_str(_x) { + "abc" + } + + pub fn example_pipe() { + int_val + |> ret_str() + |> identity() + } + "#; + + let hints = inlay_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn hints_nested_in_case_block() { + let src = r#" + const int_val = 0 + + fn identity(x) { + x + } + + fn main(a) { + case a { + _ -> { + int_val + |> identity() + } + } + } + "#; + + let hints = inlay_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn hints_nested_for_apply_fn_let() { + let src = r#" + const int_val = 0 + + fn identity(x) { + x + } + + fn main() { + let f = identity(fn() { + int_val + |> identity() + }) + } + "#; + + let hints = inlay_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn hints_in_use() { + let src = r#" + const int_val = 0 + + fn identity(x) { + x + } + + fn main(f) { + use a <- f() + int_val + |> identity() + } + "#; + + let hints = inlay_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn do_not_show_hints_by_default() { + let src = r#" + const int_val = 0 + + fn identity(x) { + x + } + + fn ret_str(_x) { + "abc" + } + + pub fn example_pipe() { + int_val + |> ret_str() + |> identity() + } + "#; + + let hints = inlay_hints_for_config(src, Configuration::default()); + insta::assert_snapshot!(hints); +} + +fn inlay_hints(src: &str) -> String { + inlay_hints_for_config( + src, + Configuration { + inlay_hints: InlayHintsConfig { pipelines: true }, + }, + ) +} + +fn inlay_hints_for_config(src: &str, user_config: Configuration) -> String { + let io = LanguageServerTestIO::new(); + let mut engine = setup_engine(&io); + { + let mut config = engine.user_config.write().expect("cannot write config"); + *config = user_config; + } + + _ = io.src_module("app", src); + let response = engine.compile_please(); + assert!(response.result.is_ok()); + + let params = InlayHintParams { + text_document: super::TestProject::build_path(), + work_done_progress_params: Default::default(), + range: Range::new( + Position::new(0, 0), + Position::new( + src.lines().count() as u32, + src.lines().last().unwrap_or_default().len() as u32, + ), + ), + }; + + let hints = engine + .inlay_hints(params) + .result + .expect("inlay hint request should not fail"); + + let stringified = serde_json::to_string_pretty(&hints).expect("json pprint should not fail"); + + stringified +} diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__do_not_show_hints_by_default.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__do_not_show_hints_by_default.snap new file mode 100644 index 00000000000..0aa0353f9aa --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__do_not_show_hints_by_default.snap @@ -0,0 +1,5 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[] diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_in_use.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_in_use.snap new file mode 100644 index 00000000000..4cf6f32a75d --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_in_use.snap @@ -0,0 +1,24 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[ + { + "position": { + "line": 9, + "character": 19 + }, + "label": "Int", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 10, + "character": 25 + }, + "label": "Int", + "kind": 1, + "paddingLeft": true + } +] diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_nested_for_apply_fn_let.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_nested_for_apply_fn_let.snap new file mode 100644 index 00000000000..e53f77303a2 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_nested_for_apply_fn_let.snap @@ -0,0 +1,24 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[ + { + "position": { + "line": 9, + "character": 21 + }, + "label": "Int", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 10, + "character": 27 + }, + "label": "Int", + "kind": 1, + "paddingLeft": true + } +] diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_nested_in_case_block.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_nested_in_case_block.snap new file mode 100644 index 00000000000..738424aaf10 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__hints_nested_in_case_block.snap @@ -0,0 +1,24 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[ + { + "position": { + "line": 10, + "character": 25 + }, + "label": "Int", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 11, + "character": 31 + }, + "label": "Int", + "kind": 1, + "paddingLeft": true + } +] diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__no_hints_when_same_line.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__no_hints_when_same_line.snap new file mode 100644 index 00000000000..0aa0353f9aa --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__no_hints_when_same_line.snap @@ -0,0 +1,5 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[] diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__no_hints_when_value_is_literal.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__no_hints_when_value_is_literal.snap new file mode 100644 index 00000000000..5fab28e3c84 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__no_hints_when_value_is_literal.snap @@ -0,0 +1,42 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[ + { + "position": { + "line": 3, + "character": 13 + }, + "label": "a", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 8, + "character": 13 + }, + "label": "a", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 13, + "character": 13 + }, + "label": "a", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 18, + "character": 13 + }, + "label": "a", + "kind": 1, + "paddingLeft": true + } +] diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_many_hints.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_many_hints.snap new file mode 100644 index 00000000000..36d25555488 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_many_hints.snap @@ -0,0 +1,33 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[ + { + "position": { + "line": 12, + "character": 19 + }, + "label": "Int", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 13, + "character": 24 + }, + "label": "String", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 14, + "character": 25 + }, + "label": "String", + "kind": 1, + "paddingLeft": true + } +] diff --git a/test-package-compiler/src/lib.rs b/test-package-compiler/src/lib.rs index 3d25600e86d..ada187ec97b 100644 --- a/test-package-compiler/src/lib.rs +++ b/test-package-compiler/src/lib.rs @@ -131,7 +131,7 @@ impl TestCompileOutput { Regex::new(r#"-file\("([^"]+)", (\d+)\)\."#).expect("Invalid regex") }) .replace_all(text, |caps: ®ex::Captures| { - let path = caps.get(1).expect("file path").as_str().replace("\\", "/"); + let path = caps.get(1).expect("file path").as_str().replace('\\', "/"); let line_number = caps.get(2).expect("line number").as_str(); format!("-file(\"{}\", {}).", path, line_number) });