diff --git a/sway-lsp/src/capabilities/inlay_hints.rs b/sway-lsp/src/capabilities/inlay_hints.rs index f6bdb95a128..a832948fc96 100644 --- a/sway-lsp/src/capabilities/inlay_hints.rs +++ b/sway-lsp/src/capabilities/inlay_hints.rs @@ -7,13 +7,16 @@ use crate::{ }; use lsp_types::{self, Range, Url}; use std::sync::Arc; -use sway_core::{language::ty::TyDecl, type_system::TypeInfo}; -use sway_types::Spanned; +use sway_core::{ + language::ty::{TyDecl, TyExpression, TyExpressionVariant}, + type_system::TypeInfo, +}; +use sway_types::{Ident, Spanned}; -// Future PR's will add more kinds #[derive(Clone, Debug, PartialEq, Eq)] pub enum InlayKind { TypeHint, + Parameter, } #[derive(Debug)] @@ -23,22 +26,27 @@ pub struct InlayHint { pub label: String, } +/// Generates inlay hints for the provided range. pub fn inlay_hints( session: Arc, uri: &Url, range: &Range, config: &InlayHintsConfig, ) -> Option> { - let _p = tracing::trace_span!("inlay_hints").entered(); - // 1. Loop through all our tokens and filter out all tokens that aren't TypedVariableDeclaration tokens - // 2. Also filter out all tokens that have a span that fall outside of the provided range - // 3. Filter out all variable tokens that have a type_ascription - // 4. Look up the type id for the remaining tokens - // 5. Convert the type into a string + let _span = tracing::trace_span!("inlay_hints").entered(); + if !config.type_hints { return None; } + // 1. Iterate through all tokens in the file + // 2. Filter for TypedVariableDeclaration tokens within the provided range + // 3. For each variable declaration: + // a. If it's a function application, generate parameter hints + // b. If it doesn't have a type ascription and its type is known: + // - Look up the type information + // - Generate a type hint + // 4. Collect all generated hints into a single vector let hints: Vec = session .token_map() .tokens_for_file(uri) @@ -46,58 +54,126 @@ pub fn inlay_hints( let token = item.value(); token.typed.as_ref().and_then(|t| match t { TypedAstToken::TypedDeclaration(TyDecl::VariableDecl(var_decl)) => { - if var_decl.type_ascription.call_path_tree.is_some() { - None + let var_range = get_range_from_span(&var_decl.name.span()); + if var_range.start >= range.start && var_range.end <= range.end { + Some(var_decl.clone()) } else { - let var_range = get_range_from_span(&var_decl.name.span()); - if var_range.start >= range.start && var_range.end <= range.end { - Some(var_decl.clone()) - } else { - None - } + None } } _ => None, }) }) - .filter_map(|var| { - let type_info = session.engines.read().te().get(var.type_ascription.type_id); - match &*type_info { - TypeInfo::Unknown | TypeInfo::UnknownGeneric { .. } => None, - _ => Some(var), + .flat_map(|var| { + let mut hints = Vec::new(); + + // Function parameter hints + if let TyExpressionVariant::FunctionApplication { arguments, .. } = &var.body.expression + { + hints.extend(handle_function_parameters(arguments, config)); } - }) - .map(|var| { - let range = get_range_from_span(&var.name.span()); - let kind = InlayKind::TypeHint; - let label = format!("{}", session.engines.read().help_out(var.type_ascription)); - let inlay_hint = InlayHint { range, kind, label }; - self::inlay_hint(config.render_colons, inlay_hint) + + // Variable declaration hints + if var.type_ascription.call_path_tree.is_none() { + let type_info = session.engines.read().te().get(var.type_ascription.type_id); + if !matches!( + *type_info, + TypeInfo::Unknown | TypeInfo::UnknownGeneric { .. } + ) { + let range = get_range_from_span(&var.name.span()); + let kind = InlayKind::TypeHint; + let label = format!("{}", session.engines.read().help_out(var.type_ascription)); + let inlay_hint = InlayHint { range, kind, label }; + hints.push(self::inlay_hint(config, inlay_hint)); + } + } + hints }) .collect(); Some(hints) } -fn inlay_hint(render_colons: bool, inlay_hint: InlayHint) -> lsp_types::InlayHint { +fn handle_function_parameters( + arguments: &[(Ident, TyExpression)], + config: &InlayHintsConfig, +) -> Vec { + arguments + .iter() + .flat_map(|(name, exp)| { + let mut hints = Vec::new(); + let (should_create_hint, span) = match &exp.expression { + TyExpressionVariant::Literal(_) + | TyExpressionVariant::ConstantExpression { .. } + | TyExpressionVariant::Tuple { .. } + | TyExpressionVariant::Array { .. } + | TyExpressionVariant::ArrayIndex { .. } + | TyExpressionVariant::FunctionApplication { .. } + | TyExpressionVariant::StructFieldAccess { .. } + | TyExpressionVariant::TupleElemAccess { .. } => (true, &exp.span), + TyExpressionVariant::EnumInstantiation { + call_path_binding, .. + } => (true, &call_path_binding.span), + _ => (false, &exp.span), + }; + if should_create_hint { + let range = get_range_from_span(span); + let kind = InlayKind::Parameter; + let label = name.as_str().to_string(); + let inlay_hint = InlayHint { range, kind, label }; + hints.push(self::inlay_hint(config, inlay_hint)); + } + // Handle nested function applications + if let TyExpressionVariant::FunctionApplication { + arguments: nested_args, + .. + } = &exp.expression + { + hints.extend(handle_function_parameters(nested_args, config)); + } + hints + }) + .collect::>() +} + +fn inlay_hint(config: &InlayHintsConfig, inlay_hint: InlayHint) -> lsp_types::InlayHint { + let truncate_label = |label: String| -> String { + if let Some(max_length) = config.max_length { + if label.len() > max_length { + format!("{}...", &label[..max_length.saturating_sub(3)]) + } else { + label + } + } else { + label + } + }; + + let label = match inlay_hint.kind { + InlayKind::TypeHint if config.render_colons => format!(": {}", inlay_hint.label), + InlayKind::Parameter if config.render_colons => format!("{}: ", inlay_hint.label), + _ => inlay_hint.label, + }; + lsp_types::InlayHint { position: match inlay_hint.kind { // after annotated thing InlayKind::TypeHint => inlay_hint.range.end, + InlayKind::Parameter => inlay_hint.range.start, }, - label: lsp_types::InlayHintLabel::String(match inlay_hint.kind { - InlayKind::TypeHint if render_colons => format!(": {}", inlay_hint.label), - InlayKind::TypeHint => inlay_hint.label, - }), + label: lsp_types::InlayHintLabel::String(truncate_label(label)), kind: match inlay_hint.kind { InlayKind::TypeHint => Some(lsp_types::InlayHintKind::TYPE), + InlayKind::Parameter => Some(lsp_types::InlayHintKind::PARAMETER), }, tooltip: None, padding_left: Some(match inlay_hint.kind { - InlayKind::TypeHint => !render_colons, + InlayKind::TypeHint => !config.render_colons, + InlayKind::Parameter => false, }), padding_right: Some(match inlay_hint.kind { InlayKind::TypeHint => false, + InlayKind::Parameter => !config.render_colons, }), text_edits: None, data: None, diff --git a/sway-lsp/src/handlers/request.rs b/sway-lsp/src/handlers/request.rs index 1703948c561..fb4b72d86a8 100644 --- a/sway-lsp/src/handlers/request.rs +++ b/sway-lsp/src/handlers/request.rs @@ -333,7 +333,7 @@ pub async fn handle_semantic_tokens_full( } } -pub(crate) async fn handle_inlay_hints( +pub async fn handle_inlay_hints( state: &ServerState, params: InlayHintParams, ) -> Result>> { diff --git a/sway-lsp/tests/fixtures/inlay_hints/.gitignore b/sway-lsp/tests/fixtures/inlay_hints/.gitignore new file mode 100644 index 00000000000..77d3844f58c --- /dev/null +++ b/sway-lsp/tests/fixtures/inlay_hints/.gitignore @@ -0,0 +1,2 @@ +out +target diff --git a/sway-lsp/tests/fixtures/inlay_hints/Forc.toml b/sway-lsp/tests/fixtures/inlay_hints/Forc.toml new file mode 100644 index 00000000000..57a6803b1f1 --- /dev/null +++ b/sway-lsp/tests/fixtures/inlay_hints/Forc.toml @@ -0,0 +1,9 @@ +[project] +authors = ["Fuel Labs "] +entry = "main.sw" +license = "Apache-2.0" +name = "inlay_hints" +implicit-std = false + +[dependencies] +std = { git = "https://github.com/FuelLabs/sway", tag = "v0.63.5" } diff --git a/sway-lsp/tests/fixtures/inlay_hints/src/main.sw b/sway-lsp/tests/fixtures/inlay_hints/src/main.sw new file mode 100644 index 00000000000..f3071c0f912 --- /dev/null +++ b/sway-lsp/tests/fixtures/inlay_hints/src/main.sw @@ -0,0 +1,51 @@ +script; + +const CONSTANT: u64 = 42; + +enum MyEnum { + A: u64, +} +struct MyStruct { + a: u64, +} +fn my_function(foo: u64, bar: u64, long_argument_name: u64) -> u64 { + foo + bar + long_argument_name +} +fn identity(x: T) -> T { + x +} +fn two_generics(_a: A, b: B) -> B { + b +} +fn three_generics(a: A, b: B, _c: C) -> B { + let _a: A = a; + b +} + +fn main() { + let _x = my_function(1, 2, 3); + let foo = 1; + let _y = my_function(foo, 2, 3); + let bar = 2; + let _function_call = identity(my_function(1, bar, 3)); + let _z = my_function(foo, bar, 3); + let long_argument_name = 3; + let _w = my_function(foo, bar, long_argument_name); + let _a: bool = identity(true); + let _b: u32 = identity(10u32); + let _c: u64 = identity(42); + let _e: str = identity("foo"); + let _f: u64 = two_generics(true, 10); + let _g: str = three_generics(true, "foo", 10); + let _const = identity(CONSTANT); + let _tuple = identity((1, 2, 3)); + let _array = identity([1, 2, 3]); + let _enum = identity(MyEnum::A(1)); + let s = MyStruct { a: 1 }; + let _struct_field_access = identity(s.a); + let t = (0, 1, 9); + let _tuple_elem_access = identity(t.2); + let a = [1, 2, 3]; + let _array_index = identity(a[1]); +} + diff --git a/sway-lsp/tests/integration/lsp.rs b/sway-lsp/tests/integration/lsp.rs index 3fd9ca2dc5b..7f26570846a 100644 --- a/sway-lsp/tests/integration/lsp.rs +++ b/sway-lsp/tests/integration/lsp.rs @@ -663,3 +663,126 @@ pub fn create_did_change_params( }], } } + +pub(crate) async fn inlay_hints_request<'a>( + server: &ServerState, + uri: &Url, +) -> Option> { + let params = InlayHintParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + range: Range { + start: Position { + line: 25, + character: 0, + }, + end: Position { + line: 26, + character: 1, + }, + }, + work_done_progress_params: Default::default(), + }; + let res = request::handle_inlay_hints(server, params) + .await + .unwrap() + .unwrap(); + let expected = vec![ + InlayHint { + position: Position { + line: 25, + character: 25, + }, + label: InlayHintLabel::String("foo: ".to_string()), + kind: Some(InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }, + InlayHint { + position: Position { + line: 25, + character: 28, + }, + label: InlayHintLabel::String("bar: ".to_string()), + kind: Some(InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }, + InlayHint { + position: Position { + line: 25, + character: 31, + }, + label: InlayHintLabel::String("long_argument_name: ".to_string()), + kind: Some(InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }, + InlayHint { + position: Position { + line: 25, + character: 10, + }, + label: InlayHintLabel::String(": u64".to_string()), + kind: Some(InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }, + ]; + + assert!( + compare_inlay_hint_vecs(&expected, &res), + "InlayHint vectors are not equal.\nExpected:\n{:#?}\n\nActual:\n{:#?}", + expected, + res + ); + Some(res) +} + +// This is a helper function to compare two inlay hints. because PartialEq is not implemented for InlayHint +fn compare_inlay_hints(a: &InlayHint, b: &InlayHint) -> bool { + a.position == b.position + && compare_inlay_hint_labels(&a.label, &b.label) + && a.kind == b.kind + && a.text_edits == b.text_edits + && compare_inlay_hint_tooltips(&a.tooltip, &b.tooltip) + && a.padding_left == b.padding_left + && a.padding_right == b.padding_right + && a.data == b.data +} + +fn compare_inlay_hint_vecs(a: &[InlayHint], b: &[InlayHint]) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(a, b)| compare_inlay_hints(a, b)) +} + +fn compare_inlay_hint_labels(a: &InlayHintLabel, b: &InlayHintLabel) -> bool { + match (a, b) { + (InlayHintLabel::String(a), InlayHintLabel::String(b)) => a == b, + _ => false, + } +} + +fn compare_inlay_hint_tooltips(a: &Option, b: &Option) -> bool { + match (a, b) { + (None, None) => true, + (Some(a), Some(b)) => match (a, b) { + (InlayHintTooltip::String(a), InlayHintTooltip::String(b)) => a == b, + (InlayHintTooltip::MarkupContent(a), InlayHintTooltip::MarkupContent(b)) => { + a.kind == b.kind && a.value == b.value + } + _ => false, + }, + _ => false, + } +} diff --git a/sway-lsp/tests/lib.rs b/sway-lsp/tests/lib.rs index 8eebe036aef..ef383c9ddba 100644 --- a/sway-lsp/tests/lib.rs +++ b/sway-lsp/tests/lib.rs @@ -2090,6 +2090,11 @@ lsp_capability_test!( lsp::completion_request, test_fixtures_dir().join("completion/src/main.sw") ); +lsp_capability_test!( + inlay_hints_function_params, + lsp::inlay_hints_request, + test_fixtures_dir().join("inlay_hints/src/main.sw") +); // This method iterates over all of the examples in the e2e language should_pass dir // and saves the lexed, parsed, and typed ASTs to the users home directory.