From 74ba7345797fecb5ecb81ff54faf1d92f5442f47 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:06:15 +0200 Subject: [PATCH 1/6] 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 f383908e195..a94b11cfd88 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -169,6 +169,7 @@ * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) ### Added +* `NavigableItem` carries `ParameterCount` and `TypeParameterCount`, the counts C# and VB give Navigate To to order matches that are otherwise equal. * FCS: add FSharpCheckFileResults.FileSignature ([PR #20478](https://github.com/dotnet/fsharp/pull/20478)) * Added the `ReraiseInComputationExpressions` language feature (`--langversion:preview`): `reraise ()` in the `with` handler of a computation expression is compiled to a rethrow through `ExceptionDispatchInfo` instead of being rejected with FS0413. ([Suggestion #660](https://github.com/fsharp/fslang-suggestions/issues/660), [RFC FS-1347](https://github.com/fsharp/fslang-design/pull/843), [PR #20405](https://github.com/dotnet/fsharp/pull/20405)) diff --git a/src/Compiler/Service/ServiceNavigation.fs b/src/Compiler/Service/ServiceNavigation.fs index f42b335da53..c09ab0c188e 100755 --- a/src/Compiler/Service/ServiceNavigation.fs +++ b/src/Compiler/Service/ServiceNavigation.fs @@ -764,10 +764,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 = @@ -778,7 +805,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 @@ -789,10 +816,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 @@ -803,11 +832,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 @@ -818,14 +852,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 @@ -863,17 +900,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 bce0bc86e9b..357272884a2 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 @@ -3949,10 +3949,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 2ff9555dee6..77ccb5a9b0a 100644 --- a/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj +++ b/tests/FSharp.Compiler.Service.Tests/FSharp.Compiler.Service.Tests.fsproj @@ -52,6 +52,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 d3876ca0028dd7be2f3d0007e5ecadd2a3184730 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:11:30 +0200 Subject: [PATCH 2/6] 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 a94b11cfd88..61faf95c599 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -169,7 +169,7 @@ * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) ### 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)) * FCS: add FSharpCheckFileResults.FileSignature ([PR #20478](https://github.com/dotnet/fsharp/pull/20478)) * Added the `ReraiseInComputationExpressions` language feature (`--langversion:preview`): `reraise ()` in the `with` handler of a computation expression is compiled to a rethrow through `ExceptionDispatchInfo` instead of being rejected with FS0413. ([Suggestion #660](https://github.com/fsharp/fslang-suggestions/issues/660), [RFC FS-1347](https://github.com/fsharp/fslang-design/pull/843), [PR #20405](https://github.com/dotnet/fsharp/pull/20405)) From 3484f1940339784bf091000b4a793c1455fd907c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:12:01 +0200 Subject: [PATCH 3/6] 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 52bbf6f7bc5..f20e66d3e9f 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -8,6 +8,8 @@ * Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. ([PR #20482](https://github.com/dotnet/fsharp/pull/20482)) * Peek Definition on an F# symbol whose definition lives in metadata no longer deadlocks Visual Studio. Peek holds the main thread in `JoinableTaskFactory.Run` without pumping messages while it asks the language service for the definition, and generating the metadata document needs that same thread; Peek now stops at definitions that already have a document, and Go To Definition, which owns the wait it makes, still opens the generated one. ([PR #20503](https://github.com/dotnet/fsharp/pull/20503)) +* 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 5e2ebc8a203b3d09a03a323d9d99493feb0de8c2 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 13 Sep 2026 20:19:34 +0200 Subject: [PATCH 4/6] Link the release note to its PR Co-Authored-By: Claude Opus 5 --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index f20e66d3e9f..c70bd3c9003 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -9,6 +9,7 @@ * Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. ([PR #20482](https://github.com/dotnet/fsharp/pull/20482)) * Peek Definition on an F# symbol whose definition lives in metadata no longer deadlocks Visual Studio. Peek holds the main thread in `JoinableTaskFactory.Run` without pumping messages while it asks the language service for the definition, and generating the metadata document needs that same thread; Peek now stops at definitions that already have a document, and Go To Definition, which owns the wait it makes, still opens the generated one. ([PR #20503](https://github.com/dotnet/fsharp/pull/20503)) * 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)) From fccf80838e77f2409b93c9211c60d2efdf386cf4 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sat, 26 Sep 2026 13:47:34 +0200 Subject: [PATCH 5/6] Keep one release note, in the version in development Linking the note added a copy of it instead of editing the first, and `main` has since opened 11.0.200 for SDK 11.0.200, leaving 11.0.100 shipped. Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.FSharp.Compiler.Service/11.0.100.md | 1 - docs/release-notes/.FSharp.Compiler.Service/11.0.200.md | 1 + docs/release-notes/.VisualStudio/18.vNext.md | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) 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 61faf95c599..f383908e195 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md @@ -169,7 +169,6 @@ * Fix signature generation (`fsc --sig`, `GenerateSignature`, `GetValSignatureText`) dropping the parentheses around a destructured or pattern-annotated tuple parameter when a sibling argument in the same curried group is named, so `(int * float) * z: string` no longer prints as the flat 3-tuple `int * float * z: string`. ([Issue #20397](https://github.com/dotnet/fsharp/issues/20397), [PR #20589](https://github.com/dotnet/fsharp/pull/20589)) ### Added -* `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)) * FCS: add FSharpCheckFileResults.FileSignature ([PR #20478](https://github.com/dotnet/fsharp/pull/20478)) * Added the `ReraiseInComputationExpressions` language feature (`--langversion:preview`): `reraise ()` in the `with` handler of a computation expression is compiled to a rethrow through `ExceptionDispatchInfo` instead of being rejected with FS0413. ([Suggestion #660](https://github.com/fsharp/fslang-suggestions/issues/660), [RFC FS-1347](https://github.com/fsharp/fslang-design/pull/843), [PR #20405](https://github.com/dotnet/fsharp/pull/20405)) diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md index 6212188b65f..7919e2d7e74 100644 --- a/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md +++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.200.md @@ -1,5 +1,6 @@ ### Added +* `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)) * F# Interactive gains a JSON-RPC server mode, `--fsi-server-jsonrpc:`, in which a host submits interactions over a named pipe and receives structured results — diagnostics with positions, escaping exceptions, the values each interaction bound, and the session's own process id — instead of recovering them by looking for a `SERVER-PROMPT>` marker in the output text. Program output continues to flow through the redirected console streams. The pipe admits only the user running the session; `--fsi-server-client-pid:` names the host process whose exit ends the session. `FsiEvaluationSession` exposes both options as `JsonRpcServerPipeName` and `JsonRpcClientProcessId`. The mode is part of the .NET fsi only. ([PR #20396](https://github.com/dotnet/fsharp/pull/20396)) ### Fixed diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index c70bd3c9003..758880d5a06 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -8,7 +8,6 @@ * Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. ([PR #20482](https://github.com/dotnet/fsharp/pull/20482)) * Peek Definition on an F# symbol whose definition lives in metadata no longer deadlocks Visual Studio. Peek holds the main thread in `JoinableTaskFactory.Run` without pumping messages while it asks the language service for the definition, and generating the metadata document needs that same thread; Peek now stops at definitions that already have a document, and Go To Definition, which owns the wait it makes, still opens the generated one. ([PR #20503](https://github.com/dotnet/fsharp/pull/20503)) -* 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)) From 4c712fcb1fa6584c36b0934c50adb59744ce1f49 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Thu, 1 Oct 2026 13:37:44 +0200 Subject: [PATCH 6/6] Pass case sensitivity and matched spans with each Navigate To result dotnet/roslyn#85280 takes the counts in the same constructor as whether the match kept the name's case and where in the name it landed, so the results window can highlight F# names and rank a case-exact match first. The pattern matcher reports the spans in the candidate it was given, container first, so they move to the name's own offsets; the substring fallback for operators and backticked names reports the span it found itself. Co-Authored-By: Claude Opus 5.5 --- docs/release-notes/.VisualStudio/18.vNext.md | 2 +- .../Navigation/NavigateToSearchService.fs | 36 ++++++++++++++++--- .../NavigateToSearchServiceTests.fs | 20 +++++++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index 758880d5a06..430acb9a187 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -8,7 +8,7 @@ * Go To Definition no longer blocks the UI thread with a bare `Task.Wait`: the synchronous `IFSharpGoToDefinitionService` call now waits through the cancellable threaded-wait dialog, and the editor's `TaskCompletionSource` bridges run their continuations on the thread pool instead of inline on whichever thread finished the check, so repeated F12 on a large solution no longer starves semantic classification and other main-thread work. ([PR #20482](https://github.com/dotnet/fsharp/pull/20482)) * Peek Definition on an F# symbol whose definition lives in metadata no longer deadlocks Visual Studio. Peek holds the main thread in `JoinableTaskFactory.Run` without pumping messages while it asks the language service for the definition, and generating the metadata document needs that same thread; Peek now stops at definitions that already have a document, and Go To Definition, which owns the wait it makes, still opens the generated one. ([PR #20503](https://github.com/dotnet/fsharp/pull/20503)) -* 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)) +* 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; the part of the name that matched is highlighted, and a match in the name's own case ranks first. ([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)) diff --git a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs index 56a18f876db..a7a48093ed9 100644 --- a/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs +++ b/vsintegration/src/FSharp.Editor/Navigation/NavigateToSearchService.fs @@ -13,6 +13,7 @@ open System.Globalization open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Navigation open Microsoft.CodeAnalysis.ExternalAccess.FSharp.NavigateTo +open Microsoft.CodeAnalysis.Text open Microsoft.VisualStudio.LanguageServices open Microsoft.VisualStudio.Text.PatternMatching @@ -115,33 +116,56 @@ type internal FSharpNavigateToSearchService | PatternMatchKind.Fuzzy -> FSharpNavigateToMatchKind.Fuzzy | _ -> FSharpNavigateToMatchKind.None + /// Where in the name the search pattern matched, for the results window to highlight. + let nameMatchSpans (m: PatternMatch) = + ImmutableArray.CreateRange [| for span in m.MatchedSpans -> TextSpan(span.Start, span.Length) |] + let createMatcherFor searchPattern = let patternMatcher = patternMatcherFactory.CreatePatternMatcher( searchPattern, PatternMatcherCreationOptions( cultureInfo = CultureInfo.CurrentUICulture, - flags = PatternMatcherCreationFlags.AllowFuzzyMatching, + flags = + (PatternMatcherCreationFlags.AllowFuzzyMatching + ||| PatternMatcherCreationFlags.IncludeMatchedSpans), containerSplitCharacters = [ '.' ] ) ) + // The results window highlights spans of the name, but the matcher reports them in the candidate it was + // given, which carries the container before the name: keep the ones inside the name, at its own offsets. + let spansOfName (offset: int) (m: PatternMatch) = + [| + for span in m.MatchedSpans do + if span.Start >= offset then + Microsoft.VisualStudio.Text.Span(span.Start - offset, span.Length) + |] + |> ImmutableArray.CreateRange + |> m.WithMatchedSpans + fun (item: NavigableItem) -> // PatternMatcher will not match operators and some backtick escaped identifiers. // To handle them, we fall back to simple substring match. let name = item.Name if item.NeedsBackticks then + let matchedSpan start = + ImmutableArray.Create(Microsoft.VisualStudio.Text.Span(start, searchPattern.Length)) + match name.IndexOf(searchPattern, StringComparison.CurrentCultureIgnoreCase) with - | i when i > 0 -> ValueSome(PatternMatch(PatternMatchKind.Substring, false, false)) - | 0 when name.Length = searchPattern.Length -> ValueSome(PatternMatch(PatternMatchKind.Exact, false, false)) - | 0 -> ValueSome(PatternMatch(PatternMatchKind.Prefix, false, false)) + | i when i > 0 -> ValueSome(PatternMatch(PatternMatchKind.Substring, false, false, matchedSpan i)) + | 0 when name.Length = searchPattern.Length -> ValueSome(PatternMatch(PatternMatchKind.Exact, false, false, matchedSpan 0)) + | 0 -> ValueSome(PatternMatch(PatternMatchKind.Prefix, false, false, matchedSpan 0)) | _ -> ValueNone else // full name with dots allows for path matching, e.g. // "f.c.so.elseif" will match "Fantomas.Core.SyntaxOak.ElseIfNode" - patternMatcher.TryMatch $"{item.Container.FullName}.{name}" + let container = item.Container.FullName + + patternMatcher.TryMatch $"{container}.{name}" |> ValueOption.ofNullable + |> ValueOption.map (spansOfName (container.Length + 1)) let processDocument (tryMatch: NavigableItem -> PatternMatch voption) (kinds: IImmutableSet) (document: Document) = cancellableTask { @@ -173,7 +197,9 @@ type internal FSharpNavigateToSearchService additionalInfo, kind, patternMatchKindToNavigateToMatchKind m.Kind, + m.IsCaseSensitive, item.Name, + nameMatchSpans m, FSharpNavigableItem( glyph, ImmutableArray.Create(TaggedText(TextTags.Text, item.Name)), diff --git a/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchServiceTests.fs b/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchServiceTests.fs index ccd9525c2aa..6b3896a53ce 100644 --- a/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchServiceTests.fs +++ b/vsintegration/tests/FSharp.Editor.Tests/NavigateToSearchServiceTests.fs @@ -77,3 +77,23 @@ module HeyHo = 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)) + + /// The text the results window highlights in a result's name. + let matchedTextOf pattern expected = + let result = navigateToSearch pattern |> Seq.find (fun i -> i.Name = expected) + + [ + for span in result.NameMatchSpans -> result.Name.Substring(span.Start, span.Length) + ] + + [] + let ``the highlighted spans are the camel case humps that matched`` () = + Assert.Equal([ "C"; "L"; "N" ], matchedTextOf "CLN" "CamelCaseLongName") + + [] + let ``the highlighted spans are offsets in the name, not in the candidate the matcher saw`` () = + Assert.Equal([ "Delta" ], matchedTextOf "Delta" "Delta") + + [] + let ``a backticked identifier highlights the matched substring`` () = + Assert.Equal([ "few" ], matchedTextOf "few" "a few words")