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