diff --git a/vsintegration/src/FSharp.Editor/CodeFix/MissingReferenceCodeFixProvider.fs b/vsintegration/src/FSharp.Editor/CodeFix/MissingReferenceCodeFixProvider.fs new file mode 100644 index 00000000000..cd817129040 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/CodeFix/MissingReferenceCodeFixProvider.fs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace rec Microsoft.VisualStudio.FSharp.Editor + +open System +open System.Composition +open System.Collections.Immutable +open System.Threading +open System.Threading.Tasks +open System.IO + +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.CodeFixes +open Microsoft.CodeAnalysis.CodeActions + +type private ReferenceType = +| AddProjectRef of ProjectReference +| AddMetadataRef of MetadataReference + +[] +type internal MissingReferenceCodeFixProvider() = + inherit CodeFixProvider() + + let fixableDiagnosticId = "FS0074" + + let createCodeFix (title: string, context: CodeFixContext, addReference: ReferenceType) = + CodeAction.Create( + title, + (fun (cancellationToken: CancellationToken) -> + async { + let project = context.Document.Project + let solution = project.Solution + + match addReference with + | AddProjectRef projectRef -> + let references = project.AllProjectReferences + let newReferences = references |> Seq.append [projectRef] + return solution.WithProjectReferences(project.Id, newReferences) + + | AddMetadataRef metadataRef -> + let references = project.MetadataReferences + let newReferences = references |> Seq.append [metadataRef] + return solution.WithProjectMetadataReferences(project.Id, newReferences) + } + |> RoslynHelpers.StartAsyncAsTask(cancellationToken) + ), + title) + + override __.FixableDiagnosticIds = Seq.toImmutableArray [fixableDiagnosticId] + + override __.RegisterCodeFixesAsync context : Task = + async { + let solution = context.Document.Project.Solution + + context.Diagnostics + |> Seq.filter (fun x -> x.Id = fixableDiagnosticId) + |> Seq.iter (fun diagnostic -> + let message = diagnostic.GetMessage() + let parts = message.Split([| '\'' |], StringSplitOptions.None) + + match parts with + | [| _; _type; _; assemblyName; _ |] -> + + let exactProjectMatches = + solution.Projects + |> Seq.tryFind (fun project -> + String.Compare(project.AssemblyName, assemblyName, StringComparison.OrdinalIgnoreCase) = 0 + ) + + match exactProjectMatches with + | Some refProject -> + let codefix = + createCodeFix( + String.Format(SR.AddProjectReference.Value, refProject.Name), + context, + AddProjectRef (ProjectReference refProject.Id) + ) + + context.RegisterCodeFix (codefix, ImmutableArray.Create diagnostic) + | None -> + let metadataReferences = + solution.Projects + |> Seq.collect (fun project -> project.MetadataReferences) + |> Seq.tryFind (fun ref -> + let referenceAssemblyName = Path.GetFileNameWithoutExtension(ref.Display) + String.Compare(referenceAssemblyName, assemblyName, StringComparison.OrdinalIgnoreCase) = 0 + ) + + match metadataReferences with + | Some metadataRef -> + let codefix = + createCodeFix( + String.Format(SR.AddAssemblyReference.Value, assemblyName), + context, + AddMetadataRef metadataRef + ) + + context.RegisterCodeFix (codefix, ImmutableArray.Create diagnostic) + | None -> + () + | _ -> () + ) + } + |> RoslynHelpers.StartAsyncUnitAsTask(context.CancellationToken) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index a7528572190..73d9498cc3c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -87,6 +87,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx index a852cb60141..cf3d87957d0 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.resx +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.resx @@ -165,6 +165,12 @@ QuickInfo + + Add an assembly reference to '{0}' + + + Add a project reference to '{0}' + Code Fixes diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 2ca8f2bc093..cf3e632a5c0 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -10,6 +10,7 @@ open System.Collections.Generic open System.ComponentModel.Composition open System.Runtime.InteropServices open System.IO +open System.Diagnostics open Microsoft.FSharp.Compiler.CompileOps open Microsoft.FSharp.Compiler.SourceCodeServices @@ -303,10 +304,9 @@ and let hashSetIgnoreCase x = new HashSet(x, StringComparer.OrdinalIgnoreCase) let updatedFiles = site.SourceFilesOnDisk() |> hashSetIgnoreCase let workspaceFiles = project.GetCurrentDocuments() |> Seq.map(fun file -> file.FilePath) |> hashSetIgnoreCase - - // If syncing project upon some reference changes, we don't have a mechanism to recognize which references have been added/removed. - // Hence, the current solution is to force update current project options. + let mutable updated = forceUpdate + for file in updatedFiles do if not(workspaceFiles.Contains(file)) then projectContext.AddSourceFile(file) @@ -315,6 +315,16 @@ and if not(updatedFiles.Contains(file)) then projectContext.RemoveSourceFile(file) updated <- true + + let updatedRefs = site.AssemblyReferences() |> hashSetIgnoreCase + let workspaceRefs = project.GetCurrentMetadataReferences() |> Seq.map(fun ref -> ref.FilePath) |> hashSetIgnoreCase + + for ref in updatedRefs do + if not(workspaceRefs.Contains(ref)) then + projectContext.AddMetadataReference(ref, MetadataReferenceProperties.Assembly) + for ref in workspaceRefs do + if not(updatedRefs.Contains(ref)) then + projectContext.RemoveMetadataReference(ref) // update the cached options if updated then @@ -332,9 +342,19 @@ and let projectContextFactory = package.ComponentModel.GetService(); let errorReporter = ProjectExternalErrorReporter(projectId, "FS", this.SystemServiceProvider) + let hierarchy = + site.ProjectProvider + |> Option.map (fun p -> p :?> IVsHierarchy) + |> Option.toObj + + // Roslyn is expecting site to be an IVsHierarchy. + // It just so happens that the object that implements IProvideProjectSite is also + // an IVsHierarchy. This assertion is to ensure that the assumption holds true. + Debug.Assert(hierarchy <> null, "About to CreateProjectContext with a non-hierarchy site") + let projectContext = projectContextFactory.CreateProjectContext( - FSharpConstants.FSharpLanguageName, projectDisplayName, projectFileName, projectGuid, siteProvider, null, errorReporter) + FSharpConstants.FSharpLanguageName, projectDisplayName, projectFileName, projectGuid, hierarchy, null, errorReporter) let project = projectContext :?> AbstractProject diff --git a/vsintegration/src/FSharp.Editor/srFSharp.Editor.fs b/vsintegration/src/FSharp.Editor/srFSharp.Editor.fs index bf3a9c90f1d..e9c61206e89 100644 --- a/vsintegration/src/FSharp.Editor/srFSharp.Editor.fs +++ b/vsintegration/src/FSharp.Editor/srFSharp.Editor.fs @@ -31,7 +31,9 @@ module SR = let FSharpDisposablesClassificationType = lazy (GetString "FSharpDisposablesClassificationType") let RemoveUnusedOpens = lazy (GetString "RemoveUnusedOpens") let UnusedOpens = lazy (GetString "UnusedOpens") - + let AddProjectReference = lazy (GetString "AddProjectReference") + let AddAssemblyReference = lazy (GetString "AddAssemblyReference") + //-------------------------------------------------------------------------------------- // Attributes used to mark up editable properties diff --git a/vsintegration/src/FSharp.LanguageService/IProjectSite.fs b/vsintegration/src/FSharp.LanguageService/IProjectSite.fs index 2d9342f823f..3bc1ad88bae 100644 --- a/vsintegration/src/FSharp.LanguageService/IProjectSite.fs +++ b/vsintegration/src/FSharp.LanguageService/IProjectSite.fs @@ -57,3 +57,5 @@ and internal IProjectSite = abstract LoadTime : System.DateTime abstract ProjectProvider : IProvideProjectSite option + + abstract AssemblyReferences : unit -> string [] diff --git a/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs b/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs index 5ae4ff77e30..f01f66d419b 100644 --- a/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs +++ b/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs @@ -34,6 +34,7 @@ type private ProjectSiteOfScriptFile(filename:string, checkOptions : FSharpProje override this.ProjectGuid = "" override this.LoadTime = checkOptions.LoadTime override this.ProjectProvider = None + override this.AssemblyReferences() = [||] interface IHaveCheckOptions with override this.OriginalCheckOptions() = checkOptions @@ -68,6 +69,7 @@ type private ProjectSiteOfSingleFile(sourceFile) = override this.ProjectGuid = "" override this.LoadTime = new DateTime(2000,1,1) // any constant time is fine, orphan files do not interact with reloading based on update time override this.ProjectProvider = None + override this.AssemblyReferences() = [||] /// Information about projects, open files and other active artifacts in visual studio. /// Keeps track of the relationship between IVsTextLines buffers, IFSharpSource objects, IProjectSite objects and FSharpProjectOptions diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs b/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs index fb0f310d539..657c8c0b10e 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/Project.fs @@ -101,6 +101,7 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem member ips.IsIncompleteTypeCheckEnvironment = false member ips.LoadTime = inner.LoadTime member ips.ProjectProvider = inner.ProjectProvider + member ips.AssemblyReferences() = inner.AssemblyReferences() type internal ProjectSiteOptionLifetimeState = | Opening=1 // The project has been opened, but has not yet called Compile() to compute sources/flags @@ -1488,6 +1489,14 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem member this.ProjectGuid = x.GetProjectGuid() member this.LoadTime = creationTime member this.ProjectProvider = Some (x :> IProvideProjectSite) + member this.AssemblyReferences() = + x.GetReferenceContainer().EnumReferences() + |> Seq.choose ( + function + | :? AssemblyReferenceNode as arn -> Some arn.Url + | _ -> None + ) + |> Array.ofSeq } // Snapshot-capture relevent values from "this", and returns an IProjectSite @@ -1504,6 +1513,14 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem let taskReporter = Some(x.TaskReporter) let targetFrameworkMoniker = x.GetTargetFrameworkMoniker() let creationTime = System.DateTime.Now + let assemblyReferences = + x.GetReferenceContainer().EnumReferences() + |> Seq.choose ( + function + | :? AssemblyReferenceNode as arn -> Some arn.Url + | _ -> None + ) + |> Array.ofSeq // This object is thread-safe { new Microsoft.VisualStudio.FSharp.LanguageService.IProjectSite with member ips.SourceFilesOnDisk() = compileItems @@ -1520,6 +1537,7 @@ namespace rec Microsoft.VisualStudio.FSharp.ProjectSystem member this.ProjectGuid = x.GetProjectGuid() member this.LoadTime = creationTime member this.ProjectProvider = Some (x :> IProvideProjectSite) + member this.AssemblyReferences() = assemblyReferences } // let the language service ask us questions diff --git a/vsintegration/tests/Salsa/salsa.fs b/vsintegration/tests/Salsa/salsa.fs index 420bb339a39..cf7f9462dd7 100644 --- a/vsintegration/tests/Salsa/salsa.fs +++ b/vsintegration/tests/Salsa/salsa.fs @@ -297,6 +297,7 @@ module internal Salsa = let projectObj, projectObjFlags = MSBuild.CrackProject(projectfile, configurationFunc(), platformFunc()) projectObj.GetProperty(ProjectFileConstants.ProjectGuid).EvaluatedValue member this.ProjectProvider = None + member this.AssemblyReferences() = [||] // Attempt to treat as MSBuild project. let internal NewMSBuildProjectSite(configurationFunc, platformFunc, msBuildProjectName) =