Skip to content

Commit d3af51f

Browse files
aaron-angmeta-codesync[bot]
authored andcommitted
Track positional and keyword arguments (#1459)
Summary: Close #1417. We need to save the keyword identifier during signature range search to find the correct position when creating signature information. I added some test cases to `signature_help.rs`, and manually verified the fix by building and running LSP locally: <img width="933" height="216" alt="Screenshot 2025-11-02 at 10 51 53 PM" src="https://github.com/user-attachments/assets/51ae3373-e87a-4f92-8d9a-b1144c1c1214" /> There is also a bug in positional arguments, as the newly-added `positional_arguments_test` fails on the `main` branch. To fix this, we need to count the number of separators (commas) and adjust the index accordingly. Pull Request resolved: #1459 Reviewed By: stroxler Differential Revision: D86417276 Pulled By: kinto0 fbshipit-source-id: 60ca6613815e44fad27e3304faf00ee9b6400b6b
1 parent 44c5980 commit d3af51f

File tree

3 files changed

+227
-27
lines changed

3 files changed

+227
-27
lines changed

crates/pyrefly_types/src/callable.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,15 @@ impl Param {
660660
}
661661
}
662662

663+
pub fn name(&self) -> Option<&Name> {
664+
match self {
665+
Param::PosOnly(name, ..) | Param::VarArg(name, ..) | Param::Kwargs(name, ..) => {
666+
name.as_ref()
667+
}
668+
Param::Pos(name, ..) | Param::KwOnly(name, ..) => Some(name),
669+
}
670+
}
671+
663672
pub fn as_type(&self) -> &Type {
664673
match self {
665674
Param::PosOnly(_, ty, _)

pyrefly/lib/state/lsp.rs

Lines changed: 125 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,17 @@ pub struct FindDefinitionItem {
478478
pub module: Module,
479479
}
480480

481+
/// The currently active argument in a function call for signature help.
482+
#[derive(Debug)]
483+
enum ActiveArgument {
484+
/// The cursor is within an existing positional argument at the given index.
485+
Positional(usize),
486+
/// The cursor is within a keyword argument whose name is provided.
487+
Keyword(Name),
488+
/// The cursor is in the argument list but not inside any argument expression yet.
489+
Next(usize),
490+
}
491+
481492
impl<'a> Transaction<'a> {
482493
fn get_type(&self, handle: &Handle, key: &Key) -> Option<Type> {
483494
let idx = self.get_bindings(handle)?.key_to_idx(key);
@@ -848,42 +859,93 @@ impl<'a> Transaction<'a> {
848859
fn visit_finding_signature_range(
849860
x: &Expr,
850861
find: TextSize,
851-
res: &mut Option<(TextRange, TextRange, usize)>,
862+
res: &mut Option<(TextRange, TextRange, ActiveArgument)>,
852863
) {
853864
if let Expr::Call(call) = x
854865
&& call.arguments.range.contains_inclusive(find)
855866
{
856-
for (i, arg) in call.arguments.args.as_ref().iter().enumerate() {
857-
if arg.range().contains_inclusive(find) {
858-
Self::visit_finding_signature_range(arg, find, res);
859-
if res.is_some() {
860-
return;
861-
}
862-
*res = Some((call.func.range(), call.arguments.range, i));
863-
}
867+
if Self::visit_positional_signature_args(call, find, res) {
868+
return;
869+
}
870+
if Self::visit_keyword_signature_args(call, find, res) {
871+
return;
864872
}
865873
if res.is_none() {
866874
*res = Some((
867875
call.func.range(),
868876
call.arguments.range,
869-
call.arguments.len(),
877+
ActiveArgument::Next(call.arguments.len()),
870878
));
871879
}
872880
} else {
873881
x.recurse(&mut |x| Self::visit_finding_signature_range(x, find, res));
874882
}
875883
}
876884

885+
fn visit_positional_signature_args(
886+
call: &ExprCall,
887+
find: TextSize,
888+
res: &mut Option<(TextRange, TextRange, ActiveArgument)>,
889+
) -> bool {
890+
for (i, arg) in call.arguments.args.as_ref().iter().enumerate() {
891+
if arg.range().contains_inclusive(find) {
892+
Self::visit_finding_signature_range(arg, find, res);
893+
if res.is_some() {
894+
return true;
895+
}
896+
*res = Some((
897+
call.func.range(),
898+
call.arguments.range,
899+
ActiveArgument::Positional(i),
900+
));
901+
return true;
902+
}
903+
}
904+
false
905+
}
906+
907+
fn visit_keyword_signature_args(
908+
call: &ExprCall,
909+
find: TextSize,
910+
res: &mut Option<(TextRange, TextRange, ActiveArgument)>,
911+
) -> bool {
912+
let kwarg_start_idx = call.arguments.args.len();
913+
for (j, kw) in call.arguments.keywords.iter().enumerate() {
914+
if kw.range.contains_inclusive(find) {
915+
Self::visit_finding_signature_range(&kw.value, find, res);
916+
if res.is_some() {
917+
return true;
918+
}
919+
let active_argument = match kw.arg.as_ref() {
920+
Some(identifier) => ActiveArgument::Keyword(identifier.id.clone()),
921+
None => ActiveArgument::Positional(kwarg_start_idx + j),
922+
};
923+
*res = Some((call.func.range(), call.arguments.range, active_argument));
924+
return true;
925+
}
926+
}
927+
false
928+
}
929+
877930
/// Finds the callable(s) (multiple if overloads exist) at position in document, returning them, chosen overload index, and arg index
878931
fn get_callables_from_call(
879932
&self,
880933
handle: &Handle,
881934
position: TextSize,
882-
) -> Option<(Vec<Type>, usize, usize)> {
935+
) -> Option<(Vec<Type>, usize, ActiveArgument)> {
883936
let mod_module = self.get_ast(handle)?;
884937
let mut res = None;
885938
mod_module.visit(&mut |x| Self::visit_finding_signature_range(x, position, &mut res));
886-
let (callee_range, call_args_range, arg_index) = res?;
939+
let (callee_range, call_args_range, mut active_argument) = res?;
940+
// When the cursor is in the argument list but not inside any argument yet,
941+
// estimate the would-be positional index by counting commas up to the cursor.
942+
// This keeps signature help useful even before the user starts typing the next arg.
943+
if let ActiveArgument::Next(index) = &mut active_argument
944+
&& let Some(next_index) =
945+
self.count_argument_separators_before(handle, call_args_range, position)
946+
{
947+
*index = next_index;
948+
}
887949
let answers = self.get_answers(handle)?;
888950
if let Some((overloads, chosen_overload_index)) =
889951
answers.get_all_overload_trace(call_args_range)
@@ -892,12 +954,12 @@ impl<'a> Transaction<'a> {
892954
Some((
893955
callables,
894956
chosen_overload_index.unwrap_or_default(),
895-
arg_index,
957+
active_argument,
896958
))
897959
} else {
898960
answers
899961
.get_type_trace(callee_range)
900-
.map(|t| (vec![t], 0, arg_index))
962+
.map(|t| (vec![t], 0, active_argument))
901963
}
902964
}
903965

@@ -907,27 +969,33 @@ impl<'a> Transaction<'a> {
907969
position: TextSize,
908970
) -> Option<SignatureHelp> {
909971
self.get_callables_from_call(handle, position).map(
910-
|(callables, chosen_overload_index, arg_index)| SignatureHelp {
911-
signatures: callables
972+
|(callables, chosen_overload_index, active_argument)| {
973+
let signatures = callables
912974
.into_iter()
913-
.map(|t| Self::create_signature_information(t, arg_index))
914-
.collect_vec(),
915-
active_signature: Some(chosen_overload_index as u32),
916-
active_parameter: Some(arg_index as u32),
975+
.map(|t| Self::create_signature_information(t, &active_argument))
976+
.collect_vec();
977+
let active_parameter = signatures
978+
.get(chosen_overload_index)
979+
.and_then(|info| info.active_parameter);
980+
SignatureHelp {
981+
signatures,
982+
active_signature: Some(chosen_overload_index as u32),
983+
active_parameter,
984+
}
917985
},
918986
)
919987
}
920988

921-
fn create_signature_information(type_: Type, arg_index: usize) -> SignatureInformation {
989+
fn create_signature_information(
990+
type_: Type,
991+
active_argument: &ActiveArgument,
992+
) -> SignatureInformation {
922993
let type_ = type_.deterministic_printing();
923994
let label = type_.as_hover_string();
924995
let (parameters, active_parameter) =
925996
if let Some(params) = Self::normalize_singleton_function_type_into_params(type_) {
926-
let active_parameter = if arg_index < params.len() {
927-
Some(arg_index as u32)
928-
} else {
929-
None
930-
};
997+
let active_parameter =
998+
Self::active_parameter_index(&params, active_argument).map(|idx| idx as u32);
931999
(
9321000
Some(params.map(|param| ParameterInformation {
9331001
label: ParameterLabel::Simple(format!("{param}")),
@@ -946,6 +1014,35 @@ impl<'a> Transaction<'a> {
9461014
}
9471015
}
9481016

1017+
fn active_parameter_index(params: &[Param], active_argument: &ActiveArgument) -> Option<usize> {
1018+
match active_argument {
1019+
ActiveArgument::Positional(index) | ActiveArgument::Next(index) => {
1020+
(*index < params.len()).then_some(*index)
1021+
}
1022+
ActiveArgument::Keyword(name) => params
1023+
.iter()
1024+
.position(|param| param.name().is_some_and(|param_name| param_name == name)),
1025+
}
1026+
}
1027+
1028+
fn count_argument_separators_before(
1029+
&self,
1030+
handle: &Handle,
1031+
arguments_range: TextRange,
1032+
position: TextSize,
1033+
) -> Option<usize> {
1034+
let module = self.get_module_info(handle)?;
1035+
let contents = module.contents();
1036+
let len = contents.len();
1037+
let start = arguments_range.start().to_usize().min(len);
1038+
let end = arguments_range.end().to_usize().min(len);
1039+
let pos = position.to_usize().clamp(start, end);
1040+
contents
1041+
.get(start..pos)
1042+
.map(|slice| slice.bytes().filter(|&b| b == b',').count())
1043+
.or(Some(0))
1044+
}
1045+
9491046
fn normalize_singleton_function_type_into_params(type_: Type) -> Option<Vec<Param>> {
9501047
let callable = type_.to_callable()?;
9511048
// We will drop the self parameter for signature help
@@ -2269,11 +2366,12 @@ impl<'a> Transaction<'a> {
22692366
position: TextSize,
22702367
completions: &mut Vec<CompletionItem>,
22712368
) {
2272-
if let Some((callables, chosen_overload_index, arg_index)) =
2369+
if let Some((callables, chosen_overload_index, active_argument)) =
22732370
self.get_callables_from_call(handle, position)
22742371
&& let Some(callable) = callables.get(chosen_overload_index)
22752372
&& let Some(params) =
22762373
Self::normalize_singleton_function_type_into_params(callable.clone())
2374+
&& let Some(arg_index) = Self::active_parameter_index(&params, &active_argument)
22772375
&& let Some(param) = params.get(arg_index)
22782376
{
22792377
Self::add_literal_completions_from_type(param.as_type(), completions);

pyrefly/lib/test/lsp/signature_help.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,99 @@ Signature Help Result: active=0
128128
);
129129
}
130130

131+
#[test]
132+
fn positional_arguments_test() {
133+
let code = r#"
134+
def f(x: int, y: int, z: int) -> None: ...
135+
136+
f(1,,)
137+
# ^
138+
f(1,,)
139+
# ^
140+
f(1,,3)
141+
# ^
142+
"#;
143+
let report = get_batched_lsp_operations_report_allow_error(&[("main", code)], get_test_report);
144+
assert_eq!(
145+
r#"
146+
# main.py
147+
4 | f(1,,)
148+
^
149+
Signature Help Result: active=0
150+
- def f(
151+
x: int,
152+
y: int,
153+
z: int
154+
) -> None: ..., parameters=[x: int, y: int, z: int], active parameter = 1
155+
156+
6 | f(1,,)
157+
^
158+
Signature Help Result: active=0
159+
- def f(
160+
x: int,
161+
y: int,
162+
z: int
163+
) -> None: ..., parameters=[x: int, y: int, z: int], active parameter = 2
164+
165+
8 | f(1,,3)
166+
^
167+
Signature Help Result: active=0
168+
- def f(
169+
x: int,
170+
y: int,
171+
z: int
172+
) -> None: ..., parameters=[x: int, y: int, z: int], active parameter = 1
173+
"#
174+
.trim(),
175+
report.trim(),
176+
);
177+
}
178+
179+
#[test]
180+
fn keyword_arguments_test() {
181+
let code = r#"
182+
def f(a: str, b: int) -> None: ...
183+
184+
f(a)
185+
# ^
186+
f(a=)
187+
# ^
188+
f(b=)
189+
# ^
190+
"#;
191+
let report = get_batched_lsp_operations_report_allow_error(&[("main", code)], get_test_report);
192+
assert_eq!(
193+
r#"
194+
# main.py
195+
4 | f(a)
196+
^
197+
Signature Help Result: active=0
198+
- def f(
199+
a: str,
200+
b: int
201+
) -> None: ..., parameters=[a: str, b: int], active parameter = 0
202+
203+
6 | f(a=)
204+
^
205+
Signature Help Result: active=0
206+
- def f(
207+
a: str,
208+
b: int
209+
) -> None: ..., parameters=[a: str, b: int], active parameter = 0
210+
211+
8 | f(b=)
212+
^
213+
Signature Help Result: active=0
214+
- def f(
215+
a: str,
216+
b: int
217+
) -> None: ..., parameters=[a: str, b: int], active parameter = 1
218+
"#
219+
.trim(),
220+
report.trim(),
221+
);
222+
}
223+
131224
#[test]
132225
fn simple_incomplete_function_call_test() {
133226
let code = r#"

0 commit comments

Comments
 (0)