From f66c3f1c8e27ed860f4fdc6f5656cad3f1bfba72 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Mon, 7 Jun 2021 16:50:08 -0700 Subject: [PATCH 01/15] Partial fix for FSharp scripts in VS --- .../FSharpProjectOptionsManager.fs | 17 ++-- .../LanguageService/SingleFileWorkspaceMap.fs | 83 ++++++++++++++++--- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index aa5d35b081..b2055057cc 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -108,12 +108,14 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor 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 @@ -193,6 +195,7 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor let projectOptions = if isScriptFile document.FilePath then + scriptUpdatedEvent.Trigger(scriptProjectOptions) scriptProjectOptions else { @@ -211,12 +214,12 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor let parsingOptions, _ = checkerProvider.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 settings ct then singleFileCache.TryRemove(document.Id) |> ignore return! tryComputeOptionsBySingleScriptOrFile document ct userOpName else @@ -407,7 +410,7 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor legacyProjectSites.TryRemove(projectId) |> ignore | FSharpProjectOptionsMessage.ClearSingleFileOptionsCache(documentId) -> match singleFileCache.TryRemove(documentId) with - | true, (_, _, projectOptions) -> + | true, (_, _, _, projectOptions) -> lastSuccessfulCompilations.TryRemove(documentId.ProjectId) |> ignore checkerProvider.Checker.ClearCache([projectOptions]) | _ -> @@ -446,6 +449,8 @@ type private FSharpProjectOptionsReactor (workspace: Workspace, settings: Editor singleFileCache.Clear() lastSuccessfulCompilations.Clear() + member _.ScriptUpdated = scriptUpdatedEvent.Publish + interface IDisposable with member _.Dispose() = cancellationTokenSource.Cancel() @@ -488,6 +493,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/SingleFileWorkspaceMap.fs b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs index 91106217e9..8ed75459ac 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs @@ -11,6 +11,7 @@ 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, @@ -19,6 +20,7 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, projectContextFactory: IWorkspaceProjectContextFactory, rdt: IVsRunningDocumentTable) as this = + let gate = obj() let files = ConcurrentDictionary(StringComparer.OrdinalIgnoreCase) let createSourceCodeKind (filePath: string) = @@ -31,36 +33,91 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, let projectContext = projectContextFactory.CreateProjectContext(FSharpConstants.FSharpLanguageName, filePath, filePath, Guid.NewGuid(), null, null) projectContext.DisplayName <- FSharpConstants.FSharpMiscellaneousFilesName projectContext.AddSourceFile(filePath, sourceCodeKind = createSourceCodeKind filePath) - projectContext + projectContext, ResizeArray() 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] + + lock gate (fun () -> + match files.TryGetValue(filePath) with + | true, (projectContext: IWorkspaceProjectContext, currentDepSourceFiles: ResizeArray<_>) -> + + let depSourceFiles = scriptProjectOptions.SourceFiles |> Array.filter (fun x -> x.Equals(filePath, StringComparison.OrdinalIgnoreCase) |> not) + + if depSourceFiles.Length <> currentDepSourceFiles.Count || + ( + (currentDepSourceFiles, depSourceFiles) + ||> Seq.forall2 (fun (x: string) y -> x.Equals(y, StringComparison.OrdinalIgnoreCase)) + |> not + + ) then + currentDepSourceFiles + |> Seq.iter (fun x -> + match files.TryGetValue(x) with + | true, (depProjectContext, _) -> + projectContext.RemoveProjectReference(depProjectContext) + | _ -> + () + ) + + currentDepSourceFiles.Clear() + depSourceFiles + |> Array.iter (fun filePath -> + currentDepSourceFiles.Add(filePath) + match files.TryGetValue(filePath) with + | true, (depProjectContext, _) -> + projectContext.AddProjectReference(depProjectContext, MetadataReferenceProperties.Assembly) + | _ -> + let result = createProjectContext filePath + files.[filePath] <- result + let depProjectContext, _ = result + projectContext.AddProjectReference(depProjectContext, MetadataReferenceProperties.Assembly) + ) + + | _ -> () + ) + ) + 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 + let filePath = document.FilePath + lock gate (fun () -> + if files.ContainsKey(filePath) |> not then + files.[filePath] <- 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() - | _ -> () + optionsManager.ClearSingleFileOptionsCache(document.Id) + + lock gate (fun () -> + match files.TryRemove(document.FilePath) with + | true, (projectContext, _) -> + projectContext.Dispose() + | _ -> () + ) ) 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 -> - optionsManager.ClearSingleFileOptionsCache(document.Id) - projectContext.Dispose() - | _ -> () + optionsManager.ClearSingleFileOptionsCache(document.Id) + + lock gate (fun () -> + match files.TryRemove(document.FilePath) with + | true, (projectContext, _) -> + projectContext.Dispose() + | _ -> () + ) ) do @@ -88,7 +145,7 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, // Handles renaming of a misc file if (grfAttribs &&& (uint32 __VSRDTATTRIB.RDTA_MkDocument)) <> 0u && files.ContainsKey(pszMkDocumentOld) then match files.TryRemove(pszMkDocumentOld) with - | true, projectContext -> + | true, (projectContext, _) -> let project = workspace.CurrentSolution.GetProject(projectContext.Id) if project <> null then let documentOpt = From 469748078fa704f3a1a9da7e12dda0869b112d8d Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 10:16:07 -0700 Subject: [PATCH 02/15] Fixing build --- .../LanguageService/FSharpProjectOptionsManager.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index e69a155eed..e30c178f5b 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -211,7 +211,7 @@ type private FSharpProjectOptionsReactor (checker: FSharpChecker) = return Some(parsingOptions, projectOptions) | true, (oldProject, oldFileStamp, parsingOptions, projectOptions) -> - if fileStamp <> oldFileStamp || isProjectInvalidated document.Project oldProject settings ct then + if fileStamp <> oldFileStamp || isProjectInvalidated document.Project oldProject ct then singleFileCache.TryRemove(document.Id) |> ignore return! tryComputeOptionsBySingleScriptOrFile document ct userOpName else From 682c4c067e9a88289eecdfb29454068660affde2 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 11:28:14 -0700 Subject: [PATCH 03/15] Added DocumentInfo/ProjectInfo CreateFSharp extension --- .../src/FSharp.Editor/Common/Extensions.fs | 45 ++++++++- .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../IFSharpWorkspaceService.fs | 15 +++ .../LanguageService/LanguageService.fs | 11 +-- .../LanguageService/SingleFileWorkspaceMap.fs | 93 ++++++++++++------- .../tests/UnitTests/Tests.RoslynHelpers.fs | 31 ++----- .../UnitTests/VisualFSharp.UnitTests.fsproj | 4 +- 7 files changed, 125 insertions(+), 75 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/LanguageService/IFSharpWorkspaceService.fs diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index f6f07117b5..686ecca15a 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -39,10 +39,49 @@ type ProjectId with this.Id.ToString("D").ToLowerInvariant() type Project with - member this.IsFSharpMiscellaneous = this.Name = FSharpConstants.FSharpMiscellaneousFilesName - member this.IsFSharpMetadata = this.Name.StartsWith(FSharpConstants.FSharpMetadataName) - member this.IsFSharpMiscellaneousOrMetadata = this.IsFSharpMiscellaneous || this.IsFSharpMetadata member this.IsFSharp = this.Language = LanguageNames.FSharp + member this.IsFSharpMiscellaneous = this.IsFSharp && this.Name = FSharpConstants.FSharpMiscellaneousFilesName + member this.IsFSharpMetadata = this.IsFSharp && this.Name.StartsWith(FSharpConstants.FSharpMetadataName) + member this.IsFSharpMiscellaneousOrMetadata = this.IsFSharp && (this.IsFSharpMiscellaneous || this.IsFSharpMetadata) + +type DocumentInfo with + + static member CreateFSharp(projectId, filePath, ?loader) = + let isScript = isScriptFile filePath + let docId = DocumentId.CreateNewId(projectId) + DocumentInfo.Create( + docId, + filePath, + filePath=filePath, + loader = defaultArg loader null, + sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) + +type ProjectInfo with + + static member CreateFSharp(name, assemblyName: string, sourceFiles: string seq, ?filePath: string) = + let projId = ProjectId.CreateNewId() + + let docInfos = + sourceFiles + |> Seq.map (fun sourceFile -> + let isScript = isScriptFile sourceFile + let docId = DocumentId.CreateNewId(projId) + DocumentInfo.Create( + docId, + sourceFile, + filePath=sourceFile, + sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) + ) + + ProjectInfo.Create( + projId, + VersionStamp.Create(DateTime.UtcNow), + name, + assemblyName, + LanguageNames.FSharp, + documents = docInfos, + filePath = defaultArg filePath null + ) type Document with member this.TryGetLanguageService<'T when 'T :> ILanguageService>() = 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/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..9373b4d131 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() @@ -270,8 +262,7 @@ type internal FSharpPackage() as this = let _singleFileWorkspaceMap = new SingleFileWorkspaceMap( workspace, - miscFilesWorkspace, - optionsManager, + miscFilesWorkspace, 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 8ed75459ac..9052a69578 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs @@ -13,47 +13,51 @@ open Microsoft.VisualStudio.Shell.Interop open Microsoft.VisualStudio.LanguageServices open FSharp.Compiler.CodeAnalysis +type private ScriptDependencies = ResizeArray + [] -type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, +type internal SingleFileWorkspaceMap(workspace: Workspace, miscFilesWorkspace: MiscellaneousFilesWorkspace, - optionsManager: FSharpProjectOptionsManager, projectContextFactory: IWorkspaceProjectContextFactory, rdt: IVsRunningDocumentTable) as this = + // 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 - let createSourceCodeKind (filePath: string) = + static let createSourceCodeKind (filePath: string) = if isScriptFile filePath then SourceCodeKind.Script else SourceCodeKind.Regular - let createProjectContext filePath = + static let canUpdateScript (depSourceFiles: string []) (currentDepSourceFiles: ScriptDependencies) = + depSourceFiles.Length <> currentDepSourceFiles.Count || + ( + (currentDepSourceFiles, depSourceFiles) + ||> Seq.forall2 (fun (x: string) y -> x.Equals(y, StringComparison.OrdinalIgnoreCase)) + |> not + + ) + + static let createProjectContext (projectContextFactory: IWorkspaceProjectContextFactory) filePath = let projectContext = projectContextFactory.CreateProjectContext(FSharpConstants.FSharpLanguageName, filePath, filePath, Guid.NewGuid(), null, null) projectContext.DisplayName <- FSharpConstants.FSharpMiscellaneousFilesName projectContext.AddSourceFile(filePath, sourceCodeKind = createSourceCodeKind filePath) - projectContext, ResizeArray() + projectContext, ScriptDependencies() 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 depSourceFiles = scriptProjectOptions.SourceFiles |> Array.take (scriptProjectOptions.SourceFiles.Length - 1) lock gate (fun () -> match files.TryGetValue(filePath) with - | true, (projectContext: IWorkspaceProjectContext, currentDepSourceFiles: ResizeArray<_>) -> - - let depSourceFiles = scriptProjectOptions.SourceFiles |> Array.filter (fun x -> x.Equals(filePath, StringComparison.OrdinalIgnoreCase) |> not) - - if depSourceFiles.Length <> currentDepSourceFiles.Count || - ( - (currentDepSourceFiles, depSourceFiles) - ||> Seq.forall2 (fun (x: string) y -> x.Equals(y, StringComparison.OrdinalIgnoreCase)) - |> not - - ) then + | true, (projectContext: IWorkspaceProjectContext, currentDepSourceFiles: ScriptDependencies) -> + if canUpdateScript depSourceFiles currentDepSourceFiles then currentDepSourceFiles |> Seq.iter (fun x -> match files.TryGetValue(x) with @@ -71,12 +75,11 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, | true, (depProjectContext, _) -> projectContext.AddProjectReference(depProjectContext, MetadataReferenceProperties.Assembly) | _ -> - let result = createProjectContext filePath + let result = createProjectContext projectContextFactory filePath files.[filePath] <- result let depProjectContext, _ = result projectContext.AddProjectReference(depProjectContext, MetadataReferenceProperties.Assembly) ) - | _ -> () ) ) @@ -84,40 +87,58 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, miscFilesWorkspace.DocumentOpened.Add(fun args -> let document = args.Document - if document.Project.Language = FSharpConstants.FSharpLanguageName && workspace.CurrentSolution.GetDocumentIdsWithFilePath(document.FilePath).Length = 0 then + // 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] <- createProjectContext filePath + files.[filePath] <- createProjectContext projectContextFactory filePath ) ) workspace.DocumentOpened.Add(fun args -> let document = args.Document - if document.Project.Language = FSharpConstants.FSharpLanguageName && - not document.Project.IsFSharpMiscellaneousOrMetadata then + if not document.Project.IsFSharpMiscellaneousOrMetadata then optionsManager.ClearSingleFileOptionsCache(document.Id) - lock gate (fun () -> - match files.TryRemove(document.FilePath) with - | true, (projectContext, _) -> - projectContext.Dispose() - | _ -> () - ) + let projectContextOpt = + lock gate (fun () -> + match files.TryRemove(document.FilePath) with + | true, (projectContext, _) -> + Some projectContext + | _ -> + None + ) + + match projectContextOpt with + | Some projectContext -> + projectContext.Dispose() + | _ -> + () ) workspace.DocumentClosed.Add(fun args -> let document = args.Document - if document.Project.Language = FSharpConstants.FSharpLanguageName && - document.Project.IsFSharpMiscellaneousOrMetadata then + if document.Project.IsFSharpMiscellaneousOrMetadata then optionsManager.ClearSingleFileOptionsCache(document.Id) - lock gate (fun () -> - match files.TryRemove(document.FilePath) with - | true, (projectContext, _) -> + let projectContextOpt = + lock gate (fun () -> + match files.TryRemove(document.FilePath) with + | true, (projectContext, _) -> + Some projectContext + | _ -> + None + ) + + match projectContextOpt with + | Some projectContext -> + let projIds = document.Project.Solution.GetDependentProjectIds(document.Project.Id) + if projIds.Count = 0 then projectContext.Dispose() - | _ -> () - ) + | _ -> + () ) do @@ -156,7 +177,7 @@ type internal SingleFileWorkspaceMap(workspace: VisualStudioWorkspace, | Some(document) -> optionsManager.ClearSingleFileOptionsCache(document.Id) projectContext.Dispose() - files.[pszMkDocumentNew] <- createProjectContext pszMkDocumentNew + files.[pszMkDocumentNew] <- createProjectContext projectContextFactory 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 | _ -> () diff --git a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs index 7481753ec3..e6d5ba9a3f 100644 --- a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs +++ b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs @@ -205,28 +205,11 @@ type RoslynTestHelpers private () = let workspace = new AdhocWorkspace(TestHostServices()) - let projId = ProjectId.CreateNewId() - let docId = DocumentId.CreateNewId(projId) - - let docInfo = - DocumentInfo.Create( - docId, - filePath, - loader=TextLoader.From(text.Container, VersionStamp.Create(DateTime.UtcNow)), - filePath=filePath, - sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) - let projFilePath = "C:\\test.fsproj" - let projInfo = - ProjectInfo.Create( - projId, - VersionStamp.Create(DateTime.UtcNow), - projFilePath, - "test.dll", - LanguageNames.FSharp, - documents = [docInfo], - filePath = projFilePath - ) + let projInfo = ProjectInfo.CreateFSharp(projFilePath, "test.dll", Seq.empty, filePath = projFilePath) + let docInfo = DocumentInfo.CreateFSharp(projInfo.Id, filePath, loader = TextLoader.From(text.Container, VersionStamp.Create(DateTime.UtcNow))) + + let projInfo = projInfo.WithDocuments([docInfo]) let solutionInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Create(DateTime.UtcNow), "test.sln", [projInfo]) @@ -234,15 +217,15 @@ type RoslynTestHelpers private () = let workspaceService = workspace.Services.GetService() - let document = solution.GetProject(projId).GetDocument(docId) + let document = solution.GetProject(projInfo.Id).GetDocument(docInfo.Id) match options with | Some options -> let options = { options with ProjectId = Some(Guid.NewGuid().ToString()) } - workspaceService.FSharpProjectOptionsManager.SetCommandLineOptions(projId, options.SourceFiles, options.OtherOptions |> ImmutableArray.CreateRange) + workspaceService.FSharpProjectOptionsManager.SetCommandLineOptions(projInfo.Id, options.SourceFiles, options.OtherOptions |> ImmutableArray.CreateRange) document.SetFSharpProjectOptionsForTesting(options) | _ -> - workspaceService.FSharpProjectOptionsManager.SetCommandLineOptions(projId, [|filePath|], ImmutableArray.Empty) + workspaceService.FSharpProjectOptionsManager.SetCommandLineOptions(projInfo.Id, [|filePath|], ImmutableArray.Empty) document diff --git a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj index baa2c21ae9..63c7fe2f60 100644 --- a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj +++ b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj @@ -131,7 +131,7 @@ - + From ed34c0508443e0979212e5d57d20851b4c23addf Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 11:30:13 -0700 Subject: [PATCH 04/15] Uncomment --- vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj index 63c7fe2f60..baa2c21ae9 100644 --- a/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj +++ b/vsintegration/tests/UnitTests/VisualFSharp.UnitTests.fsproj @@ -131,7 +131,7 @@ - + From 45c788717341c3597c1bbbdca83fbfc37b66c3f6 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 11:30:42 -0700 Subject: [PATCH 05/15] minor cleanup --- vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs | 2 -- 1 file changed, 2 deletions(-) diff --git a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs index e6d5ba9a3f..b97acd38a4 100644 --- a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs +++ b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs @@ -201,8 +201,6 @@ type TestHostServices() = type RoslynTestHelpers private () = static member CreateDocument (filePath, text: SourceText, ?options: FSharp.Compiler.CodeAnalysis.FSharpProjectOptions) = - let isScript = String.Equals(Path.GetExtension(filePath), ".fsx", StringComparison.OrdinalIgnoreCase) - let workspace = new AdhocWorkspace(TestHostServices()) let projFilePath = "C:\\test.fsproj" From 06d35a622245d8c90eeabf5e4d9799c32e12d804 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 12:24:04 -0700 Subject: [PATCH 06/15] Reverting ProjectInfo/DocumentInfo additions --- .../src/FSharp.Editor/Common/Extensions.fs | 39 ------------------- .../tests/UnitTests/Tests.RoslynHelpers.fs | 27 +++++++++++-- 2 files changed, 23 insertions(+), 43 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index 686ecca15a..2480f8ce9a 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -44,45 +44,6 @@ type Project with member this.IsFSharpMetadata = this.IsFSharp && this.Name.StartsWith(FSharpConstants.FSharpMetadataName) member this.IsFSharpMiscellaneousOrMetadata = this.IsFSharp && (this.IsFSharpMiscellaneous || this.IsFSharpMetadata) -type DocumentInfo with - - static member CreateFSharp(projectId, filePath, ?loader) = - let isScript = isScriptFile filePath - let docId = DocumentId.CreateNewId(projectId) - DocumentInfo.Create( - docId, - filePath, - filePath=filePath, - loader = defaultArg loader null, - sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) - -type ProjectInfo with - - static member CreateFSharp(name, assemblyName: string, sourceFiles: string seq, ?filePath: string) = - let projId = ProjectId.CreateNewId() - - let docInfos = - sourceFiles - |> Seq.map (fun sourceFile -> - let isScript = isScriptFile sourceFile - let docId = DocumentId.CreateNewId(projId) - DocumentInfo.Create( - docId, - sourceFile, - filePath=sourceFile, - sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) - ) - - ProjectInfo.Create( - projId, - VersionStamp.Create(DateTime.UtcNow), - name, - assemblyName, - LanguageNames.FSharp, - documents = docInfos, - filePath = defaultArg filePath null - ) - type Document with member this.TryGetLanguageService<'T when 'T :> ILanguageService>() = match this.Project with diff --git a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs index b97acd38a4..5b659eaae2 100644 --- a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs +++ b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs @@ -201,13 +201,32 @@ type TestHostServices() = type RoslynTestHelpers private () = static member CreateDocument (filePath, text: SourceText, ?options: FSharp.Compiler.CodeAnalysis.FSharpProjectOptions) = + let isScript = String.Equals(Path.GetExtension(filePath), ".fsx", StringComparison.OrdinalIgnoreCase) + let workspace = new AdhocWorkspace(TestHostServices()) - let projFilePath = "C:\\test.fsproj" - let projInfo = ProjectInfo.CreateFSharp(projFilePath, "test.dll", Seq.empty, filePath = projFilePath) - let docInfo = DocumentInfo.CreateFSharp(projInfo.Id, filePath, loader = TextLoader.From(text.Container, VersionStamp.Create(DateTime.UtcNow))) + let projId = ProjectId.CreateNewId() + let docId = DocumentId.CreateNewId(projId) - let projInfo = projInfo.WithDocuments([docInfo]) + let docInfo = + DocumentInfo.Create( + docId, + filePath, + loader=TextLoader.From(text.Container, VersionStamp.Create(DateTime.UtcNow)), + filePath=filePath, + sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) + + let projFilePath = "C:\\test.fsproj" + let projInfo = + ProjectInfo.Create( + projId, + VersionStamp.Create(DateTime.UtcNow), + projFilePath, + "test.dll", + LanguageNames.FSharp, + documents = [docInfo], + filePath = projFilePath + ) let solutionInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Create(DateTime.UtcNow), "test.sln", [projInfo]) From fa5d33408cbe43fe349b6c57484485d2f56059d7 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 12:29:09 -0700 Subject: [PATCH 07/15] Minor change --- vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs index 5b659eaae2..7481753ec3 100644 --- a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs +++ b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs @@ -234,15 +234,15 @@ type RoslynTestHelpers private () = let workspaceService = workspace.Services.GetService() - let document = solution.GetProject(projInfo.Id).GetDocument(docInfo.Id) + let document = solution.GetProject(projId).GetDocument(docId) match options with | Some options -> let options = { options with ProjectId = Some(Guid.NewGuid().ToString()) } - workspaceService.FSharpProjectOptionsManager.SetCommandLineOptions(projInfo.Id, options.SourceFiles, options.OtherOptions |> ImmutableArray.CreateRange) + workspaceService.FSharpProjectOptionsManager.SetCommandLineOptions(projId, options.SourceFiles, options.OtherOptions |> ImmutableArray.CreateRange) document.SetFSharpProjectOptionsForTesting(options) | _ -> - workspaceService.FSharpProjectOptionsManager.SetCommandLineOptions(projInfo.Id, [|filePath|], ImmutableArray.Empty) + workspaceService.FSharpProjectOptionsManager.SetCommandLineOptions(projId, [|filePath|], ImmutableArray.Empty) document From c88dcb0a2ca0490f3797adf577161c1d6dc47955 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 12:31:18 -0700 Subject: [PATCH 08/15] Minor revert --- vsintegration/src/FSharp.Editor/Common/Extensions.fs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index 2480f8ce9a..f6f07117b5 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -39,10 +39,10 @@ type ProjectId with this.Id.ToString("D").ToLowerInvariant() type Project with + member this.IsFSharpMiscellaneous = this.Name = FSharpConstants.FSharpMiscellaneousFilesName + member this.IsFSharpMetadata = this.Name.StartsWith(FSharpConstants.FSharpMetadataName) + member this.IsFSharpMiscellaneousOrMetadata = this.IsFSharpMiscellaneous || this.IsFSharpMetadata member this.IsFSharp = this.Language = LanguageNames.FSharp - member this.IsFSharpMiscellaneous = this.IsFSharp && this.Name = FSharpConstants.FSharpMiscellaneousFilesName - member this.IsFSharpMetadata = this.IsFSharp && this.Name.StartsWith(FSharpConstants.FSharpMetadataName) - member this.IsFSharpMiscellaneousOrMetadata = this.IsFSharp && (this.IsFSharpMiscellaneous || this.IsFSharpMetadata) type Document with member this.TryGetLanguageService<'T when 'T :> ILanguageService>() = From c24e14ac0428bf1cc9406c7a8e7e26978b98bbd1 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 13:57:41 -0700 Subject: [PATCH 09/15] Added IFSharpWorkspaceProjectContext and IFSharpWorkspaceProjectContextFactory --- .../LanguageService/LanguageService.fs | 8 +- .../LanguageService/SingleFileWorkspaceMap.fs | 281 +++++++++++------- 2 files changed, 185 insertions(+), 104 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 9373b4d131..19b9fe30a3 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -261,9 +261,11 @@ type internal FSharpPackage() as this = let miscFilesWorkspace = this.ComponentModel.GetService() let _singleFileWorkspaceMap = new SingleFileWorkspaceMap( - workspace, - miscFilesWorkspace, - 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 9052a69578..d82cb40a2a 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 @@ -13,18 +14,73 @@ open Microsoft.VisualStudio.Shell.Interop open Microsoft.VisualStudio.LanguageServices open FSharp.Compiler.CodeAnalysis -type private ScriptDependencies = ResizeArray +type internal IFSharpWorkspaceProjectContext = + inherit IDisposable -[] -type internal SingleFileWorkspaceMap(workspace: Workspace, - miscFilesWorkspace: MiscellaneousFilesWorkspace, - projectContextFactory: IWorkspaceProjectContextFactory, - rdt: IVsRunningDocumentTable) as this = + abstract Id : ProjectId - // 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 + 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) + | _ -> + () + + 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 @@ -32,56 +88,68 @@ type internal SingleFileWorkspaceMap(workspace: Workspace, else SourceCodeKind.Regular - static let canUpdateScript (depSourceFiles: string []) (currentDepSourceFiles: ScriptDependencies) = - depSourceFiles.Length <> currentDepSourceFiles.Count || + 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 || ( - (currentDepSourceFiles, depSourceFiles) - ||> Seq.forall2 (fun (x: string) y -> x.Equals(y, StringComparison.OrdinalIgnoreCase)) + refSourceFiles + |> Seq.forall projectContext.HasProjectReference |> not - ) - static let createProjectContext (projectContextFactory: IWorkspaceProjectContextFactory) filePath = - let projectContext = projectContextFactory.CreateProjectContext(FSharpConstants.FSharpLanguageName, filePath, filePath, Guid.NewGuid(), null, null) - projectContext.DisplayName <- FSharpConstants.FSharpMiscellaneousFilesName - projectContext.AddSourceFile(filePath, sourceCodeKind = createSourceCodeKind filePath) - projectContext, ScriptDependencies() + let tryRemove (document: Document) = + optionsManager.ClearSingleFileOptionsCache(document.Id) + + match files.TryRemove(document.FilePath) with + | true, projectContext -> + let projIds = document.Project.Solution.GetDependentProjectIds(document.Project.Id) + if projIds.Count = 0 then + (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 depSourceFiles = scriptProjectOptions.SourceFiles |> Array.take (scriptProjectOptions.SourceFiles.Length - 1) - - lock gate (fun () -> - match files.TryGetValue(filePath) with - | true, (projectContext: IWorkspaceProjectContext, currentDepSourceFiles: ScriptDependencies) -> - if canUpdateScript depSourceFiles currentDepSourceFiles then - currentDepSourceFiles - |> Seq.iter (fun x -> - match files.TryGetValue(x) with - | true, (depProjectContext, _) -> - projectContext.RemoveProjectReference(depProjectContext) - | _ -> - () - ) - - currentDepSourceFiles.Clear() - depSourceFiles - |> Array.iter (fun filePath -> - currentDepSourceFiles.Add(filePath) - match files.TryGetValue(filePath) with - | true, (depProjectContext, _) -> - projectContext.AddProjectReference(depProjectContext, MetadataReferenceProperties.Assembly) - | _ -> - let result = createProjectContext projectContextFactory filePath - files.[filePath] <- result - let depProjectContext, _ = result - projectContext.AddProjectReference(depProjectContext, MetadataReferenceProperties.Assembly) - ) - | _ -> () - ) + 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 -> @@ -93,56 +161,82 @@ type internal SingleFileWorkspaceMap(workspace: Workspace, let filePath = document.FilePath lock gate (fun () -> if files.ContainsKey(filePath) |> not then - files.[filePath] <- createProjectContext projectContextFactory filePath + files.[filePath] <- projectContextFactory.CreateProjectContext(filePath) ) ) workspace.DocumentOpened.Add(fun args -> let document = args.Document if not document.Project.IsFSharpMiscellaneousOrMetadata then - optionsManager.ClearSingleFileOptionsCache(document.Id) - - let projectContextOpt = + if files.ContainsKey(document.FilePath) then lock gate (fun () -> - match files.TryRemove(document.FilePath) with - | true, (projectContext, _) -> - Some projectContext - | _ -> - None + tryRemove document ) - - match projectContextOpt with - | Some projectContext -> - projectContext.Dispose() - | _ -> - () ) workspace.DocumentClosed.Add(fun args -> let document = args.Document if document.Project.IsFSharpMiscellaneousOrMetadata then - optionsManager.ClearSingleFileOptionsCache(document.Id) - - let projectContextOpt = - lock gate (fun () -> - match files.TryRemove(document.FilePath) with - | true, (projectContext, _) -> - Some projectContext - | _ -> - None - ) + lock gate (fun () -> + tryRemove document + ) + ) - match projectContextOpt with - | Some projectContext -> - let projIds = document.Project.Solution.GetDependentProjectIds(document.Project.Id) - if projIds.Count = 0 then - projectContext.Dispose() - | _ -> - () + 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 -> + if proj.IsFSharpMiscellaneousOrMetadata then + match proj.Documents |> Seq.tryExactlyOne with + | Some doc when not (workspace.IsDocumentOpen(doc.Id)) -> + tryRemove doc + | _ -> + () + ) + ) + | _ -> + () ) - do - rdt.AdviseRunningDocTableEvents(this) |> ignore + 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 interface IVsRunningDocTableEvents with @@ -164,23 +258,8 @@ type internal SingleFileWorkspaceMap(workspace: Workspace, 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 projectContextFactory 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 From 634b7d981877867723da21cb102f4cd0a3058b85 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 14:51:08 -0700 Subject: [PATCH 10/15] Added basic misc file workspace test --- .../tests/UnitTests/Tests.RoslynHelpers.fs | 53 ++++++++ .../UnitTests/VisualFSharp.UnitTests.fsproj | 1 + .../UnitTests/Workspace/WorkspaceTests.fs | 124 ++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs diff --git a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs index 7481753ec3..0c6e2939f8 100644 --- a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs +++ b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs @@ -200,6 +200,59 @@ type TestHostServices() = [] type RoslynTestHelpers private () = + static member CreateProjectInfoWithSingleDocument(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, + sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) + + let projFilePath = "C:\\test.fsproj" + ProjectInfo.Create( + projId, + VersionStamp.Create(DateTime.UtcNow), + projFilePath, + "test.dll", + LanguageNames.FSharp, + documents = [docInfo], + filePath = projFilePath + ) + + static member CreateSolutionInfoWithSingleDocument(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, + sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) + + let projFilePath = "C:\\test.fsproj" + let projInfo = + ProjectInfo.Create( + projId, + VersionStamp.Create(DateTime.UtcNow), + projFilePath, + "test.dll", + LanguageNames.FSharp, + documents = [docInfo], + filePath = projFilePath + ) + + let solutionInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Create(DateTime.UtcNow), "test.sln", [projInfo]) + + solutionInfo + 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..7b96820de9 --- /dev/null +++ b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs @@ -0,0 +1,124 @@ +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 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 = + + 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) + ) + ) + + if not (solution.Workspace.TryApplyChanges(solution)) then + failwith "Unable to apply workspace changes." + + mainProj <- solution.GetProject(currentProj.Id) + + type TestFSharpWorkspaceProjectContextFactory(workspace: Workspace) = + + interface IFSharpWorkspaceProjectContextFactory with + member this.CreateProjectContext(filePath: string): IFSharpWorkspaceProjectContext = + let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath) + if not (workspace.TryApplyChanges(workspace.CurrentSolution.AddProject(projInfo))) then + failwith "Unable to apply workspace changes." + + let proj = workspace.CurrentSolution.GetProject(projInfo.Id) + new TestFSharpWorkspaceProjectContext(proj) :> IFSharpWorkspaceProjectContext + + 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 ``Script file opened in misc files workspace will get transferred to normal workspace``() = + let workspace = createWorkspace() + let miscFilesWorkspace = createMiscFileWorkspace() + let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace) + + let miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) + + let filePath = + createOnDiskScript + """ +module Script1 + +let x = 1 + """ + + try + let solutionInfo = RoslynTestHelpers.CreateSolutionInfoWithSingleDocument(filePath) + miscFilesWorkspace.AddSolution(solutionInfo) |> ignore + + let doc = + miscFilesWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath) + |> Seq.exactlyOne + |> miscFilesWorkspace.CurrentSolution.GetDocument + + Assert.IsFalse(miscFilesWorkspace.IsDocumentOpen(doc.Id)) + Assert.AreEqual(0, workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Length) + + finally + try File.Delete(filePath) with | _ -> () From 3148886fc3e0d83f2a5d9214090a2a120f0b0709 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 15:30:48 -0700 Subject: [PATCH 11/15] First workspace script test done --- .../tests/UnitTests/Tests.RoslynHelpers.fs | 29 --------------- .../UnitTests/Workspace/WorkspaceTests.fs | 36 ++++++++++++++++--- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs index 0c6e2939f8..32036ef44f 100644 --- a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs +++ b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs @@ -224,35 +224,6 @@ type RoslynTestHelpers private () = filePath = projFilePath ) - static member CreateSolutionInfoWithSingleDocument(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, - sourceCodeKind= if isScript then SourceCodeKind.Script else SourceCodeKind.Regular) - - let projFilePath = "C:\\test.fsproj" - let projInfo = - ProjectInfo.Create( - projId, - VersionStamp.Create(DateTime.UtcNow), - projFilePath, - "test.dll", - LanguageNames.FSharp, - documents = [docInfo], - filePath = projFilePath - ) - - let solutionInfo = SolutionInfo.Create(SolutionId.CreateNewId(), VersionStamp.Create(DateTime.UtcNow), "test.sln", [projInfo]) - - solutionInfo - 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/Workspace/WorkspaceTests.fs b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs index 7b96820de9..ffb61ca196 100644 --- a/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs +++ b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs @@ -7,6 +7,7 @@ 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 @@ -68,10 +69,18 @@ module WorkspaceTests = mainProj <- solution.GetProject(currentProj.Id) - type TestFSharpWorkspaceProjectContextFactory(workspace: Workspace) = + 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) + if not (miscFilesWorkspace.TryApplyChanges(miscFilesWorkspace.CurrentSolution.RemoveProject(doc.Project.Id))) then + failwith "Unable to apply workspace changes." + | _ -> + () + let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath) if not (workspace.TryApplyChanges(workspace.CurrentSolution.AddProject(projInfo))) then failwith "Unable to apply workspace changes." @@ -92,13 +101,21 @@ module WorkspaceTests = 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 ``Script file opened in misc files workspace will get transferred to normal workspace``() = let workspace = createWorkspace() let miscFilesWorkspace = createMiscFileWorkspace() - let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace) + let projectContextFactory = TestFSharpWorkspaceProjectContextFactory(workspace, miscFilesWorkspace) - let miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) + let _miscFileService = FSharpMiscellaneousFileService(workspace, miscFilesWorkspace, projectContextFactory) let filePath = createOnDiskScript @@ -109,8 +126,9 @@ let x = 1 """ try - let solutionInfo = RoslynTestHelpers.CreateSolutionInfoWithSingleDocument(filePath) - miscFilesWorkspace.AddSolution(solutionInfo) |> ignore + let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath) + if not (miscFilesWorkspace.TryApplyChanges(miscFilesWorkspace.CurrentSolution.AddProject(projInfo))) then + failwith "Unable to apply workspace changes." let doc = miscFilesWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath) @@ -120,5 +138,13 @@ let x = 1 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) + Assert.IsFalse(workspace.IsDocumentOpen(doc.Id)) + finally try File.Delete(filePath) with | _ -> () From bc96ed8796959f0423770e989ad966c32d23966b Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 16:32:18 -0700 Subject: [PATCH 12/15] Added tests for scripts referencing scripts --- .../tests/UnitTests/Tests.RoslynHelpers.fs | 6 +- .../UnitTests/Workspace/WorkspaceTests.fs | 237 +++++++++++++++--- 2 files changed, 206 insertions(+), 37 deletions(-) diff --git a/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs b/vsintegration/tests/UnitTests/Tests.RoslynHelpers.fs index 32036ef44f..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,7 +201,7 @@ type TestHostServices() = [] type RoslynTestHelpers private () = - static member CreateProjectInfoWithSingleDocument(docFilePath) = + static member CreateProjectInfoWithSingleDocument(projName, docFilePath) = let isScript = String.Equals(Path.GetExtension(docFilePath), ".fsx", StringComparison.OrdinalIgnoreCase) let projId = ProjectId.CreateNewId() @@ -211,13 +212,14 @@ type RoslynTestHelpers private () = 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), - projFilePath, + projName, "test.dll", LanguageNames.FSharp, documents = [docInfo], diff --git a/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs index ffb61ca196..e9c2bfe5fe 100644 --- a/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs +++ b/vsintegration/tests/UnitTests/Workspace/WorkspaceTests.fs @@ -22,6 +22,53 @@ 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 @@ -64,8 +111,7 @@ module WorkspaceTests = ) ) - if not (solution.Workspace.TryApplyChanges(solution)) then - failwith "Unable to apply workspace changes." + not (solution.Workspace.TryApplyChanges(solution)) |> ignore mainProj <- solution.GetProject(currentProj.Id) @@ -76,39 +122,16 @@ module WorkspaceTests = match miscFilesWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath) |> Seq.tryExactlyOne with | Some docId -> let doc = miscFilesWorkspace.CurrentSolution.GetDocument(docId) - if not (miscFilesWorkspace.TryApplyChanges(miscFilesWorkspace.CurrentSolution.RemoveProject(doc.Project.Id))) then - failwith "Unable to apply workspace changes." + removeProject miscFilesWorkspace doc.Project.Id | _ -> () - let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath) - if not (workspace.TryApplyChanges(workspace.CurrentSolution.AddProject(projInfo))) then - failwith "Unable to apply workspace changes." + let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(FSharpConstants.FSharpMiscellaneousFilesName, filePath) + addProject workspace projInfo let proj = workspace.CurrentSolution.GetProject(projInfo.Id) new TestFSharpWorkspaceProjectContext(proj) :> IFSharpWorkspaceProjectContext - 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 ``Script file opened in misc files workspace will get transferred to normal workspace``() = let workspace = createWorkspace() @@ -126,14 +149,10 @@ let x = 1 """ try - let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath) - if not (miscFilesWorkspace.TryApplyChanges(miscFilesWorkspace.CurrentSolution.AddProject(projInfo))) then - failwith "Unable to apply workspace changes." + let projInfo = RoslynTestHelpers.CreateProjectInfoWithSingleDocument(filePath, filePath) + addProject miscFilesWorkspace projInfo - let doc = - miscFilesWorkspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath) - |> Seq.exactlyOne - |> miscFilesWorkspace.CurrentSolution.GetDocument + let doc = getDocument miscFilesWorkspace filePath Assert.IsFalse(miscFilesWorkspace.IsDocumentOpen(doc.Id)) Assert.AreEqual(0, workspace.CurrentSolution.GetDocumentIdsWithFilePath(filePath).Length) @@ -144,7 +163,155 @@ let x = 1 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 | _ -> () From 10002a3e6cf7896f61e3610f320e149e56882949 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 17:02:47 -0700 Subject: [PATCH 13/15] Minor fix --- .../LanguageService/SingleFileWorkspaceMap.fs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs index d82cb40a2a..d2a13437f1 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs @@ -114,15 +114,17 @@ type internal FSharpMiscellaneousFileService(workspace: Workspace, ) let tryRemove (document: Document) = - optionsManager.ClearSingleFileOptionsCache(document.Id) - - match files.TryRemove(document.FilePath) with - | true, projectContext -> - let projIds = document.Project.Solution.GetDependentProjectIds(document.Project.Id) - if projIds.Count = 0 then - (projectContext :> IDisposable).Dispose() - | _ -> - () + 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 -> + let projIds = document.Project.Solution.GetDependentProjectIds(document.Project.Id) + if projIds.Count = 0 then + (projectContext :> IDisposable).Dispose() + | _ -> + () do optionsManager.ScriptUpdated.Add(fun scriptProjectOptions -> From 494a2d2ce839fc3caf2aebc488222b10778fc831 Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 17:03:12 -0700 Subject: [PATCH 14/15] Minor fix --- .../FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs index d2a13437f1..b3a646710e 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs @@ -120,9 +120,7 @@ type internal FSharpMiscellaneousFileService(workspace: Workspace, match files.TryRemove(document.FilePath) with | true, projectContext -> - let projIds = document.Project.Solution.GetDependentProjectIds(document.Project.Id) - if projIds.Count = 0 then - (projectContext :> IDisposable).Dispose() + (projectContext :> IDisposable).Dispose() | _ -> () From 7df1647cde3fdab0109e163947801738ea4fd80b Mon Sep 17 00:00:00 2001 From: Will Smith Date: Thu, 24 Jun 2021 17:07:02 -0700 Subject: [PATCH 15/15] Minor fix --- .../LanguageService/SingleFileWorkspaceMap.fs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs index b3a646710e..637c6dd92d 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/SingleFileWorkspaceMap.fs @@ -195,12 +195,16 @@ type internal FSharpMiscellaneousFileService(workspace: Workspace, lock gate (fun () -> projRefs |> Array.iter (fun proj -> - if proj.IsFSharpMiscellaneousOrMetadata then - match proj.Documents |> Seq.tryExactlyOne with - | Some doc when not (workspace.IsDocumentOpen(doc.Id)) -> - tryRemove doc - | _ -> - () + 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 + | _ -> + () ) ) | _ ->