Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -14,6 +14,7 @@
* Make Alt+F1 (momentary toggle) work for inlay hints. ([PR #19421](https://github.com/dotnet/fsharp/pull/19421))
* Fix doubled F# diagnostics in tooltips. ([Issue #16360](https://github.com/dotnet/fsharp/issues/16360))
* Fix `NotSupportedException` in the memory-mapped-file optimization when copying `ReadOnlyMemory` into `MemoryMappedFileViewStream`. ([Issue #20263](https://github.com/dotnet/fsharp/issues/20263))
* Go To Definition into an F# project built with a path map (`DeterministicSourcePaths` or `PathMap`) opens its source instead of a generated signature: the IDE no longer applies `--pathmap` to the project options it checks with. ([PR #20470](https://github.com/dotnet/fsharp/pull/20470))

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,10 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) =
[|
// Clear any references from CompilationOptions.
// We get the references from Project.ProjectReferences/Project.MetadataReferences.
// A path map belongs to the build output: applied here it rewrites the file name of
// every range imported from a referenced project, and navigation finds no document.
for x in projectSite.CompilationOptions do
if not (x.Contains("-r:")) then
if not (x.Contains("-r:") || x.StartsWith("--pathmap:", StringComparison.Ordinal)) then
x

for x in project.MetadataReferences.OfType<PortableExecutableReference>() do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<Compile Include="CompletionProviderTests.fs" />
<Compile Include="FindReferencesTests.fs" />
<Compile Include="GoToDefinitionServiceTests.fs" />
<Compile Include="PathMapNavigationTests.fs" />
<Compile Include="HelpContextServiceTests.fs" />
<Compile Include="QuickInfoTests.fs" />
<Compile Include="TaskListServiceTests.fs" />
Expand Down
146 changes: 140 additions & 6 deletions vsintegration/tests/FSharp.Editor.Tests/Helpers/RoslynHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,14 @@ type TestHostServices() =
override this.CreateWorkspaceServices(workspace) =
new TestHostWorkspaceServices(this, workspace)

/// One Roslyn project instance of a multi-targeted F# project: its extra defines and the
/// synthetic files left out of it, as VS does per target framework.
type TargetInstance =
{
Defines: string list
ExcludedFileIds: string list
}

[<AbstractClass; Sealed>]
type RoslynTestHelpers private () =

Expand Down Expand Up @@ -258,6 +266,33 @@ type RoslynTestHelpers private () =
filePath = filePath
)

static member private ProjectInfoFor
(id, name, filePath, outputFilePath, documents, projectReferences: ProjectReference list, metadataReferences: MetadataReference seq)
=
ProjectInfo.Create(
id,
VersionStamp.Create(DateTime.UtcNow),
name,
name,
LanguageNames.FSharp,
filePath = filePath,
outputFilePath = outputFilePath,
documents = documents,
projectReferences = projectReferences,
metadataReferences = metadataReferences
)

static member private MetadataReferencesOf(options: FSharpProjectOptions, excludedPaths: string seq) =
let excluded = HashSet(excludedPaths, StringComparer.OrdinalIgnoreCase)

options.OtherOptions
|> Seq.filter (fun x -> x.StartsWith("-r:", StringComparison.Ordinal))
|> Seq.map _.Substring(3)
|> Seq.filter (excluded.Contains >> not)
|> Seq.map MetadataReference.CreateFromFile
|> Seq.cast<MetadataReference>
|> Seq.toList

static member SetProjectOptions projId (solution: Solution) (options: FSharpProjectOptions) =
solution.Workspace.Services
.GetService<IFSharpWorkspaceService>()
Expand Down Expand Up @@ -331,19 +366,118 @@ type RoslynTestHelpers private () =

let options = syntheticProject.GetProjectOptions checker

let metadataReferences =
options.OtherOptions
|> Seq.filter (fun x -> x.StartsWith("-r:"))
|> Seq.map (fun x -> x.Substring(3) |> MetadataReference.CreateFromFile :> MetadataReference)

let projInfo = projInfo.WithMetadataReferences metadataReferences
let projInfo =
projInfo.WithMetadataReferences(RoslynTestHelpers.MetadataReferencesOf(options, []))

let solution = RoslynTestHelpers.CreateSolution [ projInfo ]

options |> RoslynTestHelpers.SetProjectOptions projId solution

solution, checker

/// One Roslyn project per synthetic project, wired with project references the way VS wires
/// project-to-project references, so the options manager builds in-memory F# references.
static member CreateMultiProjectSolution(syntheticProject: SyntheticProject) =
let checker = syntheticProject.SaveAndCheck()

let projects =
syntheticProject.GetAllProjects()
|> List.distinctBy _.Name
|> List.map (fun project -> project, ProjectId.CreateNewId())

let projectIds = dict [ for project, id in projects -> project.Name, id ]

let projectInfos =
[
for project, id in projects do
let options = project.GetProjectOptions checker

RoslynTestHelpers.ProjectInfoFor(
id,
project.Name,
project.ProjectFileName,
project.OutputFilename,
[
for path in project.SourceFilePaths -> RoslynTestHelpers.CreateDocumentInfo id path (File.ReadAllText path)
],
[
for dependency in project.DependsOn -> ProjectReference projectIds[dependency.Name]
],
RoslynTestHelpers.MetadataReferencesOf(options, project.DependsOn |> List.map _.OutputFilename)
)
]

let solution = RoslynTestHelpers.CreateSolution projectInfos

for project, id in projects do
project.GetProjectOptions checker
|> RoslynTestHelpers.SetProjectOptions id solution

solution, checker

/// One Roslyn project per target instance, all sharing the .fsproj path and the document file
/// paths, like the per-target-framework projects VS creates for a multi-targeted project.
static member CreateMultiTargetSolution(syntheticProject: SyntheticProject, instances: TargetInstance list) =
assert (syntheticProject.DependsOn = [])

let checker = syntheticProject.SaveAndCheck()
let options = syntheticProject.GetProjectOptions checker
let metadataReferences = RoslynTestHelpers.MetadataReferencesOf(options, [])

let instances =
[
for instance in instances ->
let excludedPaths =
HashSet(
[
for fileId in instance.ExcludedFileIds do
syntheticProject.GetFilePath fileId

if (syntheticProject.Find fileId).HasSignatureFile then
syntheticProject.GetSignatureFilePath fileId
],
StringComparer.OrdinalIgnoreCase
)

let sourceFiles =
syntheticProject.SourceFilePaths |> List.filter (excludedPaths.Contains >> not)

let id = ProjectId.CreateNewId()

let projectInfo =
RoslynTestHelpers.ProjectInfoFor(
id,
syntheticProject.Name,
syntheticProject.ProjectFileName,
syntheticProject.OutputFilename,
[
for path in sourceFiles -> RoslynTestHelpers.CreateDocumentInfo id path (File.ReadAllText path)
],
[],
metadataReferences
)

let instanceOptions =
{ options with
SourceFiles = List.toArray sourceFiles
OtherOptions =
[|
yield! options.OtherOptions
for define in instance.Defines -> $"--define:{define}"
|]
}

id, projectInfo, instanceOptions
]

let solution =
RoslynTestHelpers.CreateSolution [ for _, projectInfo, _ in instances -> projectInfo ]

for id, _, instanceOptions in instances do
RoslynTestHelpers.SetProjectOptions id solution instanceOptions

solution, [ for id, _, _ in instances -> id ]

static member GetFsDocument(code, ?customProjectOption: string, ?customEditorOptions) =
let customProjectOptions =
customProjectOption
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

/// A library whose build maps its source paths, as DeterministicSourcePaths does: the symbols another
/// project imports from it must still name the files of the workspace.
module FSharp.Editor.Tests.PathMapNavigationTests

open System
open System.IO
open System.Threading
open Xunit
open Microsoft.VisualStudio.FSharp.Editor
open Microsoft.VisualStudio.FSharp.Editor.CancellableTasks
open FSharp.Editor.Tests.Helpers
open FSharp.Test.ProjectGeneration

/// As Directory.Build.props would set it: the same map on every project of the solution.
let private pathMap (project: SyntheticProject) =
[ $"--pathmap:{Path.GetDirectoryName project.ProjectDir}=.\\" ]

let private library =
let library = SyntheticProject.Create("Library", sourceFile "Library" [])

{ library with
OtherOptions = pathMap library
}

let private app =
let app = SyntheticProject.Create("App", sourceFile "App" [ "Library" ])

{ app with
DependsOn = [ library ]
OtherOptions = pathMap app
}

let private solution, _ = RoslynTestHelpers.CreateMultiProjectSolution app

let private documentOf (project: SyntheticProject) fileId =
solution.GetDocumentIdsWithFilePath(project.GetFilePath fileId)
|> Seq.exactlyOne
|> solution.GetDocument

[<Fact>]
let ``the path map of a project is not applied in the IDE`` () =
let _, _, _, options =
(documentOf library "Library").GetFSharpCompilationOptionsAsync "test"
|> CancellableTask.runSynchronouslyWithoutCancellation

Assert.DoesNotContain(options.OtherOptions, fun option -> option.StartsWith("--pathmap:", StringComparison.Ordinal))

[<Fact>]
let ``goto definition into a project built with a path map reaches its source`` () =
let appDocument = documentOf app "App"
let text = appDocument.GetTextAsync(CancellationToken.None).Result.ToString()

let position =
text.IndexOf("ModuleLibrary.f", StringComparison.Ordinal)
+ "ModuleLibrary.f".Length
- 1

let result =
GoToDefinition(FSharpMetadataAsSourceService()).FindDefinitionAtPosition(appDocument, position)
|> CancellableTask.runSynchronouslyWithoutCancellation

match result with
| ValueSome(FSharpGoToDefinitionResult.NavigableItem item, _) -> Assert.Equal(library.GetFilePath "Library", item.Document.FilePath)
| result -> failwith $"expected a navigable item, got %A{result}"
Loading