Skip to content

Commit

Permalink
Add inlay hints for function parameters (#6513)
Browse files Browse the repository at this point in the history
## Description

Inlay hints are now displayed in function parameter locations if a
variable with the same name is not present. This should be identical to
the behaviour of rust-analyzer. The inlay_hints benchmark is slower
because we are now also calculating inlay hints for function_params and
variable decls in the same iteration. See screen shot below for all
supported types.

![Screenshot 2024-09-21 at 10 47
12 AM](https://github.com/user-attachments/assets/c3aaa901-e01d-4e88-a405-f1077d2d2dce)

closes #5196

## Checklist

- [x] I have linked to any relevant issues.
- [x] I have commented my code, particularly in hard-to-understand
areas.
- [x] I have updated the documentation where relevant (API docs, the
reference, and the Sway book).
- [x] If my change requires substantial documentation changes, I have
[requested support from the DevRel
team](https://github.com/FuelLabs/devrel-requests/issues/new/choose)
- [x] I have added tests that prove my fix is effective or that my
feature works.
- [x] I have added (or requested a maintainer to add) the necessary
`Breaking*` or `New Feature` labels where relevant.
- [x] I have done my best to ensure that my PR adheres to [the Fuel Labs
Code Review
Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md).
- [x] I have requested a review from the relevant team or maintainers.
  • Loading branch information
JoshuaBatty authored Sep 23, 2024
1 parent e1546f6 commit 99f11c0
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 36 deletions.
146 changes: 111 additions & 35 deletions sway-lsp/src/capabilities/inlay_hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -23,81 +26,154 @@ pub struct InlayHint {
pub label: String,
}

/// Generates inlay hints for the provided range.
pub fn inlay_hints(
session: Arc<Session>,
uri: &Url,
range: &Range,
config: &InlayHintsConfig,
) -> Option<Vec<lsp_types::InlayHint>> {
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<lsp_types::InlayHint> = session
.token_map()
.tokens_for_file(uri)
.filter_map(|item| {
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<lsp_types::InlayHint> {
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::<Vec<_>>()
}

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,
Expand Down
2 changes: 1 addition & 1 deletion sway-lsp/src/handlers/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<Vec<InlayHint>>> {
Expand Down
2 changes: 2 additions & 0 deletions sway-lsp/tests/fixtures/inlay_hints/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out
target
9 changes: 9 additions & 0 deletions sway-lsp/tests/fixtures/inlay_hints/Forc.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
authors = ["Fuel Labs <[email protected]>"]
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" }
51 changes: 51 additions & 0 deletions sway-lsp/tests/fixtures/inlay_hints/src/main.sw
Original file line number Diff line number Diff line change
@@ -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<T>(x: T) -> T {
x
}
fn two_generics<A, B>(_a: A, b: B) -> B {
b
}
fn three_generics<A, B, C>(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]);
}

Loading

0 comments on commit 99f11c0

Please sign in to comment.