Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
aed1324
Build multi-project and multi-target Roslyn solutions in FSharp.Edito…
xperiandri Sep 6, 2026
eaa7dfa
Share the find-usages test context between test files
xperiandri Sep 6, 2026
e09a7f2
Host C# projects in the editor test workspace
xperiandri Sep 6, 2026
2279759
Report C# and VB uses of an F# symbol in Find All References
xperiandri Sep 6, 2026
500e435
Test Find All References from an F# declaration into C#
xperiandri Sep 6, 2026
a93b1bd
Add the release note for PR #20463
xperiandri Sep 6, 2026
7f2a9a7
Give a literal the field id Roslyn resolves
xperiandri Sep 6, 2026
f9ff2e4
Search the C# and VB consumers alongside the F# projects
xperiandri Sep 7, 2026
5877a8b
Ask the assembly that declares the symbol, and name an enum case as a…
xperiandri Sep 26, 2026
3bb6c78
Keep one failed item from ending the search, and one name from meanin…
xperiandri Sep 26, 2026
d6f0da5
Take the shared test fixture as #20462 lands it
xperiandri Oct 1, 2026
da742c7
Search the C# consumers of every target of a multi-targeted F# project
xperiandri Oct 11, 2026
b416f97
Carry the cross-language search's items as struct tuples
xperiandri Nov 5, 2026
c27ab5b
Resolve the symbol inside the declaring assembly, so a call through a…
xperiandri Nov 5, 2026
7c8bc43
Compact the cross-language find-references change
xperiandri Nov 5, 2026
b10cde3
Accept the reference assembly of the declaring project when several p…
xperiandri Nov 5, 2026
f902077
Parse the id with Roslyn before walking the assembly, and ignore refe…
xperiandri Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.VisualStudio/18.vNext.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Expand `<inheritdoc/>` in IDE tooltips, completion, and signature help, inheriting XML documentation from base classes, interfaces, overridden members, and constructors. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188))
* Added a **Tools > Options > F# Tools > Compiler** option, **"Use the .NET SDK F# compiler for builds"**, on by default. Visual Studio uses the .NET SDK F# compiler when the project supplies SDK paths, matching `dotnet build`. Otherwise, it uses the bundled .NET Framework compiler. Turn the option off to use the bundled compiler for all projects. Changes are saved on Apply or OK; Cancel discards unsaved changes. Command-line builds are unaffected. ([Issue #20484](https://github.com/dotnet/fsharp/issues/20484), [PR #20485](https://github.com/dotnet/fsharp/pull/20485))
* F# types, modules, members and values now appear in the GitHub Copilot Chat `#` mention picker, and attach their declaration source as context. ([PR #20409](https://github.com/dotnet/fsharp/pull/20409))
* Find All References on an F# symbol also lists its uses in C# and Visual Basic projects that reference the F# project's built assembly. ([PR #20463](https://github.com/dotnet/fsharp/pull/20463))

### Fixed

Expand Down
1 change: 1 addition & 0 deletions eng/Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageVersion Include="Microsoft.Build.Tasks.Core" Version="$(MicrosoftBuildTasksCoreVersion)" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCoreVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(MicrosoftCodeAnalysisCSharpVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(MicrosoftCodeAnalysisCSharpVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.EditorFeatures" Version="$(MicrosoftCodeAnalysisEditorFeaturesTextVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.EditorFeatures.Text" Version="$(MicrosoftCodeAnalysisEditorFeaturesTextVersion)" />
<PackageVersion Include="Microsoft.VisualStudio.LanguageServices.ExternalAccess" Version="$(MicrosoftVisualStudioLanguageServicesExternalAccessVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,41 @@ module internal ProjectFiltering =
let getProjectsReferencingAssembly (assemblyFilePath: string) (solution: Solution) =
let assemblyFileName = Path.GetFileName(assemblyFilePath)

let sameFileName (path: string) =
not (String.IsNullOrEmpty path)
&& String.Equals(Path.GetFileName path, assemblyFileName, StringComparison.OrdinalIgnoreCase)

let samePath (path: string) (other: string) =
not (String.IsNullOrEmpty path)
&& String.Equals(path, other, StringComparison.OrdinalIgnoreCase)

let outputsPerProjectFile =
solution.Projects
|> Seq.filter (fun project -> sameFileName project.OutputFilePath)
|> Seq.groupBy _.FilePath
|> Seq.map (fun (_, targets) ->
targets
|> Seq.collect (fun target -> [ target.OutputFilePath; target.OutputRefFilePath ])
Comment thread
xperiandri marked this conversation as resolved.
Resolved
|> Seq.toArray)
|> Seq.toArray

// Consumers reference copies of the assembly, so its file name identifies it unless several project files produce that name.
let isTheAssembly =
if outputsPerProjectFile.Length < 2 then
sameFileName
else
let declaringOutputs =
outputsPerProjectFile
|> Array.tryFindV (Array.exists (samePath assemblyFilePath))
|> ValueOption.defaultValue [| assemblyFilePath |]

fun path -> declaringOutputs |> Array.exists (samePath path)

solution.Projects
|> Seq.filter (fun project ->
project.MetadataReferences
|> Seq.exists (fun metaRef ->
match metaRef with
| :? PortableExecutableReference as peRef when not (isNull peRef.FilePath) ->
let refFileName = Path.GetFileName(peRef.FilePath)
String.Equals(refFileName, assemblyFileName, StringComparison.OrdinalIgnoreCase)
| :? PortableExecutableReference as peRef -> isTheAssembly peRef.FilePath
| _ -> false))
|> Seq.toList
16 changes: 16 additions & 0 deletions vsintegration/src/FSharp.Editor/LanguageService/Symbols.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ type FSharpSymbol with
| :? FSharpField -> not publicOrInternal
| _ -> false

member this.DocumentationCommentId =
let struct (xmlDocSig, compilesToField) =
match this with
| :? FSharpMemberOrFunctionOrValue as value -> struct (value.XmlDocSig, value.LiteralValue.IsSome)
| :? FSharpEntity as entity -> struct (entity.XmlDocSig, false)
| :? FSharpField as field -> struct (field.XmlDocSig, field.IsLiteral)
| :? FSharpUnionCase as unionCase -> struct (unionCase.XmlDocSig, false)
| _ -> struct ("", false)

match xmlDocSig with
| null
| "" -> ValueNone
// Roslyn names a field F: where FCS says P:.
| docSig when compilesToField && docSig.StartsWith("P:", System.StringComparison.Ordinal) -> ValueSome $"F:{docSig.Substring 2}"
| docSig -> ValueSome docSig

type FSharpSymbolUse with

member this.GetSymbolScope(currentDocument: Document) : SymbolScope option =
Expand Down
107 changes: 95 additions & 12 deletions vsintegration/src/FSharp.Editor/Navigation/FindUsagesService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Collections.Immutable
open System.Composition
open System.Threading.Tasks
Expand All @@ -10,13 +11,23 @@ open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.ExternalAccess.FSharp
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.FindUsages
open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Editor.FindUsages
open Microsoft.CodeAnalysis.FindSymbols

open FSharp.Compiler.EditorServices
open FSharp.Compiler.Text
open CancellableTasks

module FSharpFindUsagesService =

// REVIEW: OnReferenceFoundAsync is throwing inside Roslyn, putting a try/with so find-all refs doesn't fail.
let private reportReference (onReferenceFoundAsync: FSharpSourceReferenceItem -> Task) definitionItem (document: Document) span =
cancellableTask {
try
do! onReferenceFoundAsync (FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(document, span)))
with _ ->
()
}

let onSymbolFound
allReferences
declarationRange
Expand Down Expand Up @@ -44,17 +55,11 @@ module FSharpFindUsagesService =
externalDefinitionItem
else
definitionItems
|> Array.tryFindV (snd >> (=) doc.Project.FilePath)
|> ValueOption.map (fun (definitionItem, _) -> definitionItem)
|> Array.tryFindV (fun struct (_, project: Project) -> project.FilePath = doc.Project.FilePath)
|> ValueOption.map (fun struct (definitionItem, _) -> definitionItem)
|> ValueOption.defaultValue externalDefinitionItem

let referenceItem =
FSharpSourceReferenceItem(definitionItem, FSharpDocumentSpan(doc, fixedSpan))
// REVIEW: OnReferenceFoundAsync is throwing inside Roslyn, putting a try/with so find-all refs doesn't fail.
try
do! onReferenceFoundAsync referenceItem
with _ ->
()
do! reportReference onReferenceFoundAsync definitionItem doc fixedSpan
| _ -> ()
}

Expand Down Expand Up @@ -84,6 +89,72 @@ module FSharpFindUsagesService =
return spans |> Array.choose id
}

let private symbolInAssembly (docId: string) (assembly: IAssemblySymbol) =
let names = docId.Substring(2).Split('(').[0].Split('.')

(Seq.singleton (assembly.GlobalNamespace :> INamespaceOrTypeSymbol), Array.take (names.Length - 1) names)
||> Seq.fold (fun containers name ->
containers
|> Seq.collect _.GetMembers()
|> Seq.chooseV (function
| :? INamespaceOrTypeSymbol as container when container.MetadataName = name -> ValueSome container
| _ -> ValueNone))
|> Seq.collect _.GetMembers()
|> Seq.tryFindV (fun candidate -> DocumentationCommentId.CreateDeclarationId(candidate).Split('~').[0] = docId)
Comment thread
xperiandri marked this conversation as resolved.
Resolved

// Roslyn's parser reads the ids FCS writes, but sees only the references without an alias; walking the assembly covers the aliased ones.
let private symbolOfId (docId: string) (compilation: Compilation) (assembly: IAssemblySymbol) =
DocumentationCommentId.GetSymbolsForDeclarationId(docId, compilation)
|> Seq.tryFindV (fun symbol -> SymbolEqualityComparer.Default.Equals(symbol.ContainingAssembly, assembly))
|> ValueOption.orElseWith (fun () -> symbolInAssembly docId assembly)

let private findRoslynReferences (docId: string) (declaringAssembly: string) (project: Project) =
cancellableTask {
let! cancellationToken = CancellableTask.getCancellationToken ()

match! project.GetCompilationAsync cancellationToken with
| null -> return Seq.empty
| compilation ->
let ofDeclaringAssembly =
compilation.SourceModule.ReferencedAssemblySymbols
|> Seq.tryFindV (fun assembly -> String.Equals(assembly.Name, declaringAssembly, StringComparison.OrdinalIgnoreCase))
|> ValueOption.bind (symbolOfId docId compilation)

match ofDeclaringAssembly with
| ValueNone -> return Seq.empty
| ValueSome symbol ->
let! referencedSymbols =
SymbolFinder.FindReferencesAsync(
symbol,
project.Solution,
ImmutableHashSet.CreateRange project.Documents,
cancellationToken
)

return referencedSymbols |> Seq.collect _.Locations
}

[<Literal>]
let private ConcurrentCompilations = 4

let private findCrossLanguageReferences (docId: string) (definitionItems: struct (FSharpDefinitionItem * Project)[]) =
seq {
for struct (definitionItem, declaringProject) in definitionItems do
for project in ProjectFiltering.getProjectsReferencingAssembly declaringProject.OutputFilePath declaringProject.Solution do
if project.SupportsCompilation && not project.IsFSharp then
struct (definitionItem, declaringProject.AssemblyName, project)
}
|> Seq.distinctBy (fun struct (_, _, project) -> project.Id)
|> Seq.map (fun struct (definitionItem, declaringAssembly, project) ->
findRoslynReferences docId declaringAssembly project
|> CancellableTask.map (Seq.map (fun location -> struct (definitionItem, location))))
|> CancellableTask.whenAllThrottled ConcurrentCompilations
// Each file span once: the target-framework instances of a consumer share their files.
|> CancellableTask.map (fun found ->
found
|> Seq.concat
|> Seq.distinctBy (fun struct (_, location) -> struct (location.Document.FilePath, location.Location.SourceSpan)))

let findReferencedSymbolsAsync
(document: Document, position: int, context: IFSharpFindUsagesContext, allReferences: bool, userOp: string)
: CancellableTask<unit> =
Expand Down Expand Up @@ -139,11 +210,11 @@ module FSharpFindUsagesService =

let definitionItems =
declarationSpans
|> Array.map (fun span -> FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project.FilePath)
|> Array.map (fun span -> struct (FSharpDefinitionItem.Create(tags, displayParts, span), span.Document.Project))

do!
definitionItems
|> Seq.map (fst >> context.OnDefinitionFoundAsync)
|> Seq.map (fun struct (definitionItem, _) -> context.OnDefinitionFoundAsync definitionItem)
|> Task.WhenAll

if isExternal then
Expand All @@ -159,7 +230,19 @@ module FSharpFindUsagesService =
symbol.Ident.idText
context.OnReferenceFoundAsync

do! SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound
let crossLanguageSearch =
match symbolUse.Symbol.DocumentationCommentId with
| ValueSome docId when allReferences && not isExternal && not symbolUse.Symbol.IsInternalToProject ->
findCrossLanguageReferences docId definitionItems
| _ -> CancellableTask.singleton Seq.empty

let! _, found =
CancellableTask.parallelZip
(SymbolHelpers.findSymbolUses symbolUse document checkFileResults onFound)
crossLanguageSearch

for struct (definitionItem, location) in found do
do! reportReference context.OnReferenceFoundAsync definitionItem location.Document location.Location.SourceSpan
}

open FSharpFindUsagesService
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Compile Include="CompletionProviderTests.fs" />
<Compile Include="FindReferencesTests.fs" />
<Compile Include="InlineRenameTests.fs" />
<Compile Include="FindReferencesFromCSharpTests.fs" />
<Compile Include="GoToDefinitionServiceTests.fs" />
<Compile Include="PathMapNavigationTests.fs" />
<Compile Include="HelpContextServiceTests.fs" />
Expand Down Expand Up @@ -100,6 +101,7 @@
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.EditorFeatures" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" />

<PackageReference Include="Microsoft.VisualStudio.LanguageServices.ExternalAccess" />
<PackageReference Include="Microsoft.VisualStudio.Platform.VSEditor" />
Expand Down
Loading
Loading