From 8cc2f2464aed81cdd771e57f251e11ef36db29f8 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:06:15 +0200 Subject: [PATCH 1/4] Record the parameter and type parameter counts of a navigable item Navigate To orders matches that are otherwise equal by these counts for C# and VB. NavigableItem now carries them: every curried and tupled argument of the method a declaration compiles to, without the instance and without a solitary unit argument, and its explicitly declared type parameters. Co-Authored-By: Claude Opus 5 --- .../.FSharp.Compiler.Service/11.0.100.md | 1 + src/Compiler/Service/ServiceNavigation.fs | 63 ++++++++++++++++--- src/Compiler/Service/ServiceNavigation.fsi | 20 ++++-- ...iler.Service.SurfaceArea.netstandard20.bsl | 6 +- .../FSharp.Compiler.Service.Tests.fsproj | 1 + .../NavigateToTests.fs | 63 +++++++++++++++++++ 6 files changed, 138 insertions(+), 16 deletions(-) create mode 100644 tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index 27f2c0070d7..bbb077eac55 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -150,6 +150,7 @@ * IL: fix leaking binary view ([PR #20250](https://github.com/dotnet/fsharp/pull/20250)) ### Added +* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. * Added a "most concrete" tiebreaker for overload resolution (`--langversion:preview`). ([RFC FS-1340](https://github.com/fsharp/fslang-design/pull/834), [PR #19277](https://github.com/dotnet/fsharp/pull/19277)) * Added support for `OverloadResolutionPriorityAttribute` in overload resolution (`--langversion:preview`). ([RFC FS-1338](https://github.com/fsharp/fslang-design/pull/828), [PR #19277](https://github.com/dotnet/fsharp/pull/19277)) diff --git a/src/Compiler/Service/ServiceNavigation.fs b/src/Compiler/Service/ServiceNavigation.fs index 41d565ebed4..3d2233690e4 100755 --- a/src/Compiler/Service/ServiceNavigation.fs +++ b/src/Compiler/Service/ServiceNavigation.fs @@ -762,10 +762,37 @@ type NavigableItem = IsSignature: bool Kind: NavigableItemKind Container: NavigableContainer + ParameterCount: int + TypeParameterCount: int } [] module NavigateTo = + let private typeParameterCountOf (typars: SynTyparDecls option) = + match typars with + | Some typars -> typars.TyparDecls.Length + | None -> 0 + + let rec private isUnitType synType = + match synType with + | SynType.LongIdent(SynLongIdent([ id ], _, _)) -> id.idText = "unit" + | SynType.Paren(innerType, _) + | SynType.WithGlobalConstraints(innerType, _, _) -> isUnitType innerType + | _ -> false + + /// The parameters of the compiled method: the parser leaves a solitary unit argument of a signature in its arity, + /// where a binding has already dropped it. + let private parameterCountOfSignature (SynValInfo(curriedArgInfos, _)) (synType: SynType) = + match curriedArgInfos, synType with + | [ [ _ ] ], SynType.Fun(argType = argType) when isUnitType argType -> 0 + | [ [ _ ] ], SynType.WithGlobalConstraints(SynType.Fun(argType = argType), _, _) when isUnitType argType -> 0 + | _ -> List.sumBy List.length curriedArgInfos + + let private parameterCountOfBinding (SynValData(memberFlags = memberFlags; valInfo = SynValInfo(curriedArgInfos, _))) = + match memberFlags, curriedArgInfos with + | Some memberFlags, _self :: argInfos when memberFlags.IsInstance -> List.sumBy List.length argInfos + | _ -> List.sumBy List.length curriedArgInfos + let GetNavigableItems (parsedInput: ParsedInput) : NavigableItem[] = let convertToDisplayName name = @@ -776,7 +803,7 @@ module NavigateTo = let result = ResizeArray() - let addLongIdent kind (lid: LongIdent) (isSignature: bool) (container: NavigableContainer) = + let addLongIdent kind (lid: LongIdent) (isSignature: bool) (container: NavigableContainer) typeParameterCount = if not lid.IsEmpty then let name = textOfLid lid @@ -787,10 +814,12 @@ module NavigateTo = IsSignature = isSignature Kind = kind Container = container + ParameterCount = 0 + TypeParameterCount = typeParameterCount } |> result.Add - let addIdent kind (id: Ident) (isSignature: bool) (container: NavigableContainer) = + let addIdentWithArity kind (id: Ident) (isSignature: bool) (container: NavigableContainer) parameterCount typeParameterCount = if not (String.IsNullOrEmpty id.idText) then let name = convertToDisplayName id.idText @@ -801,11 +830,16 @@ module NavigateTo = IsSignature = isSignature Kind = kind Container = container + ParameterCount = parameterCount + TypeParameterCount = typeParameterCount } |> result.Add + let addIdent kind id isSignature container = + addIdentWithArity kind id isSignature container 0 0 + let addModule lid isSig container = - addLongIdent NavigableItemKind.Module lid isSig container + addLongIdent NavigableItemKind.Module lid isSig container 0 let addModuleAbbreviation (id: Ident) isSig container = addIdent NavigableItemKind.ModuleAbbreviation id isSig container @@ -816,14 +850,17 @@ module NavigateTo = NavigableContainer.Container(NavigableContainerType.Exception, [ id.idText ], container) let addComponentInfo containerType kind (info: SynComponentInfo) isSig container = + let (SynComponentInfo(typeParams = typeParams)) = info let lid = info.LongIdent - addLongIdent kind lid isSig container + addLongIdent kind lid isSig container (typeParameterCountOf typeParams) NavigableContainer.Container(containerType, pathOfLid lid, container) let addValSig kind synValSig isSig container = - let (SynValSig(ident = SynIdent(id, _))) = synValSig - addIdent kind id isSig container + let (SynValSig(ident = SynIdent(id, _); explicitTypeParams = SynValTyparDecls(typars, _); synType = synType; arity = arity)) = + synValSig + + addIdentWithArity kind id isSig container (parameterCountOfSignature arity synType) (typeParameterCountOf typars) let addField synField isSig container = let (SynField(idOpt = id)) = synField @@ -861,17 +898,25 @@ module NavigateTo = | Some mf -> mapMemberKind mf.MemberKind | _ -> NavigableItemKind.ModuleValue + let typeParameterCount = + match headPat with + | SynPat.LongIdent(typarDecls = Some(SynValTyparDecls(typars, _))) -> typeParameterCountOf typars + | _ -> 0 + + let addBindingIdent id = + addIdentWithArity kind id false container (parameterCountOfBinding valData) typeParameterCount + match headPat with | SynPat.LongIdent(longDotId = SynLongIdent([ _; id ], _, _)) -> // instance members - addIdent kind id false container + addBindingIdent id | SynPat.LongIdent(longDotId = SynLongIdent([ id ], _, _)) -> // functions - addIdent kind id false container + addBindingIdent id | SynPat.Named(SynIdent(id, _), _, _, _) | SynPat.As(_, SynPat.Named(SynIdent(id, _), _, _, _), _) -> // values - addIdent kind id false container + addBindingIdent id | _ -> () let addMember valSig (memberFlags: SynMemberFlags) isSig container = diff --git a/src/Compiler/Service/ServiceNavigation.fsi b/src/Compiler/Service/ServiceNavigation.fsi index cfccd6ef20c..49475c7f93b 100755 --- a/src/Compiler/Service/ServiceNavigation.fsi +++ b/src/Compiler/Service/ServiceNavigation.fsi @@ -112,12 +112,20 @@ type NavigableContainer = member Name: string type NavigableItem = - { Name: string - NeedsBackticks: bool - Range: range - IsSignature: bool - Kind: NavigableItemKind - Container: NavigableContainer } + { + Name: string + NeedsBackticks: bool + Range: range + IsSignature: bool + Kind: NavigableItemKind + Container: NavigableContainer + /// The number of parameters of the method the declaration compiles to, as C# and VB count them to order + /// equally good Navigate To matches: every curried and tupled argument, without the instance and without a + /// solitary unit argument. + ParameterCount: int + /// The number of explicitly declared type parameters. + TypeParameterCount: int + } [] module public NavigateTo = diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl index 5c9c346b613..719aa266360 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.SurfaceArea.netstandard20.bsl @@ -3946,10 +3946,14 @@ FSharp.Compiler.EditorServices.NavigableItem: FSharp.Compiler.Text.Range Range FSharp.Compiler.EditorServices.NavigableItem: FSharp.Compiler.Text.Range get_Range() FSharp.Compiler.EditorServices.NavigableItem: Int32 GetHashCode() FSharp.Compiler.EditorServices.NavigableItem: Int32 GetHashCode(System.Collections.IEqualityComparer) +FSharp.Compiler.EditorServices.NavigableItem: Int32 ParameterCount +FSharp.Compiler.EditorServices.NavigableItem: Int32 TypeParameterCount +FSharp.Compiler.EditorServices.NavigableItem: Int32 get_ParameterCount() +FSharp.Compiler.EditorServices.NavigableItem: Int32 get_TypeParameterCount() FSharp.Compiler.EditorServices.NavigableItem: System.String Name FSharp.Compiler.EditorServices.NavigableItem: System.String ToString() FSharp.Compiler.EditorServices.NavigableItem: System.String get_Name() -FSharp.Compiler.EditorServices.NavigableItem: Void .ctor(System.String, Boolean, FSharp.Compiler.Text.Range, Boolean, FSharp.Compiler.EditorServices.NavigableItemKind, FSharp.Compiler.EditorServices.NavigableContainer) +FSharp.Compiler.EditorServices.NavigableItem: Void .ctor(System.String, Boolean, FSharp.Compiler.Text.Range, Boolean, FSharp.Compiler.EditorServices.NavigableItemKind, FSharp.Compiler.EditorServices.NavigableContainer, Int32, Int32) FSharp.Compiler.EditorServices.NavigableItemKind+Tags: Int32 Constructor FSharp.Compiler.EditorServices.NavigableItemKind+Tags: Int32 EnumCase FSharp.Compiler.EditorServices.NavigableItemKind+Tags: Int32 Exception diff --git a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj index e043d8554ad..f6fb240a665 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -51,6 +51,7 @@ + diff --git a/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs b/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs new file mode 100644 index 00000000000..cf67e20c870 --- /dev/null +++ b/tests/FSharp.Compiler.Service.Tests/NavigateToTests.fs @@ -0,0 +1,63 @@ +module FSharp.Compiler.Service.Tests.NavigateToTests + +open FSharp.Compiler.EditorServices +open FSharp.Compiler.Service.Tests.Common +open FSharp.Compiler.Syntax +open Xunit + +let private implementation = + """ +let value = 1 +let curried a b = a + b +let tupledAndCurried (a, b) c = a + b + c +let takesUnit () = 1 +let generic<'T> (x: 'T) = x + +type C<'T, 'U>() = + member _.Method(a: int, b: int) = a + b + member _.Property = 1 + static member Static x y = x + y +""" + +let private signature = + """ +module M + +val curried: int -> int -> int +val takesUnit: unit -> int +val generic<'T> : 'T * 'T -> 'T + +type C<'T> = + member Method: a: int * b: int -> int + abstract Abstract: unit -> unit +""" + +let private arityOf (parseTree: ParsedInput) name = + let item = + NavigateTo.GetNavigableItems parseTree + |> Array.find (fun item -> item.Name = name) + + item.ParameterCount, item.TypeParameterCount + +[] +[] +[] +[] +[] +[] +[] +[] +[] +[] +let ``A declaration in an implementation file counts the parameters it compiles to`` (name: string, parameterCount: int, typeParameterCount: int) = + Assert.Equal((parameterCount, typeParameterCount), arityOf (getParseResults implementation) name) + +[] +[] +[] +[] +[] +[] +[] +let ``A declaration in a signature file counts the parameters it compiles to`` (name: string, parameterCount: int, typeParameterCount: int) = + Assert.Equal((parameterCount, typeParameterCount), arityOf (getParseResultsOfSignatureFile signature) name) From b0f52c971dcc469d3f9223eab0b3a3b6544a1235 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:11:30 +0200 Subject: [PATCH 2/4] Link the release note to its PR Co-Authored-By: Claude Opus 5 --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md index bbb077eac55..6753d0ee080 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -150,7 +150,7 @@ * IL: fix leaking binary view ([PR #20250](https://github.com/dotnet/fsharp/pull/20250)) ### Added -* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. +* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. ([PR #20531](https://github.com/dotnet/fsharp/pull/20531)) * Added a "most concrete" tiebreaker for overload resolution (`--langversion:preview`). ([RFC FS-1340](https://github.com/fsharp/fslang-design/pull/834), [PR #19277](https://github.com/dotnet/fsharp/pull/19277)) * Added support for `OverloadResolutionPriorityAttribute` in overload resolution (`--langversion:preview`). ([RFC FS-1338](https://github.com/fsharp/fslang-design/pull/828), [PR #19277](https://github.com/dotnet/fsharp/pull/19277)) From e71e06581ff8cfad21011d70ec84a594c94093ff Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:12:01 +0200 Subject: [PATCH 3/4] Give Navigate To the counts it sorts equal F# matches by Navigate To orders matches of the same kind by the folder distance to the file being edited, then by parameter and type parameter count and name, for C# and VB. Roslyn computes the same key for F# results once they carry the counts, which NavigableItem now records. Needs the FSharpNavigateToSearchResult constructor that takes the counts, from dotnet/roslyn#85280. Co-Authored-By: Claude Opus 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 2 ++ .../src/FSharp.Editor/Navigation/NavigateToSearchService.fs | 4 +++- .../FSharp.Editor.Tests/NavigateToSearchServiceTests.fs | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index ba03f663967..205a6181303 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -5,6 +5,8 @@ ### Fixed +* Navigate To (Ctrl+T) orders F# matches of the same kind as it orders C# and Visual Basic ones: a match in the file being edited first, then by how far its folder is from that file, then by parameter and type parameter count and name. + * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) * Fixed Find All References crash when F# project contains non-F# files like `.cshtml`. ([Issue #16394](https://github.com/dotnet/fsharp/issues/16394), [PR #19252](https://github.com/dotnet/fsharp/pull/19252)) diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index 546b00e1b16..56a18f876db 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -179,7 +179,9 @@ type internal FSharpNavigateToSearchService ImmutableArray.Create(TaggedText(TextTags.Text, item.Name)), document, sourceSpan - ) + ), + item.ParameterCount, + item.TypeParameterCount ) | _ -> () |] diff --git a/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchServiceTests.fs index 08816ea6191..ccd9525c2aa 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchServiceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchServiceTests.fs @@ -72,3 +72,8 @@ module HeyHo = [] let ``nested containers`` () = assertResultsContain "hh.a.b.g.d" "Delta" + + [] + let ``results carry the counts Navigate To sorts equal matches by`` () = + let result = navigateToSearch "+>" |> Seq.find (fun i -> i.Name = "+>") + Assert.Equal((2, 0), (result.ParameterCount, result.TypeParameterCount)) From 31e13c8380006e2a24c0ae1d980737fe24c2c6a6 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:19:34 +0200 Subject: [PATCH 4/4] Link the release note to its PR Co-Authored-By: Claude Opus 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 205a6181303..06f29a9f23b 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -5,7 +5,7 @@ ### Fixed -* Navigate To (Ctrl+T) orders F# matches of the same kind as it orders C# and Visual Basic ones: a match in the file being edited first, then by how far its folder is from that file, then by parameter and type parameter count and name. +* Navigate To (Ctrl+T) orders F# matches of the same kind as it orders C# and Visual Basic ones: a match in the file being edited first, then by how far its folder is from that file, then by parameter and type parameter count and name. ([PR #20532](https://github.com/dotnet/fsharp/pull/20532)) * Improve Find All References performance by throttling parallel typechecks. ([PR #20128](https://github.com/dotnet/fsharp/pull/20128)) * Fixed Rename incorrectly renaming `get` and `set` keywords for properties with explicit accessors. ([Issue #18270](https://github.com/dotnet/fsharp/issues/18270), [PR #19252](https://github.com/dotnet/fsharp/pull/19252))