diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index aff4b4c25d..da325f32a9 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -46,6 +46,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 3700d639e1..e30c178f5b 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -100,12 +100,14 @@ type private FSharpProjectOptionsReactor (checker: FSharpChecker) = let legacyProjectSites = ConcurrentDictionary() let cache = ConcurrentDictionary() - let singleFileCache = ConcurrentDictionary() + let singleFileCache = ConcurrentDictionary() // This is used to not constantly emit the same compilation. let weakPEReferences = ConditionalWeakTable() let lastSuccessfulCompilations = ConcurrentDictionary() + let scriptUpdatedEvent = Event() + let createPEReference (referencedProject: Project) (comp: Compilation) = let projectId = referencedProject.Id @@ -185,6 +187,7 @@ type private FSharpProjectOptionsReactor (checker: FSharpChecker) = let projectOptions = if isScriptFile document.FilePath then + scriptUpdatedEvent.Trigger(scriptProjectOptions) scriptProjectOptions else { @@ -203,12 +206,12 @@ type private FSharpProjectOptionsReactor (checker: FSharpChecker) = let parsingOptions, _ = checker.GetParsingOptionsFromProjectOptions(projectOptions) - singleFileCache.[document.Id] <- (fileStamp, parsingOptions, projectOptions) + singleFileCache.[document.Id] <- (document.Project, fileStamp, parsingOptions, projectOptions) return Some(parsingOptions, projectOptions) - | true, (fileStamp2, parsingOptions, projectOptions) -> - if fileStamp <> fileStamp2 then + | true, (oldProject, oldFileStamp, parsingOptions, projectOptions) -> + if fileStamp <> oldFileStamp || isProjectInvalidated document.Project oldProject ct then singleFileCache.TryRemove(document.Id) |> ignore return! tryComputeOptionsBySingleScriptOrFile document ct userOpName else @@ -399,7 +402,7 @@ type private FSharpProjectOptionsReactor (checker: FSharpChecker) = legacyProjectSites.TryRemove(projectId) |> ignore | FSharpProjectOptionsMessage.ClearSingleFileOptionsCache(documentId) -> match singleFileCache.TryRemove(documentId) with - | true, (_, _, projectOptions) -> + | true, (_, _, _, projectOptions) -> lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore checker.ClearCache([projectOptions]) | _ -> @@ -438,6 +441,8 @@ type private FSharpProjectOptionsReactor (checker: FSharpChecker) = singleFileCache.Clear() lastSuccessfulCompilations.Clear() + member _.ScriptUpdated = scriptUpdatedEvent.Publish + interface IDisposable with member _.Dispose() = cancellationTokenSource.Cancel() @@ -468,6 +473,8 @@ type internal FSharpProjectOptionsManager reactor.ClearSingleFileOptionsCache(doc.Id) ) + member _.ScriptUpdated = reactor.ScriptUpdated + member _.SetLegacyProjectSite (projectId, projectSite) = reactor.SetLegacyProjectSite (projectId, projectSite) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/IFSharpWorkspaceService.fs b/vsintegration/src/FSharp.Editor/LanguageService/IFSharpWorkspaceService.fs new file mode 100644 index 0000000000..db1794308b --- /dev/null +++ b/vsintegration/src/FSharp.Editor/LanguageService/IFSharpWorkspaceService.fs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open FSharp.Compiler.CodeAnalysis +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.CodeAnalysis.Host + +// Used to expose FSharpChecker/ProjectInfo manager to diagnostic providers +// Diagnostic providers can be executed in environment that does not use MEF so they can rely only +// on services exposed by the workspace +type internal IFSharpWorkspaceService = + inherit IWorkspaceService + abstract Checker: FSharpChecker + abstract FSharpProjectOptionsManager: FSharpProjectOptionsManager \ No newline at end of file diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 6a921b01cb..19b9fe30a3 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -29,14 +29,6 @@ open Microsoft.CodeAnalysis.Host.Mef #nowarn "9" // NativePtr.toNativeInt -// Used to expose FSharpChecker/ProjectInfo manager to diagnostic providers -// Diagnostic providers can be executed in environment that does not use MEF so they can rely only -// on services exposed by the workspace -type internal IFSharpWorkspaceService = - inherit IWorkspaceService - abstract Checker: FSharpChecker - abstract FSharpProjectOptionsManager: FSharpProjectOptionsManager - type internal RoamingProfileStorageLocation(keyName: string) = inherit OptionStorageLocation() @@ -269,10 +261,11 @@ type internal FSharpPackage() as this = let miscFilesWorkspace = this.ComponentModel.GetService() let _singleFileWorkspaceMap = new SingleFileWorkspaceMap( - workspace, - miscFilesWorkspace, - optionsManager, - projectContextFactory, + FSharpMiscellaneousFileService( + workspace, + miscFilesWorkspace, + FSharpWorkspaceProjectContextFactory(projectContextFactory) + ), rdt) let _legacyProjectWorkspaceMap = new LegacyProjectWorkspaceMap(solution, optionsManager, projectContextFactory) () diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs index 91106217e9..637c6dd92d 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs @@ -4,6 +4,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor open System open System.Collections.Concurrent +open System.Collections.Immutable open Microsoft.CodeAnalysis open Microsoft.VisualStudio open Microsoft.VisualStudio.FSharp.Editor @@ -11,60 +12,235 @@ open Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem open Microsoft.VisualStudio.LanguageServices.ProjectSystem open Microsoft.VisualStudio.Shell.Interop open Microsoft.VisualStudio.LanguageServices +open FSharp.Compiler.CodeAnalysis -[] -type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, - miscFilesWorkspace: MiscellaneousFilesWorkspace, - optionsManager: FSharpProjectOptionsManager, - projectContextFactory: IWorkspaceProjectContextFactory, - rdt: IVsRunningDocumentTable) as this = +type internal IFSharpWorkspaceProjectContext = + inherit IDisposable - let files = ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + abstract Id : ProjectId + + abstract FilePath : string + + abstract ProjectReferenceCount : int + + abstract HasProjectReference : filePath: string -> bool + + abstract SetProjectReferences : IFSharpWorkspaceProjectContext seq -> unit + +type internal IFSharpWorkspaceProjectContextFactory = + + abstract CreateProjectContext : filePath: string -> IFSharpWorkspaceProjectContext + +type internal FSharpWorkspaceProjectContext(vsProjectContext: IWorkspaceProjectContext) = + + let mutable refs = ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase) + + member private _.VisualStudioProjectContext = vsProjectContext + + member private _.AddProjectReference(builder: ImmutableDictionary<_, _>.Builder, projectContext: IFSharpWorkspaceProjectContext) = + match projectContext with + | :? FSharpWorkspaceProjectContext as fsProjectContext -> + vsProjectContext.AddProjectReference(fsProjectContext.VisualStudioProjectContext, MetadataReferenceProperties.Assembly) + builder.Add(projectContext.FilePath, projectContext) + | _ -> + () - let createSourceCodeKind (filePath: string) = + member private _.RemoveProjectReference(projectContext: IFSharpWorkspaceProjectContext) = + match projectContext with + | :? FSharpWorkspaceProjectContext as fsProjectContext -> + vsProjectContext.RemoveProjectReference(fsProjectContext.VisualStudioProjectContext) + | _ -> + () + + interface IFSharpWorkspaceProjectContext with + + member _.Id = vsProjectContext.Id + + member _.FilePath = vsProjectContext.ProjectFilePath + + member _.ProjectReferenceCount = refs.Count + + member _.HasProjectReference(filePath) = refs.ContainsKey(filePath) + + member this.SetProjectReferences(projRefs) = + let builder = ImmutableDictionary.CreateBuilder() + + refs.Values + |> Seq.iter (fun x -> + this.RemoveProjectReference(x) + ) + + projRefs + |> Seq.iter (fun x -> + this.AddProjectReference(builder, x) + ) + + refs <- builder.ToImmutable() + + member _.Dispose() = + vsProjectContext.Dispose() + +type internal FSharpWorkspaceProjectContextFactory(projectContextFactory: IWorkspaceProjectContextFactory) = + + static let createSourceCodeKind (filePath: string) = if isScriptFile filePath then SourceCodeKind.Script else SourceCodeKind.Regular - let createProjectContext filePath = - let projectContext = projectContextFactory.CreateProjectContext(FSharpConstants.FSharpLanguageName, filePath, filePath, Guid.NewGuid(), null, null) - projectContext.DisplayName <- FSharpConstants.FSharpMiscellaneousFilesName - projectContext.AddSourceFile(filePath, sourceCodeKind = createSourceCodeKind filePath) - projectContext + interface IFSharpWorkspaceProjectContextFactory with + + member _.CreateProjectContext filePath = + let projectContext = projectContextFactory.CreateProjectContext(FSharpConstants.FSharpLanguageName, filePath, filePath, Guid.NewGuid(), null, null) + projectContext.DisplayName <- FSharpConstants.FSharpMiscellaneousFilesName + projectContext.AddSourceFile(filePath, sourceCodeKind = createSourceCodeKind filePath) + new FSharpWorkspaceProjectContext(projectContext) :> IFSharpWorkspaceProjectContext + +type internal FSharpMiscellaneousFileService(workspace: Workspace, + miscFilesWorkspace: Workspace, + projectContextFactory: IFSharpWorkspaceProjectContextFactory) = + + // We have a lock because the `ScriptUpdated` event may happen concurrently when a document opens or closes. + let gate = obj() + let files = ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) + let optionsManager = workspace.Services.GetRequiredService().FSharpProjectOptionsManager + + static let mustUpdateProject (refSourceFiles: string []) (projectContext: IFSharpWorkspaceProjectContext) = + refSourceFiles.Length <> projectContext.ProjectReferenceCount || + ( + refSourceFiles + |> Seq.forall projectContext.HasProjectReference + |> not + ) + + let tryRemove (document: Document) = + let projIds = document.Project.Solution.GetDependentProjectIds(document.Project.Id) + if projIds.Count = 0 then + optionsManager.ClearSingleFileOptionsCache(document.Id) + + match files.TryRemove(document.FilePath) with + | true, projectContext -> + (projectContext :> IDisposable).Dispose() + | _ -> + () do + optionsManager.ScriptUpdated.Add(fun scriptProjectOptions -> + if scriptProjectOptions.SourceFiles.Length > 0 then + // The last file in the project options is the main script file. + let filePath = scriptProjectOptions.SourceFiles.[scriptProjectOptions.SourceFiles.Length - 1] + let refSourceFiles = scriptProjectOptions.SourceFiles |> Array.take (scriptProjectOptions.SourceFiles.Length - 1) + + match files.TryGetValue(filePath) with + | true, (projectContext: IFSharpWorkspaceProjectContext) -> + if mustUpdateProject refSourceFiles projectContext then + lock gate (fun () -> + let newProjRefs = + refSourceFiles + |> Array.map (fun filePath -> + match files.TryGetValue(filePath) with + | true, refProjectContext -> refProjectContext + | _ -> + let refProjectContext = projectContextFactory.CreateProjectContext(filePath) + files.[filePath] <- refProjectContext + refProjectContext + ) + + projectContext.SetProjectReferences(newProjRefs) + ) + | _ -> + () + ) + miscFilesWorkspace.DocumentOpened.Add(fun args -> let document = args.Document - if document.Project.Language = FSharpConstants.FSharpLanguageName && workspace.CurrentSolution.GetDocumentIdsWithFilePath(document.FilePath).Length = 0 then - files.[document.FilePath] <- createProjectContext document.FilePath + // If the file does not exist in the current solution, then we can create new project in the VisualStudioWorkspace that represents + // a F# miscellaneous project, which could be a script or not. + if document.Project.IsFSharp && workspace.CurrentSolution.GetDocumentIdsWithFilePath(document.FilePath).Length = 0 then + let filePath = document.FilePath + lock gate (fun () -> + if files.ContainsKey(filePath) |> not then + files.[filePath] <- projectContextFactory.CreateProjectContext(filePath) + ) ) workspace.DocumentOpened.Add(fun args -> let document = args.Document - if document.Project.Language = FSharpConstants.FSharpLanguageName && - not document.Project.IsFSharpMiscellaneousOrMetadata then - match files.TryRemove(document.FilePath) with - | true, projectContext -> - optionsManager.ClearSingleFileOptionsCache(document.Id) - projectContext.Dispose() - | _ -> () + if not document.Project.IsFSharpMiscellaneousOrMetadata then + if files.ContainsKey(document.FilePath) then + lock gate (fun () -> + tryRemove document + ) ) workspace.DocumentClosed.Add(fun args -> let document = args.Document - if document.Project.Language = FSharpConstants.FSharpLanguageName && - document.Project.IsFSharpMiscellaneousOrMetadata then - match files.TryRemove(document.FilePath) with - | true, projectContext -> + if document.Project.IsFSharpMiscellaneousOrMetadata then + lock gate (fun () -> + tryRemove document + ) + ) + + workspace.WorkspaceChanged.Add(fun args -> + match args.Kind with + | WorkspaceChangeKind.ProjectRemoved -> + let proj = args.OldSolution.GetProject(args.ProjectId) + if proj.IsFSharpMiscellaneousOrMetadata then + let projRefs = + proj.GetAllProjectsThisProjectDependsOn() + |> Array.ofSeq + + if projRefs.Length > 0 then + lock gate (fun () -> + projRefs + |> Array.iter (fun proj -> + let proj = args.NewSolution.GetProject(proj.Id) + match proj with + | null -> () + | _ -> + if proj.IsFSharpMiscellaneousOrMetadata then + match proj.Documents |> Seq.tryExactlyOne with + | Some doc when not (workspace.IsDocumentOpen(doc.Id)) -> + tryRemove doc + | _ -> + () + ) + ) + | _ -> + () + ) + + member _.Workspace = workspace + + member _.ProjectContextFactory = projectContextFactory + + member _.ContainsFile filePath = files.ContainsKey(filePath) + + member _.RenameFile(filePath, newFilePath) = + match files.TryRemove(filePath) with + | true, projectContext -> + let project = workspace.CurrentSolution.GetProject(projectContext.Id) + if project <> null then + let documentOpt = + project.Documents + |> Seq.tryFind (fun x -> String.Equals(x.FilePath, filePath, StringComparison.OrdinalIgnoreCase)) + match documentOpt with + | None -> () + | Some(document) -> optionsManager.ClearSingleFileOptionsCache(document.Id) projectContext.Dispose() - | _ -> () - ) + files.[newFilePath] <- projectContextFactory.CreateProjectContext(newFilePath) + else + projectContext.Dispose() // fallback, shouldn't happen, but in case it does let's dispose of the project context so we don't leak + | _ -> () + +[] +type internal SingleFileWorkspaceMap(miscFileService: FSharpMiscellaneousFileService, + rdt: IVsRunningDocumentTable) as this = - do - rdt.AdviseRunningDocTableEvents(this) |> ignore + do + rdt.AdviseRunningDocTableEvents(this) |> ignore interface IVsRunningDocTableEvents with @@ -86,23 +262,8 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, member _.OnAfterAttributeChangeEx(_, grfAttribs, _, _, pszMkDocumentOld, _, _, pszMkDocumentNew) = // Handles renaming of a misc file - if (grfAttribs &&& (uint32 __VSRDTATTRIB.RDTA_MkDocument)) <> 0u && files.ContainsKey(pszMkDocumentOld) then - match files.TryRemove(pszMkDocumentOld) with - | true, projectContext -> - let project = workspace.CurrentSolution.GetProject(projectContext.Id) - if project <> null then - let documentOpt = - project.Documents - |> Seq.tryFind (fun x -> String.Equals(x.FilePath, pszMkDocumentOld, StringComparison.OrdinalIgnoreCase)) - match documentOpt with - | None -> () - | Some(document) -> - optionsManager.ClearSingleFileOptionsCache(document.Id) - projectContext.Dispose() - files.[pszMkDocumentNew] <- createProjectContext pszMkDocumentNew - else - projectContext.Dispose() // fallback, shouldn't happen, but in case it does let's dispose of the project context so we don't leak - | _ -> () + if (grfAttribs &&& (uint32 __VSRDTATTRIB.RDTA_MkDocument)) <> 0u && miscFileService.ContainsFile(pszMkDocumentOld) then + miscFileService.RenameFile(pszMkDocumentOld, pszMkDocumentNew) VSConstants.S_OK member _.OnAfterDocumentWindowHide(_, _) = VSConstants.E_NOTIMPL diff --git a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs index 7481753ec3..4387329518 100644 --- a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs +++ b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs @@ -2,6 +2,7 @@ open System open System.IO +open System.Text open System.Reflection open System.Linq open System.Composition.Hosting @@ -200,6 +201,31 @@ type TestHostServices() = [] type RoslynTestHelpers private () = + static member CreateProjectInfoWithSingleDocument(projName, docFilePath) = + let isScript = String.Equals(Path.GetExtension(docFilePath), ".fsx", StringComparison.OrdinalIgnoreCase) + + let projId = ProjectId.CreateNewId() + let docId = DocumentId.CreateNewId(projId) + + let docInfo = + DocumentInfo.Create( + docId, + docFilePath, + filePath=docFilePath, + loader = new FileTextLoader(docFilePath, Encoding.Default), + sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) + + let projFilePath = "C:\\test.fsproj" + ProjectInfo.Create( + projId, + VersionStamp.Create(DateTime.UtcNow), + projName, + "test.dll", + LanguageNames.FSharp, + documents = [docInfo], + filePath = projFilePath + ) + static member CreateDocument (filePath, text: SourceText, ?options: FSharp.Compiler.CodeAnalysis.FSharpProjectOptions) = let isScript = String.Equals(Path.GetExtension(filePath), ".fsx", StringComparison.OrdinalIgnoreCase) diff --git a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj index baa2c21ae9..d4a67482ad 100644 --- a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj +++ b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj @@ -37,6 +37,7 @@ + diff --git a/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs new file mode 100644 index 0000000000..e9c2bfe5fe --- /dev/null +++ b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs @@ -0,0 +1,317 @@ +namespace VisualFSharp.UnitTests + +open System +open System.IO +open System.Reflection +open System.Linq +open System.Composition.Hosting +open System.Collections.Generic +open System.Collections.Immutable +open System.Threading +open Microsoft.VisualStudio.Composition +open Microsoft.CodeAnalysis +open Microsoft.CodeAnalysis.Host +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.FSharp.Editor +open Microsoft.CodeAnalysis.Host.Mef +open Microsoft.VisualStudio.LanguageServices +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.FSharp.Editor.Tests.Roslyn +open NUnit.Framework + +[] +module WorkspaceTests = + + let createOnDiskScript src = + let tmpFilePath = Path.GetTempFileName() + let tmpRealFilePath = Path.ChangeExtension(tmpFilePath, ".fsx") + try File.Delete(tmpFilePath) with | _ -> () + File.WriteAllText(tmpRealFilePath, src) + tmpRealFilePath + + let createWorkspace() = + new AdhocWorkspace(TestHostServices()) + + let createMiscFileWorkspace() = + createWorkspace() + + let openDocument (workspace: Workspace) (docId: DocumentId) = + use waitHandle = new ManualResetEventSlim(false) + use _sub = workspace.DocumentOpened.Subscribe(fun _ -> + waitHandle.Set() + ) + workspace.OpenDocument(docId) + waitHandle.Wait() + + let getDocument (workspace: Workspace) filePath = + let solution = workspace.CurrentSolution + solution.GetDocumentIdsWithFilePath(filePath) + |> Seq.exactlyOne + |> solution.GetDocument + + let addProject (workspace: Workspace) projInfo = + if not (workspace.TryApplyChanges(workspace.CurrentSolution.AddProject(projInfo))) then + failwith "Unable to apply workspace changes." + + let removeProject (workspace: Workspace) projId = + if not (workspace.TryApplyChanges(workspace.CurrentSolution.RemoveProject(projId))) then + failwith "Unable to apply workspace changes." + + let assertEmptyDocumentDiagnostics (doc: Document) = + let parseResults, checkResults = doc.GetFSharpParseAndCheckResultsAsync("assertEmptyDocumentDiagnostics") |> Async.RunSynchronously + + Assert.IsEmpty(parseResults.Diagnostics) + Assert.IsEmpty(checkResults.Diagnostics) + + let assertHasDocumentDiagnostics (doc: Document) = + let parseResults, checkResults = doc.GetFSharpParseAndCheckResultsAsync("assertHasDocumentDiagnostics") |> Async.RunSynchronously + + Assert.IsEmpty(parseResults.Diagnostics) + Assert.IsNotEmpty(checkResults.Diagnostics) + + type TestFSharpWorkspaceProjectContext(mainProj: Project) = + + let mutable mainProj = mainProj + + interface IFSharpWorkspaceProjectContext with + + member this.Dispose(): unit = () + + member this.FilePath: string = mainProj.FilePath + + member this.HasProjectReference(filePath: string): bool = + mainProj.ProjectReferences + |> Seq.exists (fun x -> + let projRef = mainProj.Solution.GetProject(x.ProjectId) + if projRef <> null then + String.Equals(filePath, projRef.FilePath, StringComparison.OrdinalIgnoreCase) + else + false + ) + + member this.Id: ProjectId = mainProj.Id + + member this.ProjectReferenceCount: int = mainProj.ProjectReferences.Count() + + member this.SetProjectReferences(projRefs: seq): unit = + let currentProj = mainProj + let mutable solution = currentProj.Solution + + mainProj.ProjectReferences + |> Seq.iter (fun projRef -> + solution <- solution.RemoveProjectReference(currentProj.Id, projRef) + ) + + projRefs + |> Seq.iter (fun projRef -> + solution <- + solution.AddProjectReference( + currentProj.Id, + ProjectReference(projRef.Id) + ) + ) + + not (solution.Workspace.TryApplyChanges(solution)) |> ignore + + mainProj <- solution.GetProject(currentProj.Id) + + type TestFSharpWorkspaceProjectContextFactory(workspace: Workspace, miscFilesWorkspace: Workspace) = + + interface IFSharpWorkspaceProjectContextFactory with + member this.CreateProjectContext(filePath: string): IFSharpWorkspaceProjectContext = + match miscFilesWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath) |> Seq.tryExactlyOne with + | Some docId -> + let doc = miscFilesWorkspace.CurrentSolution.GetDocument(docId) + removeProject miscFilesWorkspace doc.Project.Id + | _ -> + () + + let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(FSharpConstants.FSharpMiscellaneousFilesName, filePath) + addProject workspace projInfo + + let proj = workspace.CurrentSolution.GetProject(projInfo.Id) + new TestFSharpWorkspaceProjectContext(proj) :> IFSharpWorkspaceProjectContext + + [] + let ``Script file opened in misc files workspace will get transferred to normal workspace``() = + let workspace = createWorkspace() + let miscFilesWorkspace = createMiscFileWorkspace() + let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) + + let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) + + let filePath = + createOnDiskScript + """ +module Script1 + +let x = 1 + """ + + try + let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath, filePath) + addProject miscFilesWorkspace projInfo + + let doc = getDocument miscFilesWorkspace filePath + + Assert.IsFalse(miscFilesWorkspace.IsDocumentOpen(doc.Id)) + Assert.AreEqual(0, workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Length) + + openDocument miscFilesWorkspace doc.Id + // Although we opened the document, this is false as it has been transferred to the other workspace. + Assert.IsFalse(miscFilesWorkspace.IsDocumentOpen(doc.Id)) + + Assert.AreEqual(0, miscFilesWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Length) + Assert.AreEqual(1, workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Length) + + let doc = getDocument workspace filePath + + Assert.IsFalse(workspace.IsDocumentOpen(doc.Id)) + + assertEmptyDocumentDiagnostics doc + + finally + try File.Delete(filePath) with | _ -> () + + [] + let ``Script file referencing another script should have no diagnostics``() = + let workspace = createWorkspace() + let miscFilesWorkspace = createMiscFileWorkspace() + let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) + + let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) + + let filePath1 = + createOnDiskScript + """ +module Script1 + +let x = 1 + """ + + let filePath2 = + createOnDiskScript + $""" +module Script2 +#load "{ Path.GetFileName(filePath1) }" + +let x = Script1.x + """ + + try + let projInfo2 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath2, filePath2) + + addProject miscFilesWorkspace projInfo2 + + openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath2).Id + + let doc2 = getDocument workspace filePath2 + assertEmptyDocumentDiagnostics doc2 + + finally + try File.Delete(filePath1) with | _ -> () + try File.Delete(filePath2) with | _ -> () + + [] + let ``Script file referencing another script will correct update when the referenced script file changes``() = + let workspace = createWorkspace() + let miscFilesWorkspace = createMiscFileWorkspace() + let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) + + let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) + + let filePath1 = + createOnDiskScript + """ +module Script1 + """ + + let filePath2 = + createOnDiskScript + $""" +module Script2 +#load "{ Path.GetFileName(filePath1) }" + +let x = Script1.x + """ + + try + let projInfo1 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath1, filePath1) + let projInfo2 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath2, filePath2) + + addProject miscFilesWorkspace projInfo1 + addProject miscFilesWorkspace projInfo2 + + openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath1).Id + openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath2).Id + + let doc1 = getDocument workspace filePath1 + assertEmptyDocumentDiagnostics doc1 + + let doc2 = getDocument workspace filePath2 + assertHasDocumentDiagnostics doc2 + + File.WriteAllText(filePath1, + """ +module Script1 + +let x = 1 + """) + + assertEmptyDocumentDiagnostics doc2 + + finally + try File.Delete(filePath1) with | _ -> () + try File.Delete(filePath2) with | _ -> () + + [] + let ``Script file referencing another script will correct update when the referenced script file changes with opening in reverse order``() = + let workspace = createWorkspace() + let miscFilesWorkspace = createMiscFileWorkspace() + let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) + + let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) + + let filePath1 = + createOnDiskScript + """ +module Script1 + """ + + let filePath2 = + createOnDiskScript + $""" +module Script2 +#load "{ Path.GetFileName(filePath1) }" + +let x = Script1.x + """ + + try + let projInfo1 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath1, filePath1) + let projInfo2 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath2, filePath2) + + addProject miscFilesWorkspace projInfo1 + addProject miscFilesWorkspace projInfo2 + + openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath2).Id + openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath1).Id + + let doc2 = getDocument workspace filePath2 + assertHasDocumentDiagnostics doc2 + + let doc1 = getDocument workspace filePath1 + assertEmptyDocumentDiagnostics doc1 + + File.WriteAllText(filePath1, + """ +module Script1 + +let x = 1 + """) + + assertEmptyDocumentDiagnostics doc2 + + finally + try File.Delete(filePath1) with | _ -> () + try File.Delete(filePath2) with | _ -> ()