diff --git a/src/fsharp/service/IncrementalBuild.fs b/src/fsharp/service/IncrementalBuild.fs index ddcfefd2a5a..828c553e4e9 100644 --- a/src/fsharp/service/IncrementalBuild.fs +++ b/src/fsharp/service/IncrementalBuild.fs @@ -729,13 +729,12 @@ type IncrementalBuilder( #if !NO_EXTENSIONTYPING importsInvalidatedByTypeProvider: Event, #endif - allDependencies) = + allDependencies, + defaultTimeStamp: DateTime) = let fileParsed = new Event() let projectChecked = new Event() - let defaultTimeStamp = DateTime.UtcNow - let mutable isImportsInvalidated = false #if !NO_EXTENSIONTYPING @@ -1075,7 +1074,7 @@ type IncrementalBuilder( let tryGetBeforeSlot (state: IncrementalBuilderState) slot = match slot with | 0 (* first file *) -> - (initialBoundModel, DateTime.MinValue) + (initialBoundModel, defaultTimeStamp) |> Some | _ -> tryGetSlot state (slot - 1) @@ -1083,7 +1082,7 @@ type IncrementalBuilder( let evalUpToTargetSlot (state: IncrementalBuilderState) targetSlot = node { if targetSlot < 0 then - return Some(initialBoundModel, DateTime.MinValue) + return Some(initialBoundModel, defaultTimeStamp) else let! boundModel = state.boundModels.[targetSlot].GetOrComputeValue() return Some(boundModel, state.stampedFileNames.[targetSlot]) @@ -1091,7 +1090,7 @@ type IncrementalBuilder( let MaxTimeStampInDependencies stamps = if Seq.isEmpty stamps then - DateTime.MinValue + defaultTimeStamp else stamps |> Seq.max @@ -1500,6 +1499,8 @@ type IncrementalBuilder( | None -> new DependencyProvider() | Some dependencyProvider -> dependencyProvider + let defaultTimeStamp = DateTime.UtcNow + let! initialBoundModel = CombineImportedAssembliesTask( assemblyName, @@ -1539,7 +1540,8 @@ type IncrementalBuilder( #if !NO_EXTENSIONTYPING importsInvalidatedByTypeProvider, #endif - allDependencies) + allDependencies, + defaultTimeStamp) return Some builder with e -> errorRecoveryNoRange e diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index 1ed29a76651..501ad1c4b02 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -334,6 +334,25 @@ type BackgroundCompiler( return (builderOpt, diagnostics) } + let parseCacheLock = Lock() + + // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.parseFileInProjectCache. Most recently used cache for parsing files. + let parseFileCache = MruCache(parseFileCacheSize, areSimilar = AreSimilarForParsing, areSame = AreSameForParsing) + + // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.checkFileInProjectCache + // + /// Cache which holds recently seen type-checks. + /// This cache may hold out-of-date entries, in two senses + /// - there may be a more recent antecedent state available because the background build has made it available + /// - the source for the file may have changed + + // Also keyed on source. This can only be out of date if the antecedent is out of date + let checkFileInProjectCache = + MruCache> + (keepStrongly=checkFileInProjectCacheSize, + areSame=AreSameForChecking3, + areSimilar=AreSubsumable3) + // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.backgroundCompiler.incrementalBuildersCache. This root typically holds more // live information than anything else in the F# Language Service, since it holds up to 3 (projectCacheStrongSize) background project builds // strongly. @@ -388,6 +407,17 @@ type BackgroundCompiler( Logger.Log LogCompilerFunctionId.Service_IncrementalBuildersCache_GettingCache return builderOpt,creationDiags | _ -> + // The builder could be re-created, + // clear the check file caches that are associated with it. + // We must do this in order to not return stale results when references + // in the project get changed/added/removed. + parseCacheLock.AcquireLock(fun ltok -> + options.SourceFiles + |> Array.iter (fun sourceFile -> + let key = (sourceFile, 0L, options) + checkFileInProjectCache.RemoveAnySimilar(ltok, key) + ) + ) return! createAndGetBuilder (options, userOpName) } | _ -> @@ -413,25 +443,6 @@ type BackgroundCompiler( | _ -> getOrCreateBuilder (options, userOpName) - let parseCacheLock = Lock() - - // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.parseFileInProjectCache. Most recently used cache for parsing files. - let parseFileCache = MruCache(parseFileCacheSize, areSimilar = AreSimilarForParsing, areSame = AreSameForParsing) - - // STATIC ROOT: FSharpLanguageServiceTestable.FSharpChecker.checkFileInProjectCache - // - /// Cache which holds recently seen type-checks. - /// This cache may hold out-of-date entries, in two senses - /// - there may be a more recent antecedent state available because the background build has made it available - /// - the source for the file may have changed - - // Also keyed on source. This can only be out of date if the antecedent is out of date - let checkFileInProjectCache = - MruCache> - (keepStrongly=checkFileInProjectCacheSize, - areSame=AreSameForChecking3, - areSimilar=AreSubsumable3) - /// Should be a fast operation. Ensures that we have only one async lazy object per file and its hash. let getCheckFileNode (parseResults, sourceText, diff --git a/tests/fsharp/Compiler/Service/MultiProjectTests.fs b/tests/fsharp/Compiler/Service/MultiProjectTests.fs index 6b4bedc74f0..9f0bc331786 100644 --- a/tests/fsharp/Compiler/Service/MultiProjectTests.fs +++ b/tests/fsharp/Compiler/Service/MultiProjectTests.fs @@ -72,6 +72,54 @@ let test() = Assert.shouldBeEmpty(checkResults.Diagnostics) WeakReference(ms) + let compileFileAsDll (checker: FSharpChecker) filePath outputFilePath = + try + let result, _ = + checker.Compile([|"fsc.dll";filePath;$"-o:{ outputFilePath }";"--deterministic+";"--optimize+";"--target:library"|]) + |> Async.RunSynchronously + + if result.Length > 0 then + failwith "Compilation has errors." + with + | _ -> + try File.Delete(outputFilePath) with | _ -> () + reraise() + + let createOnDisk src = + let tmpFilePath = Path.GetTempFileName() + let tmpRealFilePath = Path.ChangeExtension(tmpFilePath, ".fs") + try File.Delete(tmpFilePath) with | _ -> () + File.WriteAllText(tmpRealFilePath, src) + tmpRealFilePath + + let createOnDiskCompiledAsDll checker src = + let tmpFilePath = Path.GetTempFileName() + let tmpRealFilePath = Path.ChangeExtension(tmpFilePath, ".fs") + try File.Delete(tmpFilePath) with | _ -> () + File.WriteAllText(tmpRealFilePath, src) + + let outputFilePath = Path.ChangeExtension(tmpRealFilePath, ".dll") + + try + compileFileAsDll checker tmpRealFilePath outputFilePath + outputFilePath + finally + try File.Delete(tmpRealFilePath) with | _ -> () + + let updateFileOnDisk filePath src = + File.WriteAllText(filePath, src) + + let updateCompiledDllOnDisk checker (dllPath: string) src = + if not (File.Exists dllPath) then + failwith $"File {dllPath} does not exist." + + let filePath = createOnDisk src + + try + compileFileAsDll checker filePath dllPath + finally + try File.Delete(filePath) with | _ -> () + [] let ``Using a CSharp reference project in-memory``() = AssertInMemoryCSharpReferenceIsValid() |> ignore @@ -83,5 +131,73 @@ let test() = GC.Collect(2, GCCollectionMode.Forced, true) Assert.shouldBeFalse(weakRef.IsAlive) + [] + let ``Using compiler service, file referencing a DLL will correctly update when the referenced DLL file changes``() = + let checker = CompilerAssert.Checker + + let dllPath1 = + createOnDiskCompiledAsDll checker + """ +module Script1 + +let x = 1 + """ + + let filePath1 = + createOnDisk + """ +module Script2 + +let x = Script1.x + """ + + try + let fsOptions1 = CompilerAssert.DefaultProjectOptions + let fsOptions1 = + { fsOptions1 with + ProjectId = Some(Guid.NewGuid().ToString()) + OtherOptions = [|"-r:" + dllPath1|] + ReferencedProjects = [||] + SourceFiles = [|filePath1|] } + + let checkProjectResults = + checker.ParseAndCheckProject(fsOptions1) + |> Async.RunSynchronously + + Assert.IsEmpty(checkProjectResults.Diagnostics) + + updateFileOnDisk filePath1 + """ +module Script2 + +let x = Script1.x +let y = Script1.y + """ + + let checkProjectResults = + checker.ParseAndCheckProject(fsOptions1) + |> Async.RunSynchronously + + Assert.IsNotEmpty(checkProjectResults.Diagnostics) + + updateCompiledDllOnDisk checker dllPath1 + """ +module Script1 + +let x = 1 +let y = 1 + """ + + let checkProjectResults = + checker.ParseAndCheckProject(fsOptions1) + |> Async.RunSynchronously + + Assert.IsEmpty(checkProjectResults.Diagnostics) + + finally + try File.Delete(dllPath1) with | _ -> () + try File.Delete(filePath1) with | _ -> () + + diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs index 637c6dd92d7..908e71ae13d 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs @@ -27,13 +27,29 @@ type internal IFSharpWorkspaceProjectContext = abstract SetProjectReferences : IFSharpWorkspaceProjectContext seq -> unit + abstract MetadataReferenceCount : int + + abstract HasMetadataReference : referencePath: string -> bool + + abstract SetMetadataReferences : referencePaths: string seq -> unit + type internal IFSharpWorkspaceProjectContextFactory = abstract CreateProjectContext : filePath: string -> IFSharpWorkspaceProjectContext +type private ProjectContextState = + { + refs: ImmutableDictionary + metadataRefs: ImmutableHashSet + } + type internal FSharpWorkspaceProjectContext(vsProjectContext: IWorkspaceProjectContext) = - let mutable refs = ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase) + let mutable state = + { + refs = ImmutableDictionary.Create(StringComparer.OrdinalIgnoreCase) + metadataRefs = ImmutableHashSet.Create(equalityComparer = StringComparer.OrdinalIgnoreCase) + } member private _.VisualStudioProjectContext = vsProjectContext @@ -52,20 +68,27 @@ type internal FSharpWorkspaceProjectContext(vsProjectContext: IWorkspaceProjectC | _ -> () + member private _.AddMetadataReference(builder: ImmutableHashSet<_>.Builder, referencePath: string) = + vsProjectContext.AddMetadataReference(referencePath, MetadataReferenceProperties.Assembly) + builder.Add(referencePath) |> ignore + + member private _.RemoveMetadataReference(referencePath: string) = + vsProjectContext.RemoveMetadataReference(referencePath) + interface IFSharpWorkspaceProjectContext with member _.Id = vsProjectContext.Id member _.FilePath = vsProjectContext.ProjectFilePath - member _.ProjectReferenceCount = refs.Count + member _.ProjectReferenceCount = state.refs.Count - member _.HasProjectReference(filePath) = refs.ContainsKey(filePath) + member _.HasProjectReference(filePath) = state.refs.ContainsKey(filePath) member this.SetProjectReferences(projRefs) = let builder = ImmutableDictionary.CreateBuilder() - refs.Values + state.refs.Values |> Seq.iter (fun x -> this.RemoveProjectReference(x) ) @@ -75,7 +98,26 @@ type internal FSharpWorkspaceProjectContext(vsProjectContext: IWorkspaceProjectC this.AddProjectReference(builder, x) ) - refs <- builder.ToImmutable() + state <- { state with refs = builder.ToImmutable() } + + member _.MetadataReferenceCount = state.metadataRefs.Count + + member _.HasMetadataReference(referencePath) = state.metadataRefs.Contains(referencePath) + + member this.SetMetadataReferences(referencePaths) = + let builder = ImmutableHashSet.CreateBuilder() + + state.metadataRefs + |> Seq.iter (fun x -> + this.RemoveMetadataReference(x) + ) + + referencePaths + |> Seq.iter (fun x -> + this.AddMetadataReference(builder, x) + ) + + state <- { state with metadataRefs = builder.ToImmutable() } member _.Dispose() = vsProjectContext.Dispose() @@ -105,7 +147,7 @@ type internal FSharpMiscellaneousFileService(workspace: Workspace, let files = ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) let optionsManager = workspace.Services.GetRequiredService().FSharpProjectOptionsManager - static let mustUpdateProject (refSourceFiles: string []) (projectContext: IFSharpWorkspaceProjectContext) = + static let mustUpdateProjectReferences (refSourceFiles: string []) (projectContext: IFSharpWorkspaceProjectContext) = refSourceFiles.Length <> projectContext.ProjectReferenceCount || ( refSourceFiles @@ -113,6 +155,14 @@ type internal FSharpMiscellaneousFileService(workspace: Workspace, |> not ) + static let mustUpdateMetadataReferences (referencePaths: string []) (projectContext: IFSharpWorkspaceProjectContext) = + referencePaths.Length <> projectContext.MetadataReferenceCount || + ( + referencePaths + |> Seq.forall projectContext.HasMetadataReference + |> not + ) + let tryRemove (document: Document) = let projIds = document.Project.Solution.GetDependentProjectIds(document.Project.Id) if projIds.Count = 0 then @@ -131,9 +181,18 @@ type internal FSharpMiscellaneousFileService(workspace: Workspace, let filePath = scriptProjectOptions.SourceFiles.[scriptProjectOptions.SourceFiles.Length - 1] let refSourceFiles = scriptProjectOptions.SourceFiles |> Array.take (scriptProjectOptions.SourceFiles.Length - 1) + let referencePaths = + scriptProjectOptions.OtherOptions + |> Seq.filter (fun x -> x.StartsWith("-r:", StringComparison.OrdinalIgnoreCase)) + |> Seq.map (fun x -> + let startIndex = "-r:".Length + x.Substring(startIndex, x.Length - startIndex) + ) + |> Array.ofSeq + match files.TryGetValue(filePath) with | true, (projectContext: IFSharpWorkspaceProjectContext) -> - if mustUpdateProject refSourceFiles projectContext then + if mustUpdateProjectReferences refSourceFiles projectContext then lock gate (fun () -> let newProjRefs = refSourceFiles @@ -148,6 +207,11 @@ type internal FSharpMiscellaneousFileService(workspace: Workspace, projectContext.SetProjectReferences(newProjRefs) ) + + if mustUpdateMetadataReferences referencePaths projectContext then + lock gate (fun () -> + projectContext.SetMetadataReferences(referencePaths) + ) | _ -> () ) diff --git a/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs index e9c2bfe5fee..f33ffbc52da 100644 --- a/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs +++ b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs @@ -2,6 +2,7 @@ open System open System.IO +open System.Text open System.Reflection open System.Linq open System.Composition.Hosting @@ -22,6 +23,21 @@ open NUnit.Framework [] module WorkspaceTests = + let compileFileAsDll (workspace: Workspace) filePath outputFilePath = + let workspaceService = workspace.Services.GetRequiredService() + + try + let result, _ = + workspaceService.Checker.Compile([|"fsc.dll";filePath;$"-o:{ outputFilePath }";"--deterministic+";"--optimize+";"--target:library"|]) + |> Async.RunSynchronously + + if result.Length > 0 then + failwith "Compilation has errors." + with + | _ -> + try File.Delete(outputFilePath) with | _ -> () + reraise() + let createOnDiskScript src = let tmpFilePath = Path.GetTempFileName() let tmpRealFilePath = Path.ChangeExtension(tmpFilePath, ".fsx") @@ -29,13 +45,44 @@ module WorkspaceTests = File.WriteAllText(tmpRealFilePath, src) tmpRealFilePath + let createOnDiskCompiledScriptAsDll (workspace: Workspace) src = + let tmpFilePath = Path.GetTempFileName() + let tmpRealFilePath = Path.ChangeExtension(tmpFilePath, ".fsx") + try File.Delete(tmpFilePath) with | _ -> () + File.WriteAllText(tmpRealFilePath, src) + + let outputFilePath = Path.ChangeExtension(tmpRealFilePath, ".dll") + + try + compileFileAsDll workspace tmpRealFilePath outputFilePath + outputFilePath + finally + try File.Delete(tmpRealFilePath) with | _ -> () + let createWorkspace() = new AdhocWorkspace(TestHostServices()) let createMiscFileWorkspace() = createWorkspace() - let openDocument (workspace: Workspace) (docId: DocumentId) = + let createProjectInfoWithFileOnDisk filePath = + RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath, filePath) + + let getDocumentId (workspace: Workspace) filePath = + let solution = workspace.CurrentSolution + solution.GetDocumentIdsWithFilePath(filePath) + |> Seq.exactlyOne + + let getDocument (workspace: Workspace) filePath = + let solution = workspace.CurrentSolution + solution.GetDocumentIdsWithFilePath(filePath) + |> Seq.exactlyOne + |> solution.GetDocument + + let openDocument (workspace: Workspace) (filePath: string) = + let docId = + workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath) + |> Seq.exactlyOne use waitHandle = new ManualResetEventSlim(false) use _sub = workspace.DocumentOpened.Subscribe(fun _ -> waitHandle.Set() @@ -43,11 +90,71 @@ module WorkspaceTests = workspace.OpenDocument(docId) waitHandle.Wait() - let getDocument (workspace: Workspace) filePath = - let solution = workspace.CurrentSolution - solution.GetDocumentIdsWithFilePath(filePath) - |> Seq.exactlyOne - |> solution.GetDocument + let updateDocumentOnDisk (workspace: Workspace) filePath src = + File.WriteAllText(filePath, src) + + let doc = getDocument workspace filePath + // The adhoc workspaces do not listen for files changing on disk, + // so we simulate the update here. + let newSolution = + doc.Project.Solution.WithDocumentTextLoader( + doc.Id, + new FileTextLoader(doc.FilePath, Encoding.Default), + PreservationMode.PreserveIdentity) + workspace.TryApplyChanges(newSolution) |> ignore + + let updateCompiledDllOnDisk workspace (dllPath: string) src = + if not (File.Exists dllPath) then + failwith $"File {dllPath} does not exist." + + let filePath = createOnDiskScript src + + try + compileFileAsDll workspace filePath dllPath + + let newMetadataReference = + PortableExecutableReference.CreateFromFile( + dllPath, + MetadataReferenceProperties.Assembly + ) + // The adhoc workspaces do not listen for files changing on disk, + // so we simulate the update here. + let solution = workspace.CurrentSolution + let projects = solution.Projects + let newSolution = + (solution, projects) + ||> Seq.fold (fun solution proj -> + let mutable mustUpdate = false + let metadataReferences = + proj.MetadataReferences + |> Array.ofSeq + |> Array.map (fun x -> + match x with + | :? PortableExecutableReference as x -> + if String.Equals(x.FilePath, newMetadataReference.FilePath, StringComparison.OrdinalIgnoreCase) then + mustUpdate <- true + newMetadataReference :> MetadataReference + else + x :> MetadataReference + | _ -> + x + ) + + if mustUpdate then + solution.WithProjectMetadataReferences(proj.Id, metadataReferences) + else + solution + ) + workspace.TryApplyChanges(newSolution) |> ignore + finally + try File.Delete(filePath) with | _ -> () + + let isDocumentOpen (workspace: Workspace) filePath = + let docId = getDocumentId workspace filePath + workspace.IsDocumentOpen(docId) + + let hasDocument (workspace: Workspace) filePath = + workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Length > 0 let addProject (workspace: Workspace) projInfo = if not (workspace.TryApplyChanges(workspace.CurrentSolution.AddProject(projInfo))) then @@ -57,13 +164,15 @@ module WorkspaceTests = if not (workspace.TryApplyChanges(workspace.CurrentSolution.RemoveProject(projId))) then failwith "Unable to apply workspace changes." - let assertEmptyDocumentDiagnostics (doc: Document) = + let assertEmptyDocumentDiagnostics (workspace: Workspace) (filePath: string) = + let doc = getDocument workspace filePath let parseResults, checkResults = doc.GetFSharpParseAndCheckResultsAsync("assertEmptyDocumentDiagnostics") |> Async.RunSynchronously Assert.IsEmpty(parseResults.Diagnostics) Assert.IsEmpty(checkResults.Diagnostics) - let assertHasDocumentDiagnostics (doc: Document) = + let assertHasDocumentDiagnostics (workspace: Workspace) (filePath: string) = + let doc = getDocument workspace filePath let parseResults, checkResults = doc.GetFSharpParseAndCheckResultsAsync("assertHasDocumentDiagnostics") |> Async.RunSynchronously Assert.IsEmpty(parseResults.Diagnostics) @@ -97,7 +206,7 @@ module WorkspaceTests = let currentProj = mainProj let mutable solution = currentProj.Solution - mainProj.ProjectReferences + currentProj.ProjectReferences |> Seq.iter (fun projRef -> solution <- solution.RemoveProjectReference(currentProj.Id, projRef) ) @@ -115,6 +224,42 @@ module WorkspaceTests = mainProj <- solution.GetProject(currentProj.Id) + member this.MetadataReferenceCount: int = mainProj.MetadataReferences.Count + + member this.HasMetadataReference(referencePath: string): bool = + mainProj.MetadataReferences + |> Seq.exists (fun x -> + match x with + | :? PortableExecutableReference as r -> + String.Equals(r.FilePath, referencePath, StringComparison.OrdinalIgnoreCase) + | _ -> + false) + + member this.SetMetadataReferences(referencePaths: string seq): unit = + let currentProj = mainProj + let mutable solution = currentProj.Solution + + currentProj.MetadataReferences + |> Seq.iter (fun r -> + solution <- solution.RemoveMetadataReference(currentProj.Id, r) + ) + + referencePaths + |> Seq.iter (fun referencePath -> + solution <- + solution.AddMetadataReference( + currentProj.Id, + PortableExecutableReference.CreateFromFile( + referencePath, + MetadataReferenceProperties.Assembly + ) + ) + ) + + not (solution.Workspace.TryApplyChanges(solution)) |> ignore + + mainProj <- solution.GetProject(currentProj.Id) + type TestFSharpWorkspaceProjectContextFactory(workspace: Workspace, miscFilesWorkspace: Workspace) = interface IFSharpWorkspaceProjectContextFactory with @@ -134,8 +279,8 @@ module WorkspaceTests = [] let ``Script file opened in misc files workspace will get transferred to normal workspace``() = - let workspace = createWorkspace() - let miscFilesWorkspace = createMiscFileWorkspace() + use workspace = createWorkspace() + use miscFilesWorkspace = createMiscFileWorkspace() let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) @@ -149,34 +294,31 @@ let x = 1 """ try - let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath, filePath) + let projInfo = createProjectInfoWithFileOnDisk 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.IsTrue(hasDocument miscFilesWorkspace filePath) + Assert.IsFalse(hasDocument workspace filePath) - Assert.AreEqual(0, miscFilesWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Length) - Assert.AreEqual(1, workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Length) + Assert.IsFalse(isDocumentOpen miscFilesWorkspace filePath) + openDocument miscFilesWorkspace filePath - let doc = getDocument workspace filePath + // Although we opened the document, it has been transferred to the other workspace. + Assert.IsFalse(hasDocument miscFilesWorkspace filePath) + Assert.IsTrue(hasDocument workspace filePath) - Assert.IsFalse(workspace.IsDocumentOpen(doc.Id)) + // Should not be automatically opened when transferred. + Assert.IsFalse(isDocumentOpen workspace filePath) - assertEmptyDocumentDiagnostics doc + assertEmptyDocumentDiagnostics workspace filePath finally try File.Delete(filePath) with | _ -> () [] let ``Script file referencing another script should have no diagnostics``() = - let workspace = createWorkspace() - let miscFilesWorkspace = createMiscFileWorkspace() + use workspace = createWorkspace() + use miscFilesWorkspace = createMiscFileWorkspace() let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) @@ -199,23 +341,19 @@ let x = Script1.x """ try - let projInfo2 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath2, filePath2) - + let projInfo2 = createProjectInfoWithFileOnDisk filePath2 addProject miscFilesWorkspace projInfo2 - - openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath2).Id - - let doc2 = getDocument workspace filePath2 - assertEmptyDocumentDiagnostics doc2 + openDocument miscFilesWorkspace filePath2 + assertEmptyDocumentDiagnostics workspace filePath2 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 ``Script file referencing another script will correctly update when the referenced script file changes``() = + use workspace = createWorkspace() + use miscFilesWorkspace = createMiscFileWorkspace() let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) @@ -236,38 +374,35 @@ let x = Script1.x """ try - let projInfo1 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath1, filePath1) - let projInfo2 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath2, filePath2) + let projInfo1 = createProjectInfoWithFileOnDisk filePath1 + let projInfo2 = createProjectInfoWithFileOnDisk filePath2 addProject miscFilesWorkspace projInfo1 addProject miscFilesWorkspace projInfo2 - openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath1).Id - openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath2).Id + openDocument miscFilesWorkspace filePath1 + openDocument miscFilesWorkspace filePath2 - let doc1 = getDocument workspace filePath1 - assertEmptyDocumentDiagnostics doc1 - - let doc2 = getDocument workspace filePath2 - assertHasDocumentDiagnostics doc2 + assertEmptyDocumentDiagnostics workspace filePath1 + assertHasDocumentDiagnostics workspace filePath2 - File.WriteAllText(filePath1, + updateDocumentOnDisk workspace filePath1 """ module Script1 let x = 1 - """) + """ - assertEmptyDocumentDiagnostics doc2 + assertEmptyDocumentDiagnostics workspace filePath2 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 ``Script file referencing another script will correctly update when the referenced script file changes with opening in reverse order``() = + use workspace = createWorkspace() + use miscFilesWorkspace = createMiscFileWorkspace() let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) @@ -288,30 +423,86 @@ let x = Script1.x """ try - let projInfo1 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath1, filePath1) - let projInfo2 = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath2, filePath2) + let projInfo1 = createProjectInfoWithFileOnDisk filePath1 + let projInfo2 = createProjectInfoWithFileOnDisk filePath2 addProject miscFilesWorkspace projInfo1 addProject miscFilesWorkspace projInfo2 - openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath2).Id - openDocument miscFilesWorkspace (getDocument miscFilesWorkspace filePath1).Id + openDocument miscFilesWorkspace filePath2 + openDocument miscFilesWorkspace filePath1 - let doc2 = getDocument workspace filePath2 - assertHasDocumentDiagnostics doc2 - - let doc1 = getDocument workspace filePath1 - assertEmptyDocumentDiagnostics doc1 + assertHasDocumentDiagnostics workspace filePath2 + assertEmptyDocumentDiagnostics workspace filePath1 - File.WriteAllText(filePath1, + updateDocumentOnDisk workspace filePath1 """ module Script1 let x = 1 - """) + """ - assertEmptyDocumentDiagnostics doc2 + assertEmptyDocumentDiagnostics workspace filePath2 finally try File.Delete(filePath1) with | _ -> () try File.Delete(filePath2) with | _ -> () + + [] + let ``Script file referencing a DLL will correctly update when the referenced DLL file changes``() = + use workspace = createWorkspace() + use miscFilesWorkspace = createMiscFileWorkspace() + let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) + + let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) + + let dllPath1 = + createOnDiskCompiledScriptAsDll workspace + """ +module Script1 + +let x = 1 + """ + + let filePath1 = + createOnDiskScript + $""" +module Script2 +#r "{ Path.GetFileName(dllPath1) }" + +let x = Script1.x + """ + + try + let projInfo1 = createProjectInfoWithFileOnDisk filePath1 + + addProject miscFilesWorkspace projInfo1 + + openDocument miscFilesWorkspace filePath1 + + assertEmptyDocumentDiagnostics workspace filePath1 + + updateDocumentOnDisk workspace filePath1 + $""" +module Script2 +#r "{ Path.GetFileName(dllPath1) }" + +let x = Script1.x +let y = Script1.y + """ + + assertHasDocumentDiagnostics workspace filePath1 + + updateCompiledDllOnDisk workspace dllPath1 + """ +module Script1 + +let x = 1 +let y = 1 + """ + + assertEmptyDocumentDiagnostics workspace filePath1 + + finally + try File.Delete(dllPath1) with | _ -> () + try File.Delete(filePath1) with | _ -> ()