mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Function argument inlay hints (#19269)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
This commit is contained in:
parent
3458f365da
commit
ad28b80f96
6 changed files with 692 additions and 67 deletions
|
@ -6,7 +6,7 @@ use ruff_python_ast::{AnyNodeRef, Expr, Stmt};
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::Formatter;
|
use std::fmt::Formatter;
|
||||||
use ty_python_semantic::types::Type;
|
use ty_python_semantic::types::{Type, inlay_hint_function_argument_details};
|
||||||
use ty_python_semantic::{HasType, SemanticModel};
|
use ty_python_semantic::{HasType, SemanticModel};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
@ -24,7 +24,7 @@ impl<'db> InlayHint<'db> {
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
pub enum InlayHintContent<'db> {
|
pub enum InlayHintContent<'db> {
|
||||||
Type(Type<'db>),
|
Type(Type<'db>),
|
||||||
ReturnType(Type<'db>),
|
FunctionArgumentName(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> InlayHintContent<'db> {
|
impl<'db> InlayHintContent<'db> {
|
||||||
|
@ -44,8 +44,8 @@ impl fmt::Display for DisplayInlayHint<'_, '_> {
|
||||||
InlayHintContent::Type(ty) => {
|
InlayHintContent::Type(ty) => {
|
||||||
write!(f, ": {}", ty.display(self.db))
|
write!(f, ": {}", ty.display(self.db))
|
||||||
}
|
}
|
||||||
InlayHintContent::ReturnType(ty) => {
|
InlayHintContent::FunctionArgumentName(name) => {
|
||||||
write!(f, " -> {}", ty.display(self.db))
|
write!(f, "{name}=")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,9 +76,18 @@ pub struct InlayHintSettings {
|
||||||
/// x": Literal[1]" = 1
|
/// x": Literal[1]" = 1
|
||||||
/// ```
|
/// ```
|
||||||
pub variable_types: bool,
|
pub variable_types: bool,
|
||||||
|
/// Whether to show function argument names.
|
||||||
|
///
|
||||||
|
/// For example, this would enable / disable hints like the ones quoted below:
|
||||||
|
/// ```python
|
||||||
|
/// def foo(x: int): pass
|
||||||
|
/// foo("x="1)
|
||||||
|
/// ```
|
||||||
|
pub function_argument_names: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InlayHintVisitor<'a, 'db> {
|
struct InlayHintVisitor<'a, 'db> {
|
||||||
|
db: &'db dyn Db,
|
||||||
model: SemanticModel<'db>,
|
model: SemanticModel<'db>,
|
||||||
hints: Vec<InlayHint<'db>>,
|
hints: Vec<InlayHint<'db>>,
|
||||||
in_assignment: bool,
|
in_assignment: bool,
|
||||||
|
@ -89,6 +98,7 @@ struct InlayHintVisitor<'a, 'db> {
|
||||||
impl<'a, 'db> InlayHintVisitor<'a, 'db> {
|
impl<'a, 'db> InlayHintVisitor<'a, 'db> {
|
||||||
fn new(db: &'db dyn Db, file: File, range: TextRange, settings: &'a InlayHintSettings) -> Self {
|
fn new(db: &'db dyn Db, file: File, range: TextRange, settings: &'a InlayHintSettings) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
db,
|
||||||
model: SemanticModel::new(db, file),
|
model: SemanticModel::new(db, file),
|
||||||
hints: Vec::new(),
|
hints: Vec::new(),
|
||||||
in_assignment: false,
|
in_assignment: false,
|
||||||
|
@ -98,11 +108,29 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) {
|
fn add_type_hint(&mut self, position: TextSize, ty: Type<'db>) {
|
||||||
|
if !self.settings.variable_types {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.hints.push(InlayHint {
|
self.hints.push(InlayHint {
|
||||||
position,
|
position,
|
||||||
content: InlayHintContent::Type(ty),
|
content: InlayHintContent::Type(ty),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_function_argument_name(&mut self, position: TextSize, name: String) {
|
||||||
|
if !self.settings.function_argument_names {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if name.starts_with('_') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hints.push(InlayHint {
|
||||||
|
position,
|
||||||
|
content: InlayHintContent::FunctionArgumentName(name),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||||
|
@ -123,25 +151,23 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||||
|
|
||||||
match stmt {
|
match stmt {
|
||||||
Stmt::Assign(assign) => {
|
Stmt::Assign(assign) => {
|
||||||
if !self.settings.variable_types {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.in_assignment = true;
|
self.in_assignment = true;
|
||||||
for target in &assign.targets {
|
for target in &assign.targets {
|
||||||
self.visit_expr(target);
|
self.visit_expr(target);
|
||||||
}
|
}
|
||||||
self.in_assignment = false;
|
self.in_assignment = false;
|
||||||
|
|
||||||
|
self.visit_expr(&assign.value);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Stmt::Expr(expr) => {
|
||||||
|
self.visit_expr(&expr.value);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO
|
// TODO
|
||||||
Stmt::FunctionDef(_) => {}
|
Stmt::FunctionDef(_) => {}
|
||||||
Stmt::For(_) => {}
|
Stmt::For(_) => {}
|
||||||
Stmt::Expr(_) => {
|
|
||||||
// Don't traverse into expression statements because we don't show any hints.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,15 +175,32 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visit_expr(&mut self, expr: &'_ Expr) {
|
fn visit_expr(&mut self, expr: &'_ Expr) {
|
||||||
if !self.in_assignment {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match expr {
|
match expr {
|
||||||
Expr::Name(name) => {
|
Expr::Name(name) => {
|
||||||
if name.ctx.is_store() {
|
if self.in_assignment {
|
||||||
let ty = expr.inferred_type(&self.model);
|
if name.ctx.is_store() {
|
||||||
self.add_type_hint(expr.range().end(), ty);
|
let ty = expr.inferred_type(&self.model);
|
||||||
|
self.add_type_hint(expr.range().end(), ty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
source_order::walk_expr(self, expr);
|
||||||
|
}
|
||||||
|
Expr::Call(call) => {
|
||||||
|
let argument_names =
|
||||||
|
inlay_hint_function_argument_details(self.db, &self.model, call)
|
||||||
|
.map(|details| details.argument_names)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
self.visit_expr(&call.func);
|
||||||
|
|
||||||
|
for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() {
|
||||||
|
if let Some(name) = argument_names.get(&index) {
|
||||||
|
self.add_function_argument_name(
|
||||||
|
arg_or_keyword.range().start(),
|
||||||
|
name.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.visit_expr(arg_or_keyword.value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -177,6 +220,7 @@ mod tests {
|
||||||
files::{File, system_path_to_file},
|
files::{File, system_path_to_file},
|
||||||
source::source_text,
|
source::source_text,
|
||||||
};
|
};
|
||||||
|
use ruff_python_trivia::textwrap::dedent;
|
||||||
use ruff_text_size::TextSize;
|
use ruff_text_size::TextSize;
|
||||||
|
|
||||||
use ruff_db::system::{DbWithWritableSystem, SystemPathBuf};
|
use ruff_db::system::{DbWithWritableSystem, SystemPathBuf};
|
||||||
|
@ -194,6 +238,8 @@ mod tests {
|
||||||
SystemPathBuf::from("/"),
|
SystemPathBuf::from("/"),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let source = dedent(source);
|
||||||
|
|
||||||
let start = source.find(START);
|
let start = source.find(START);
|
||||||
let end = source
|
let end = source
|
||||||
.find(END)
|
.find(END)
|
||||||
|
@ -245,6 +291,7 @@ mod tests {
|
||||||
fn inlay_hints(&self) -> String {
|
fn inlay_hints(&self) -> String {
|
||||||
self.inlay_hints_with_settings(&InlayHintSettings {
|
self.inlay_hints_with_settings(&InlayHintSettings {
|
||||||
variable_types: true,
|
variable_types: true,
|
||||||
|
function_argument_names: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,16 +361,529 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn disabled_variable_types() {
|
fn test_disabled_variable_types() {
|
||||||
let test = inlay_hint_test("x = 1");
|
let test = inlay_hint_test("x = 1");
|
||||||
|
|
||||||
assert_snapshot!(
|
assert_snapshot!(
|
||||||
test.inlay_hints_with_settings(&InlayHintSettings {
|
test.inlay_hints_with_settings(&InlayHintSettings {
|
||||||
variable_types: false,
|
variable_types: false,
|
||||||
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
@r"
|
@r"
|
||||||
x = 1
|
x = 1
|
||||||
"
|
"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_with_positional_or_keyword_parameter() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int): pass
|
||||||
|
foo(1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int): pass
|
||||||
|
foo([x=]1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_with_positional_only_parameter() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int, /): pass
|
||||||
|
foo(1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int, /): pass
|
||||||
|
foo(1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_with_variadic_parameter() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(*args: int): pass
|
||||||
|
foo(1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(*args: int): pass
|
||||||
|
foo(1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_with_keyword_variadic_parameter() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(**kwargs: int): pass
|
||||||
|
foo(x=1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(**kwargs: int): pass
|
||||||
|
foo(x=1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_with_keyword_only_parameter() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(*, x: int): pass
|
||||||
|
foo(x=1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(*, x: int): pass
|
||||||
|
foo(x=1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_positional_only_and_positional_or_keyword_parameters() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int, /, y: int): pass
|
||||||
|
foo(1, 2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int, /, y: int): pass
|
||||||
|
foo(1, [y=]2)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_positional_only_and_variadic_parameters() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int, /, *args: int): pass
|
||||||
|
foo(1, 2, 3)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int, /, *args: int): pass
|
||||||
|
foo(1, 2, 3)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_positional_only_and_keyword_variadic_parameters() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int, /, **kwargs: int): pass
|
||||||
|
foo(1, x=2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int, /, **kwargs: int): pass
|
||||||
|
foo(1, x=2)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_class_constructor_call_init() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, x: int): pass
|
||||||
|
Foo(1)
|
||||||
|
f = Foo(1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, x: int): pass
|
||||||
|
Foo([x=]1)
|
||||||
|
f[: Foo] = Foo([x=]1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_class_constructor_call_new() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
class Foo:
|
||||||
|
def __new__(cls, x: int): pass
|
||||||
|
Foo(1)
|
||||||
|
f = Foo(1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
class Foo:
|
||||||
|
def __new__(cls, x: int): pass
|
||||||
|
Foo([x=]1)
|
||||||
|
f[: Foo] = Foo([x=]1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_class_constructor_call_meta_class_call() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
class MetaFoo:
|
||||||
|
def __call__(self, x: int): pass
|
||||||
|
class Foo(metaclass=MetaFoo):
|
||||||
|
pass
|
||||||
|
Foo(1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
class MetaFoo:
|
||||||
|
def __call__(self, x: int): pass
|
||||||
|
class Foo(metaclass=MetaFoo):
|
||||||
|
pass
|
||||||
|
Foo([x=]1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_callable_call() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
from typing import Callable
|
||||||
|
def foo(x: Callable[[int], int]):
|
||||||
|
x(1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
from typing import Callable
|
||||||
|
def foo(x: Callable[[int], int]):
|
||||||
|
x(1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_instance_method_call() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
class Foo:
|
||||||
|
def bar(self, y: int): pass
|
||||||
|
Foo().bar(2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
class Foo:
|
||||||
|
def bar(self, y: int): pass
|
||||||
|
Foo().bar([y=]2)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_class_method_call() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
class Foo:
|
||||||
|
@classmethod
|
||||||
|
def bar(cls, y: int): pass
|
||||||
|
Foo.bar(2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
class Foo:
|
||||||
|
@classmethod
|
||||||
|
def bar(cls, y: int): pass
|
||||||
|
Foo.bar([y=]2)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_static_method_call() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
class Foo:
|
||||||
|
@staticmethod
|
||||||
|
def bar(y: int): pass
|
||||||
|
Foo.bar(2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
class Foo:
|
||||||
|
@staticmethod
|
||||||
|
def bar(y: int): pass
|
||||||
|
Foo.bar([y=]2)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_with_union_type() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int | str): pass
|
||||||
|
foo(1)
|
||||||
|
foo('abc')",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int | str): pass
|
||||||
|
foo([x=]1)
|
||||||
|
foo([x=]'abc')
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_multiple_positional_arguments() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int, y: str, z: bool): pass
|
||||||
|
foo(1, 'hello', True)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int, y: str, z: bool): pass
|
||||||
|
foo([x=]1, [y=]'hello', [z=]True)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_mixed_positional_and_keyword() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int, y: str, z: bool): pass
|
||||||
|
foo(1, z=True, y='hello')",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int, y: str, z: bool): pass
|
||||||
|
foo([x=]1, z=True, y='hello')
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_with_default_parameters() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int, y: str = 'default', z: bool = False): pass
|
||||||
|
foo(1)
|
||||||
|
foo(1, 'custom')
|
||||||
|
foo(1, 'custom', True)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int, y: str = 'default', z: bool = False): pass
|
||||||
|
foo([x=]1)
|
||||||
|
foo([x=]1, [y=]'custom')
|
||||||
|
foo([x=]1, [y=]'custom', [z=]True)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nested_function_calls() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int) -> int:
|
||||||
|
return x * 2
|
||||||
|
|
||||||
|
def bar(y: str) -> str:
|
||||||
|
return y
|
||||||
|
|
||||||
|
def baz(a: int, b: str, c: bool): pass
|
||||||
|
|
||||||
|
baz(foo(5), bar(bar('test')), True)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int) -> int:
|
||||||
|
return x * 2
|
||||||
|
|
||||||
|
def bar(y: str) -> str:
|
||||||
|
return y
|
||||||
|
|
||||||
|
def baz(a: int, b: str, c: bool): pass
|
||||||
|
|
||||||
|
baz([a=]foo([x=]5), [b=]bar([y=]bar([y=]'test')), [c=]True)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_method_chaining() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
class A:
|
||||||
|
def foo(self, value: int) -> 'A':
|
||||||
|
return self
|
||||||
|
def bar(self, name: str) -> 'A':
|
||||||
|
return self
|
||||||
|
def baz(self): pass
|
||||||
|
A().foo(42).bar('test').baz()",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
class A:
|
||||||
|
def foo(self, value: int) -> 'A':
|
||||||
|
return self
|
||||||
|
def bar(self, name: str) -> 'A':
|
||||||
|
return self
|
||||||
|
def baz(self): pass
|
||||||
|
A().foo([value=]42).bar([name=]'test').baz()
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nexted_keyword_function_calls() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: str) -> str:
|
||||||
|
return x
|
||||||
|
def bar(y: int): pass
|
||||||
|
bar(y=foo('test'))
|
||||||
|
",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: str) -> str:
|
||||||
|
return x
|
||||||
|
def bar(y: int): pass
|
||||||
|
bar(y=foo([x=]'test'))
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lambda_function_calls() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
foo = lambda x: x * 2
|
||||||
|
bar = lambda a, b: a + b
|
||||||
|
foo(5)
|
||||||
|
bar(1, 2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
foo[: (x) -> Unknown] = lambda x: x * 2
|
||||||
|
bar[: (a, b) -> Unknown] = lambda a, b: a + b
|
||||||
|
foo([x=]5)
|
||||||
|
bar([a=]1, [b=]2)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_complex_parameter_combinations() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass
|
||||||
|
foo(1, 'pos', 3.14, False, e=42)
|
||||||
|
foo(1, 'pos', 3.14, e=42, f='custom')",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(a: int, b: str, /, c: float, d: bool = True, *, e: int, f: str = 'default'): pass
|
||||||
|
foo(1, 'pos', [c=]3.14, [d=]False, e=42)
|
||||||
|
foo(1, 'pos', [c=]3.14, e=42, f='custom')
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_generic_function_calls() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
from typing import TypeVar, Generic
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
def identity(x: T) -> T:
|
||||||
|
return x
|
||||||
|
|
||||||
|
identity(42)
|
||||||
|
identity('hello')",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r#"
|
||||||
|
from typing import TypeVar, Generic
|
||||||
|
|
||||||
|
T[: typing.TypeVar("T")] = TypeVar([name=]'T')
|
||||||
|
|
||||||
|
def identity(x: T) -> T:
|
||||||
|
return x
|
||||||
|
|
||||||
|
identity([x=]42)
|
||||||
|
identity([x=]'hello')
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_overloaded_function_calls() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def foo(x: int) -> str: ...
|
||||||
|
@overload
|
||||||
|
def foo(x: str) -> int: ...
|
||||||
|
def foo(x):
|
||||||
|
return x
|
||||||
|
|
||||||
|
foo(42)
|
||||||
|
foo('hello')",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def foo(x: int) -> str: ...
|
||||||
|
@overload
|
||||||
|
def foo(x: str) -> int: ...
|
||||||
|
def foo(x):
|
||||||
|
return x
|
||||||
|
|
||||||
|
foo([x=]42)
|
||||||
|
foo([x=]'hello')
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_disabled_function_argument_names() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(x: int): pass
|
||||||
|
foo(1)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints_with_settings(&InlayHintSettings {
|
||||||
|
function_argument_names: false,
|
||||||
|
..Default::default()
|
||||||
|
}), @r"
|
||||||
|
def foo(x: int): pass
|
||||||
|
foo(1)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_out_of_range() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
<START>def foo(x: int): pass
|
||||||
|
def bar(y: int): pass
|
||||||
|
foo(1)<END>
|
||||||
|
bar(2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(x: int): pass
|
||||||
|
def bar(y: int): pass
|
||||||
|
foo([x=]1)
|
||||||
|
bar(2)
|
||||||
|
");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_function_call_with_argument_name_starting_with_underscore() {
|
||||||
|
let test = inlay_hint_test(
|
||||||
|
"
|
||||||
|
def foo(_x: int, y: int): pass
|
||||||
|
foo(1, 2)",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_snapshot!(test.inlay_hints(), @r"
|
||||||
|
def foo(_x: int, y: int): pass
|
||||||
|
foo(1, [y=]2)
|
||||||
|
");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,11 @@ use ruff_db::parsed::parsed_module;
|
||||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||||
use ty_python_semantic::ResolvedDefinition;
|
use ty_python_semantic::ResolvedDefinition;
|
||||||
|
use ty_python_semantic::SemanticModel;
|
||||||
use ty_python_semantic::semantic_index::definition::Definition;
|
use ty_python_semantic::semantic_index::definition::Definition;
|
||||||
use ty_python_semantic::types::{CallSignatureDetails, call_signature_details};
|
use ty_python_semantic::types::{
|
||||||
|
CallSignatureDetails, call_signature_details, find_active_signature_from_details,
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: We may want to add special-case handling for calls to constructors
|
// TODO: We may want to add special-case handling for calls to constructors
|
||||||
// so the class docstring is used in place of (or inaddition to) any docstring
|
// so the class docstring is used in place of (or inaddition to) any docstring
|
||||||
|
@ -66,9 +69,11 @@ pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<Signa
|
||||||
// Get the call expression at the given position.
|
// Get the call expression at the given position.
|
||||||
let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?;
|
let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?;
|
||||||
|
|
||||||
|
let model = SemanticModel::new(db, file);
|
||||||
|
|
||||||
// Get signature details from the semantic analyzer.
|
// Get signature details from the semantic analyzer.
|
||||||
let signature_details: Vec<CallSignatureDetails<'_>> =
|
let signature_details: Vec<CallSignatureDetails<'_>> =
|
||||||
call_signature_details(db, file, call_expr);
|
call_signature_details(db, &model, call_expr);
|
||||||
|
|
||||||
if signature_details.is_empty() {
|
if signature_details.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
|
@ -258,45 +263,6 @@ fn create_parameters_from_offsets(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the active signature index from `CallSignatureDetails`.
|
|
||||||
/// The active signature is the first signature where all arguments present in the call
|
|
||||||
/// have valid mappings to parameters (i.e., none of the mappings are None).
|
|
||||||
fn find_active_signature_from_details(signature_details: &[CallSignatureDetails]) -> Option<usize> {
|
|
||||||
let first = signature_details.first()?;
|
|
||||||
|
|
||||||
// If there are no arguments in the mapping, just return the first signature.
|
|
||||||
if first.argument_to_parameter_mapping.is_empty() {
|
|
||||||
return Some(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, try to find a signature where all arguments have valid parameter mappings.
|
|
||||||
let perfect_match = signature_details.iter().position(|details| {
|
|
||||||
// Check if all arguments have valid parameter mappings.
|
|
||||||
details
|
|
||||||
.argument_to_parameter_mapping
|
|
||||||
.iter()
|
|
||||||
.all(|mapping| mapping.matched)
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(index) = perfect_match {
|
|
||||||
return Some(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no perfect match, find the signature with the most valid argument mappings.
|
|
||||||
let (best_index, _) = signature_details
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.max_by_key(|(_, details)| {
|
|
||||||
details
|
|
||||||
.argument_to_parameter_mapping
|
|
||||||
.iter()
|
|
||||||
.filter(|mapping| mapping.matched)
|
|
||||||
.count()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Some(best_index)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::signature_help::SignatureHelpInfo;
|
use crate::signature_help::SignatureHelpInfo;
|
||||||
|
|
|
@ -53,7 +53,7 @@ use crate::types::generics::{
|
||||||
pub use crate::types::ide_support::{
|
pub use crate::types::ide_support::{
|
||||||
CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name,
|
CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name,
|
||||||
definitions_for_attribute, definitions_for_imported_symbol, definitions_for_keyword_argument,
|
definitions_for_attribute, definitions_for_imported_symbol, definitions_for_keyword_argument,
|
||||||
definitions_for_name,
|
definitions_for_name, find_active_signature_from_details, inlay_hint_function_argument_details,
|
||||||
};
|
};
|
||||||
use crate::types::infer::infer_unpack_types;
|
use crate::types::infer::infer_unpack_types;
|
||||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::place::{
|
use crate::place::{
|
||||||
Place, builtins_module_scope, imported_symbol, place_from_bindings, place_from_declarations,
|
Place, builtins_module_scope, imported_symbol, place_from_bindings, place_from_declarations,
|
||||||
|
@ -15,8 +16,8 @@ use crate::types::{ClassBase, ClassLiteral, DynamicType, KnownClass, KnownInstan
|
||||||
use crate::{Db, HasType, NameKind, SemanticModel};
|
use crate::{Db, HasType, NameKind, SemanticModel};
|
||||||
use ruff_db::files::{File, FileRange};
|
use ruff_db::files::{File, FileRange};
|
||||||
use ruff_db::parsed::parsed_module;
|
use ruff_db::parsed::parsed_module;
|
||||||
use ruff_python_ast as ast;
|
|
||||||
use ruff_python_ast::name::Name;
|
use ruff_python_ast::name::Name;
|
||||||
|
use ruff_python_ast::{self as ast};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
|
||||||
|
@ -805,17 +806,16 @@ pub struct CallSignatureDetails<'db> {
|
||||||
/// (in case of overloads or union types).
|
/// (in case of overloads or union types).
|
||||||
pub fn call_signature_details<'db>(
|
pub fn call_signature_details<'db>(
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
file: File,
|
model: &SemanticModel<'db>,
|
||||||
call_expr: &ast::ExprCall,
|
call_expr: &ast::ExprCall,
|
||||||
) -> Vec<CallSignatureDetails<'db>> {
|
) -> Vec<CallSignatureDetails<'db>> {
|
||||||
let model = SemanticModel::new(db, file);
|
let func_type = call_expr.func.inferred_type(model);
|
||||||
let func_type = call_expr.func.inferred_type(&model);
|
|
||||||
|
|
||||||
// Use into_callable to handle all the complex type conversions
|
// Use into_callable to handle all the complex type conversions
|
||||||
if let Some(callable_type) = func_type.into_callable(db) {
|
if let Some(callable_type) = func_type.into_callable(db) {
|
||||||
let call_arguments =
|
let call_arguments =
|
||||||
CallArguments::from_arguments(db, &call_expr.arguments, |_, splatted_value| {
|
CallArguments::from_arguments(db, &call_expr.arguments, |_, splatted_value| {
|
||||||
splatted_value.inferred_type(&model)
|
splatted_value.inferred_type(model)
|
||||||
});
|
});
|
||||||
let bindings = callable_type.bindings(db).match_parameters(&call_arguments);
|
let bindings = callable_type.bindings(db).match_parameters(&call_arguments);
|
||||||
|
|
||||||
|
@ -845,6 +845,102 @@ pub fn call_signature_details<'db>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the active signature index from `CallSignatureDetails`.
|
||||||
|
/// The active signature is the first signature where all arguments present in the call
|
||||||
|
/// have valid mappings to parameters (i.e., none of the mappings are None).
|
||||||
|
pub fn find_active_signature_from_details(
|
||||||
|
signature_details: &[CallSignatureDetails],
|
||||||
|
) -> Option<usize> {
|
||||||
|
let first = signature_details.first()?;
|
||||||
|
|
||||||
|
// If there are no arguments in the mapping, just return the first signature.
|
||||||
|
if first.argument_to_parameter_mapping.is_empty() {
|
||||||
|
return Some(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, try to find a signature where all arguments have valid parameter mappings.
|
||||||
|
let perfect_match = signature_details.iter().position(|details| {
|
||||||
|
// Check if all arguments have valid parameter mappings.
|
||||||
|
details
|
||||||
|
.argument_to_parameter_mapping
|
||||||
|
.iter()
|
||||||
|
.all(|mapping| mapping.matched)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(index) = perfect_match {
|
||||||
|
return Some(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no perfect match, find the signature with the most valid argument mappings.
|
||||||
|
let (best_index, _) = signature_details
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.max_by_key(|(_, details)| {
|
||||||
|
details
|
||||||
|
.argument_to_parameter_mapping
|
||||||
|
.iter()
|
||||||
|
.filter(|mapping| mapping.matched)
|
||||||
|
.count()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some(best_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct InlayHintFunctionArgumentDetails {
|
||||||
|
pub argument_names: HashMap<usize, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn inlay_hint_function_argument_details<'db>(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
model: &SemanticModel<'db>,
|
||||||
|
call_expr: &ast::ExprCall,
|
||||||
|
) -> Option<InlayHintFunctionArgumentDetails> {
|
||||||
|
let signature_details = call_signature_details(db, model, call_expr);
|
||||||
|
|
||||||
|
if signature_details.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let active_signature_index = find_active_signature_from_details(&signature_details)?;
|
||||||
|
|
||||||
|
let call_signature_details = signature_details.get(active_signature_index)?;
|
||||||
|
|
||||||
|
let parameters = call_signature_details.signature.parameters();
|
||||||
|
let mut argument_names = HashMap::new();
|
||||||
|
|
||||||
|
for arg_index in 0..call_expr.arguments.args.len() {
|
||||||
|
let Some(arg_mapping) = call_signature_details
|
||||||
|
.argument_to_parameter_mapping
|
||||||
|
.get(arg_index)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !arg_mapping.matched {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(param_index) = arg_mapping.parameters.first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(param) = parameters.get(*param_index) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add hints for parameters that can be specified by name
|
||||||
|
if !param.is_positional_only() && !param.is_variadic() && !param.is_keyword_variadic() {
|
||||||
|
let Some(name) = param.name() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
argument_names.insert(arg_index, name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(InlayHintFunctionArgumentDetails { argument_names })
|
||||||
|
}
|
||||||
|
|
||||||
/// Find the text range of a specific parameter in function parameters by name.
|
/// Find the text range of a specific parameter in function parameters by name.
|
||||||
/// Only searches for parameters that can be addressed by name in keyword arguments.
|
/// Only searches for parameters that can be addressed by name in keyword arguments.
|
||||||
fn find_parameter_range(parameters: &ast::Parameters, parameter_name: &str) -> Option<TextRange> {
|
fn find_parameter_range(parameters: &ast::Parameters, parameter_name: &str) -> Option<TextRange> {
|
||||||
|
|
|
@ -237,12 +237,14 @@ impl WorkspaceOptions {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct InlayHintOptions {
|
struct InlayHintOptions {
|
||||||
variable_types: Option<bool>,
|
variable_types: Option<bool>,
|
||||||
|
function_argument_names: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InlayHintOptions {
|
impl InlayHintOptions {
|
||||||
fn into_settings(self) -> InlayHintSettings {
|
fn into_settings(self) -> InlayHintSettings {
|
||||||
InlayHintSettings {
|
InlayHintSettings {
|
||||||
variable_types: self.variable_types.unwrap_or_default(),
|
variable_types: self.variable_types.unwrap_or_default(),
|
||||||
|
function_argument_names: self.function_argument_names.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -438,6 +438,7 @@ impl Workspace {
|
||||||
// TODO: Provide a way to configure this
|
// TODO: Provide a way to configure this
|
||||||
&InlayHintSettings {
|
&InlayHintSettings {
|
||||||
variable_types: true,
|
variable_types: true,
|
||||||
|
function_argument_names: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue