diff --git a/compiler-core/src/language_server.rs b/compiler-core/src/language_server.rs index 1f27d34e0b2..b62c8cfd47f 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 reference; @@ -41,13 +43,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..464e5b09100 --- /dev/null +++ b/compiler-core/src/language_server/configuration.rs @@ -0,0 +1,22 @@ +use serde::Deserialize; + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] +#[serde(default)] +#[serde(rename_all = "camelCase")] +pub struct Configuration { + pub inlay_hints: InlayHintsConfig, +} + +#[derive(Debug, Default, Clone, Deserialize, PartialEq, Eq)] +#[serde(default)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsConfig { + /// Whether to show type inlay hints of multiline pipelines + pub pipelines: bool, + + /// Whether to show type inlay hints of function parameters + pub function_parameter_types: bool, + + /// Whether to show type inlay hints of return types of functions + pub function_return_types: bool, +} diff --git a/compiler-core/src/language_server/engine.rs b/compiler-core/src/language_server/engine.rs index 921b077584e..fb5bcf349a1 100644 --- a/compiler-core/src/language_server/engine.rs +++ b/compiler-core/src/language_server/engine.rs @@ -23,11 +23,14 @@ use ecow::EcoString; use itertools::Itertools; use lsp::CodeAction; use lsp_types::{ - self as lsp, DocumentSymbol, Hover, HoverContents, MarkedString, Position, + self as lsp, DocumentSymbol, Hover, HoverContents, InlayHint, MarkedString, Position, PrepareRenameResponse, Range, SignatureHelp, SymbolKind, SymbolTag, TextEdit, Url, WorkspaceEdit, }; -use std::{collections::HashSet, sync::Arc}; +use std::{ + collections::HashSet, + sync::{Arc, RwLock}, +}; use super::{ DownloadDependencies, MakeLocker, @@ -42,6 +45,8 @@ use super::{ code_action_inexhaustive_let_to_case, }, completer::Completer, + configuration::Configuration, + inlay_hints, reference::{ Referenced, find_module_value_references, find_variable_references, reference_for_ast_node, }, @@ -87,6 +92,9 @@ pub struct LanguageServerEngine { /// Used to know if to show the "View on HexDocs" link /// when hovering on an imported value hex_deps: HashSet, + + /// Configuration the user has set in their editor. + pub(crate) user_config: Arc>, } impl<'a, IO, Reporter> LanguageServerEngine @@ -107,6 +115,7 @@ where progress_reporter: Reporter, io: FileSystemProxy, paths: ProjectPaths, + user_config: Arc>, ) -> Result { let locker = io.inner().make_locker(&paths, config.target)?; @@ -143,6 +152,7 @@ where paths, error: None, hex_deps, + user_config, }) } @@ -550,6 +560,26 @@ 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![]); + }; + + let Some(module) = this.module_for_uri(¶ms.text_document.uri) else { + return Ok(vec![]); + }; + + let hints = inlay_hints::get_inlay_hints( + config.inlay_hints.clone(), + module.ast.clone(), + &LineNumbers::new(&module.code), + ); + + Ok(hints) + }) + } + /// Check whether a particular module is in the same package as this one fn is_same_package(&self, current_module: &Module, module_name: &str) -> bool { let other_module = self 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..a5f636ec671 --- /dev/null +++ b/compiler-core/src/language_server/inlay_hints.rs @@ -0,0 +1,263 @@ +use lsp_types::{InlayHint, InlayHintKind, InlayHintLabel}; + +use crate::{ + ast::{ + PipelineAssignmentKind, SrcSpan, Statement, TypeAst, TypedExpr, TypedModule, + TypedPipelineAssignment, visit::Visit, + }, + line_numbers::LineNumbers, + type_::{self, Type}, +}; + +use super::{configuration::InlayHintsConfig, src_offset_to_lsp_position}; + +struct InlayHintsVisitor<'a> { + config: InlayHintsConfig, + module_names: &'a type_::printer::Names, + current_declaration_printer: type_::printer::Printer<'a>, + + hints: Vec, + line_numbers: &'a LineNumbers, +} + +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 InlayHintsVisitor<'_> { + pub fn push_binding_annotation( + &mut self, + type_: &Type, + type_annotation_ast: Option<&TypeAst>, + span: &SrcSpan, + ) { + if type_annotation_ast.is_some() { + return; + } + + let label = format!(": {}", self.current_declaration_printer.print_type(type_)); + + let mut hint = default_inlay_hint(self.line_numbers, span.end, label); + hint.padding_left = Some(false); + + self.hints.push(hint); + } + + pub fn push_return_annotation( + &mut self, + type_: &Type, + type_annotation_ast: Option<&TypeAst>, + span: &SrcSpan, + ) { + if type_annotation_ast.is_some() { + return; + } + + let label = format!("-> {}", self.current_declaration_printer.print_type(type_)); + + let hint = default_inlay_hint(self.line_numbers, span.end, label); + + self.hints.push(hint); + } +} + +impl<'ast> Visit<'ast> for InlayHintsVisitor<'_> { + fn visit_typed_statement(&mut self, stmt: &'ast crate::ast::TypedStatement) { + // This must be reset on every statement + self.current_declaration_printer = type_::printer::Printer::new(self.module_names); + + match stmt { + Statement::Expression(expr) => self.visit_typed_expr(expr), + Statement::Assignment(assignment) => self.visit_typed_assignment(assignment), + Statement::Use(use_) => self.visit_typed_use(use_), + } + } + + fn visit_typed_function(&mut self, fun: &'ast crate::ast::TypedFunction) { + for statement in &fun.body { + self.visit_typed_statement(statement); + } + + if self.config.function_parameter_types { + for argument in &fun.arguments { + self.push_binding_annotation( + &argument.type_, + argument.annotation.as_ref(), + &argument.location, + ); + } + } + + if self.config.function_return_types { + self.push_return_annotation( + &fun.return_type, + fun.return_annotation.as_ref(), + &fun.location, + ); + } + } + + fn visit_typed_expr_fn( + &mut self, + _location: &'ast SrcSpan, + type_: &'ast std::sync::Arc, + kind: &'ast crate::ast::FunctionLiteralKind, + args: &'ast [crate::ast::TypedArg], + body: &'ast vec1::Vec1, + return_annotation: &'ast Option, + ) { + for st in body { + self.visit_typed_statement(st); + } + + let crate::ast::FunctionLiteralKind::Anonymous { head } = kind else { + return; + }; + + if self.config.function_parameter_types { + for arg in args { + self.push_binding_annotation(&arg.type_, arg.annotation.as_ref(), &arg.location); + } + } + + if self.config.function_return_types { + if let Some((_arguments, ret_type)) = type_.fn_types() { + self.push_return_annotation(&ret_type, return_annotation.as_ref(), head); + } + } + } + + fn visit_typed_expr_pipeline( + &mut self, + _location: &'ast SrcSpan, + first_value: &'ast TypedPipelineAssignment, + assignments: &'ast [(TypedPipelineAssignment, PipelineAssignmentKind)], + finally: &'ast TypedExpr, + _finally_kind: &'ast PipelineAssignmentKind, + ) { + self.visit_typed_pipeline_assignment(first_value); + for (assignment, _kind) in assignments { + self.visit_typed_pipeline_assignment(assignment); + } + self.visit_typed_expr(finally); + + if !self.config.pipelines { + return; + } + + let mut prev_hint: Option<(u32, Option)> = None; + + let assigments_values = + std::iter::once(first_value).chain(assignments.iter().map(|p| &p.0)); + + for assign in assigments_values { + 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, + self.current_declaration_printer + .print_type(assign.type_().as_ref()) + .to_string(), + ); + + prev_hint = Some(( + this_line, + if is_simple_lit(&assign.value) { + None + } else { + Some(this_hint) + }, + )); + } + + 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, + self.current_declaration_printer + .print_type(finally.type_().as_ref()) + .to_string(), + ); + self.hints.push(hint); + } + } + } +} + +pub fn get_inlay_hints( + config: InlayHintsConfig, + typed_module: TypedModule, + line_numbers: &LineNumbers, +) -> Vec { + let mut visitor = InlayHintsVisitor { + config, + module_names: &typed_module.names, + current_declaration_printer: type_::printer::Printer::new(&typed_module.names), + hints: vec![], + line_numbers, + }; + + visitor.visit_typed_module(&typed_module); + visitor.hints +} + +/// Determines if the expression is a simple literal (e.g. 42, 42.2, "hello", or <<0, 1, 2>>) whose inlayHints must not be showed +/// in a pipeline chain +fn is_simple_lit(typed_expr: &TypedExpr) -> bool { + match typed_expr { + 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 { .. } + | TypedExpr::Echo { .. } => false, + } +} diff --git a/compiler-core/src/language_server/messages.rs b/compiler-core/src/language_server/messages.rs index eef0849efb6..8e422dabf1a 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,16 @@ use lsp_types::{ notification::{DidChangeTextDocument, DidCloseTextDocument, DidSaveTextDocument}, request::{ CodeActionRequest, Completion, DocumentSymbolRequest, Formatting, GotoTypeDefinition, - HoverRequest, PrepareRenameRequest, References, Rename, SignatureHelpRequest, + HoverRequest, InlayHintRequest, PrepareRenameRequest, References, Rename, + 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), } @@ -29,11 +32,17 @@ pub enum Request { CodeAction(lsp::CodeActionParams), SignatureHelp(lsp::SignatureHelpParams), DocumentSymbol(lsp::DocumentSymbolParams), + ShowInlayHints(lsp::InlayHintParams), PrepareRename(lsp::TextDocumentPositionParams), Rename(lsp::RenameParams), FindReferences(lsp::ReferenceParams), } +#[derive(Debug)] +pub enum Response { + Configuration(Configuration), +} + impl Request { fn extract(request: lsp_server::Request) -> Option { let id = request.id.clone(); @@ -66,6 +75,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))) + } "textDocument/rename" => { let params = cast_request::(request); Some(Message::Request(id, Request::Rename(params))) @@ -95,6 +108,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, } @@ -141,6 +156,11 @@ impl Notification { }; Some(Message::Notification(notification)) } + + "workspace/didChangeConfiguration" => { + Some(Message::Notification(Notification::ConfigChanged)) + } + _ => None, } } @@ -158,15 +178,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(), } } @@ -226,7 +250,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 } @@ -271,3 +340,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 8cbab2456ae..8fe52d58b9d 100644 --- a/compiler-core/src/language_server/router.rs +++ b/compiler-core/src/language_server/router.rs @@ -11,12 +11,13 @@ use crate::{ }; use std::{ collections::{HashMap, hash_map::Entry}, + sync::{Arc, RwLock}, time::SystemTime, }; use camino::{Utf8Path, Utf8PathBuf}; -use super::feedback::FeedbackBookKeeper; +use super::{configuration::Configuration, 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 @@ -30,6 +31,7 @@ pub(crate) struct Router { io: FileSystemProxy, engines: HashMap>, progress_reporter: Reporter, + user_config: Arc>, } impl Router @@ -45,11 +47,16 @@ where // IO to be supplied from inside of gleam-core Reporter: ProgressReporter + Clone, { - pub fn new(progress_reporter: Reporter, io: FileSystemProxy) -> Self { + pub fn new( + progress_reporter: Reporter, + io: FileSystemProxy, + user_config: Arc>, + ) -> Self { Self { io, engines: HashMap::new(), progress_reporter, + user_config, } } @@ -85,8 +92,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) } })) @@ -124,19 +135,21 @@ where path: Utf8PathBuf, io: FileSystemProxy, progress_reporter: Reporter, + user_config: Arc>, ) -> 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 66ff4cb7b7f..db6ebdc6e4d 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::Configuration, + messages::{Message, MessageBuffer, Next, Notification, Request, Response, ResponseHandler}, progress::ConnectionProgressReporter, }; use crate::{ @@ -20,11 +21,14 @@ use camino::{Utf8Path, Utf8PathBuf}; use debug_ignore::DebugIgnore; use itertools::Itertools; use lsp_types::{ - self as lsp, HoverProviderCapability, InitializeParams, Position, PublishDiagnosticsParams, - Range, RenameOptions, TextEdit, Url, + self as lsp, ConfigurationItem, HoverProviderCapability, InitializeParams, Position, + PublishDiagnosticsParams, Range, RenameOptions, TextEdit, Url, }; use serde_json::Value as Json; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, RwLock}, +}; /// This class is responsible for handling the language server protocol and /// delegating the work to the engine. @@ -44,6 +48,8 @@ pub struct LanguageServer<'a, IO> { router: Router>, changed_projects: HashSet, io: FileSystemProxy, + message_buffer: MessageBuffer, + config: Arc>, } impl<'a, IO> LanguageServer<'a, IO> @@ -60,23 +66,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: Arc> = Default::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) => { @@ -94,9 +106,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; + } + + self.send_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), @@ -106,6 +146,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), Request::PrepareRename(param) => self.prepare_rename(param), Request::Rename(param) => self.rename(param), Request::GoToTypeDefinition(param) => self.goto_type_definition(param), @@ -133,6 +174,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); } @@ -168,6 +210,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 @@ -198,18 +269,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) { @@ -269,6 +336,53 @@ where } } + fn send_inlay_hints_refresh(&mut self) { + 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); + } + } + + 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), @@ -346,6 +460,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 prepare_rename(&mut self, params: lsp::TextDocumentPositionParams) -> (Json, Feedback) { let path = super::path(¶ms.text_document.uri); self.respond_with_engine(path, |engine| engine.prepare_rename(params)) @@ -464,7 +583,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 d19cd0476c8..d3b8fe10e7c 100644 --- a/compiler-core/src/language_server/tests.rs +++ b/compiler-core/src/language_server/tests.rs @@ -4,13 +4,14 @@ mod completion; mod definition; mod document_symbols; mod hover; +mod inlay_hints; mod reference; mod rename; mod signature_help; use std::{ collections::{HashMap, HashSet}, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, RwLock}, time::SystemTime, }; @@ -21,6 +22,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use itertools::Itertools; use lsp_types::{Position, TextDocumentIdentifier, TextDocumentPositionParams, Url}; +use super::configuration::Configuration; use crate::{ Result, config::PackageConfig, @@ -385,6 +387,7 @@ fn setup_engine( io.clone(), FileSystemProxy::new(io.clone()), io.paths.clone(), + Arc::new(RwLock::new(Configuration::default())), ) .unwrap() } @@ -547,7 +550,7 @@ impl<'a> TestProject<'a> { engine } - pub fn build_path(&self, position: Position) -> TextDocumentPositionParams { + pub fn build_path() -> TextDocumentIdentifier { let path = Utf8PathBuf::from(if cfg!(target_family = "windows") { r"\\?\C:\src\app.gleam" } else { @@ -556,7 +559,7 @@ impl<'a> TestProject<'a> { let url = Url::from_file_path(path).unwrap(); - TextDocumentPositionParams::new(TextDocumentIdentifier::new(url), position) + TextDocumentIdentifier::new(url) } pub fn build_test_path( @@ -590,7 +593,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/completion.rs b/compiler-core/src/language_server/tests/completion.rs index f80d8c669f4..b06998e9690 100644 --- a/compiler-core/src/language_server/tests/completion.rs +++ b/compiler-core/src/language_server/tests/completion.rs @@ -369,7 +369,7 @@ pub opaque type Wibble { let _ = engine.compile_please(); // update src to the one we want to test _ = io.src_module("app", src); - let param = tester.build_path(position); + let param = TextDocumentPositionParams::new(TestProject::build_path(), position); let response = engine.completion(param, src.into()); let mut completions = response.result.unwrap().unwrap_or_default(); 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..477cda9e70b --- /dev/null +++ b/compiler-core/src/language_server/tests/inlay_hints.rs @@ -0,0 +1,281 @@ +use crate::language_server::{ + configuration::{Configuration, InlayHintsConfig}, + tests::{LanguageServerTestIO, TestProject, setup_engine}, +}; +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 = pipeline_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 = pipeline_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 = pipeline_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 = pipeline_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 = pipeline_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 = pipeline_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn show_hints_in_params() { + let src = r#" + fn example_fn( + flex_type_arg, + b: do_not_show_this, + ) { 0 } + "#; + + let hints = parameter_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn show_hints_in_return() { + let src = r#" + fn example_fn() { 0 } + "#; + + let hints = return_hints(src); + insta::assert_snapshot!(hints); +} + +#[test] +fn show_correct_type_names_in_functions() { + let src = r#" + fn complex( + x, //: b + y: rigid_type_var, //: rigid_type_var + ) { //-> fn(a, rigid_type_var) -> #(b, rigid_type_var, a) + fn( + z, //: a + ) { //-> #(b, rigid_type_var, a) + #(x, y, z) + } + } + "#; + + let hints = inlay_hints_for_config( + src, + InlayHintsConfig { + function_parameter_types: true, + function_return_types: true, + ..Default::default() + }, + ); + 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, InlayHintsConfig::default()); + insta::assert_snapshot!(hints); +} + +fn pipeline_hints(src: &str) -> String { + inlay_hints_for_config( + src, + InlayHintsConfig { + pipelines: true, + ..Default::default() + }, + ) +} + +fn parameter_hints(src: &str) -> String { + inlay_hints_for_config( + src, + InlayHintsConfig { + function_parameter_types: true, + ..Default::default() + }, + ) +} + +fn return_hints(src: &str) -> String { + inlay_hints_for_config( + src, + InlayHintsConfig { + function_return_types: true, + ..Default::default() + }, + ) +} + +fn inlay_hints_for_config(src: &str, inlay_hints_config: InlayHintsConfig) -> String { + let io = LanguageServerTestIO::new(); + let mut engine = setup_engine(&io); + { + let mut config = engine.user_config.write().expect("cannot write config"); + *config = Configuration { + inlay_hints: inlay_hints_config, + ..Default::default() + }; + } + + _ = io.src_module("app", src); + let response = engine.compile_please(); + assert!(response.result.is_ok()); + + let params = InlayHintParams { + text_document: 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_correct_type_names_in_functions.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_correct_type_names_in_functions.snap new file mode 100644 index 00000000000..cc4326c6c96 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_correct_type_names_in_functions.snap @@ -0,0 +1,42 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[ + { + "position": { + "line": 6, + "character": 11 + }, + "label": ": a", + "kind": 1, + "paddingLeft": false + }, + { + "position": { + "line": 7, + "character": 9 + }, + "label": "-> #(b, rigid_type_var, a)", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 2, + "character": 9 + }, + "label": ": b", + "kind": 1, + "paddingLeft": false + }, + { + "position": { + "line": 4, + "character": 7 + }, + "label": "-> fn(a) -> #(b, rigid_type_var, a)", + "kind": 1, + "paddingLeft": true + } +] diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_hints_in_params.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_hints_in_params.snap new file mode 100644 index 00000000000..849f4068891 --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_hints_in_params.snap @@ -0,0 +1,15 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[ + { + "position": { + "line": 2, + "character": 21 + }, + "label": ": a", + "kind": 1, + "paddingLeft": false + } +] diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_hints_in_return.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_hints_in_return.snap new file mode 100644 index 00000000000..99b3b4ac03f --- /dev/null +++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__inlay_hints__show_hints_in_return.snap @@ -0,0 +1,15 @@ +--- +source: compiler-core/src/language_server/tests/inlay_hints.rs +expression: hints +--- +[ + { + "position": { + "line": 1, + "character": 21 + }, + "label": "-> Int", + "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 + } +]