diff --git a/src/fsharp/service/service.fs b/src/fsharp/service/service.fs index 8e1e2654f6ac..ff209e2d7778 100644 --- a/src/fsharp/service/service.fs +++ b/src/fsharp/service/service.fs @@ -1760,6 +1760,7 @@ type UnresolvedReferencesSet = UnresolvedReferencesSet of UnresolvedAssemblyRefe type FSharpProjectOptions = { ProjectFileName: string + ProjectId: string option SourceFiles: string[] OtherOptions: string[] ReferencedProjects: (string * FSharpProjectOptions)[] @@ -1774,7 +1775,11 @@ type FSharpProjectOptions = member x.ProjectOptions = x.OtherOptions /// Whether the two parse options refer to the same project. static member UseSameProjectFileName(options1,options2) = - options1.ProjectFileName = options2.ProjectFileName + match options1.ProjectId, options2.ProjectId with + | Some(projectId1), Some(projectId2) -> projectId1 = projectId2 + | _ -> + options1.ProjectFileName = options2.ProjectFileName && + options1.ProjectId = options2.ProjectId /// Compare two options sets with respect to the parts of the options that are important to building. static member AreSameForChecking(options1,options2) = @@ -2660,7 +2665,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC let execWithReactorAsync action = reactor.EnqueueAndAwaitOpAsync(userOpName, "ParseAndCheckFileInProject", filename, action) async { try - let strGuid = "_" + Guid.NewGuid().ToString() + let strGuid = "_ProjectId=" + options.ProjectId.ToString() Logger.LogBlockMessageStart (filename + strGuid) LogCompilerFunctionId.Service_ParseAndCheckFileInProject if implicitlyStartBackgroundWork then @@ -2832,6 +2837,7 @@ type BackgroundCompiler(legacyReferenceResolver, projectCacheSize, keepAssemblyC let options = { ProjectFileName = filename + ".fsproj" // Make a name that is unique in this directory. + ProjectId = None SourceFiles = loadClosure.SourceFiles |> List.map fst |> List.toArray OtherOptions = otherFlags ReferencedProjects= [| |] @@ -3184,6 +3190,7 @@ type FSharpChecker(legacyReferenceResolver, projectCacheSize, keepAssemblyConten member ic.GetProjectOptionsFromCommandLineArgs(projectFileName, argv, ?loadedTimeStamp, ?extraProjectInfo: obj) = let loadedTimeStamp = defaultArg loadedTimeStamp DateTime.MaxValue // Not 'now', we don't want to force reloading { ProjectFileName = projectFileName + ProjectId = None SourceFiles = [| |] // the project file names will be inferred from the ProjectOptions OtherOptions = argv ReferencedProjects= [| |] diff --git a/src/fsharp/service/service.fsi b/src/fsharp/service/service.fsi index 072d2813b846..9356eab84941 100755 --- a/src/fsharp/service/service.fsi +++ b/src/fsharp/service/service.fsi @@ -309,6 +309,9 @@ type public FSharpProjectOptions = // Note that this may not reduce to just the project directory, because there may be two projects in the same directory. ProjectFileName: string + /// This is the unique identifier for the project. If it's None, will key off of ProjectFileName in our caching. + ProjectId: string option + /// The files in the project SourceFiles: string[] diff --git a/vsintegration/Utils/LanguageServiceProfiling/Options.fs b/vsintegration/Utils/LanguageServiceProfiling/Options.fs index 2d9a26aec97f..cf9001814423 100644 --- a/vsintegration/Utils/LanguageServiceProfiling/Options.fs +++ b/vsintegration/Utils/LanguageServiceProfiling/Options.fs @@ -196,6 +196,7 @@ let FCS (repositoryDir: string) : Options = { Options = {ProjectFileName = repositoryDir @"src\fsharp\FSharp.Compiler.Private\FSharp.Compiler.Private.fsproj" + ProjectId = None SourceFiles = files |> Array.map (fun x -> repositoryDir x) OtherOptions = [|@"-o:obj\Release\FSharp.Compiler.Private.dll"; "-g"; "--noframework"; @@ -301,6 +302,7 @@ let FCS (repositoryDir: string) : Options = let VFPT (repositoryDir: string) : Options = { Options = {ProjectFileName = repositoryDir @"src\FSharp.Editing\FSharp.Editing.fsproj" + ProjectId = None SourceFiles = [|@"src\FSharp.Editing\AssemblyInfo.fs"; @"src\FSharp.Editing\Common\Utils.fs"; diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index 4446dbd04068..b1ca7f032543 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -5,6 +5,7 @@ module internal Microsoft.VisualStudio.FSharp.Editor.Extensions open System open System.IO +open System.Collections.Immutable open Microsoft.CodeAnalysis open Microsoft.FSharp.Compiler.Ast open Microsoft.FSharp.Compiler.SourceCodeServices @@ -24,6 +25,34 @@ type System.IServiceProvider with member x.GetService<'T>() = x.GetService(typeof<'T>) :?> 'T member x.GetService<'S, 'T>() = x.GetService(typeof<'S>) :?> 'T +type Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.AbstractProject with + + member this.GetCurrentProjectReferenceIds() = + this.GetCurrentProjectReferences() + |> Seq.map (fun x -> x.ProjectId) + |> ImmutableArray.ToImmutableArray + +type Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.VisualStudioWorkspaceImpl with + + member this.GetCurrentProjectReferenceIds(projectId: ProjectId) = + match this.ProjectTracker.GetProject(projectId) with + | null -> ImmutableArray.Empty + | project -> project.GetCurrentProjectReferenceIds() + + /// Tries to get the project file path based on the project id. + member this.TryGetProjectFilePath(projectId: ProjectId) = + match this.ProjectTracker.GetProject(projectId) with + | null -> None + | project -> + let filePath = project.ProjectFilePath + if String.IsNullOrWhiteSpace(filePath) then None + else Some(filePath) + + /// Gets a project file path. Throws if there is no project file path. + member this.GetProjectFilePath(projectId) = + match this.TryGetProjectFilePath(projectId) with + | None -> failwithf "Can't find project file path from %A." projectId + | Some(filePath) -> filePath type FSharpNavigationDeclarationItem with member x.RoslynGlyph : Glyph = diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 37f973c5c6c8..13f678c199b1 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -129,10 +129,6 @@ type internal FSharpProjectOptionsManager // the original options for editing let singleFileProjectTable = ConcurrentDictionary() - let tryGetOrCreateProjectId (projectFileName:string) = - let projectDisplayName = projectDisplayNameOf projectFileName - Some (workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName)) - /// Retrieve the projectOptionsTable member __.FSharpOptions = projectOptionsTable @@ -147,38 +143,43 @@ type internal FSharpProjectOptionsManager member this.AddOrUpdateSingleFileProject(projectId, data) = singleFileProjectTable.[projectId] <- data /// Get the exact options for a single-file script - member this.ComputeSingleFileOptions (tryGetOrCreateProjectId, fileName, loadTime, fileContents) = + member this.ComputeSingleFileOptions (projectId, loadTime, fileContents) = + let filePath = workspace.GetProjectFilePath(projectId) + let deps = workspace.GetCurrentProjectReferenceIds(projectId) |> Seq.toArray + async { let extraProjectInfo = Some(box workspace) - let tryGetOptionsForReferencedProject f = f |> tryGetOrCreateProjectId |> Option.bind this.TryGetOptionsForProject |> Option.map(fun (_, _, projectOptions) -> projectOptions) - if SourceFile.MustBeSingleFileProject(fileName) then + let tryGetOptionsForReferencedProject _f = None + if SourceFile.MustBeSingleFileProject(filePath) then // NOTE: we don't use a unique stamp for single files, instead comparing options structurally. // This is because we repeatedly recompute the options. let optionsStamp = None - let! options, _diagnostics = checkerProvider.Checker.GetProjectOptionsFromScript(fileName, fileContents, loadTime, [| |], ?extraProjectInfo=extraProjectInfo, ?optionsStamp=optionsStamp) + let! options, _diagnostics = checkerProvider.Checker.GetProjectOptionsFromScript(filePath, fileContents, loadTime, [| |], ?extraProjectInfo=extraProjectInfo, ?optionsStamp=optionsStamp) // NOTE: we don't use FCS cross-project references from scripts to projects. THe projects must have been // compiled and #r will refer to files on disk - let referencedProjectFileNames = [| |] - let site = ProjectSitesAndFiles.CreateProjectSiteForScript(fileName, referencedProjectFileNames, options) - let deps, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(Settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, site, serviceProvider, (tryGetOrCreateProjectId fileName), fileName, options.ExtraProjectInfo, Some projectOptionsTable) + let referencedProjectFileNames = [| |] + let site = ProjectSitesAndFiles.CreateProjectSiteForScript(filePath, referencedProjectFileNames, options) + let _deps, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(Settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, site, serviceProvider, Some projectId, filePath, options.ExtraProjectInfo, Some projectOptionsTable) let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) return (deps, parsingOptions, projectOptions) else - let site = ProjectSitesAndFiles.ProjectSiteOfSingleFile(fileName) - let deps, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(Settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, site, serviceProvider, (tryGetOrCreateProjectId fileName), fileName, extraProjectInfo, Some projectOptionsTable) + let site = ProjectSitesAndFiles.ProjectSiteOfSingleFile(filePath) + let _deps, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(Settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, site, serviceProvider, Some projectId, filePath, extraProjectInfo, Some projectOptionsTable) let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) return (deps, parsingOptions, projectOptions) } /// Update the info for a project in the project table - member this.UpdateProjectInfo(tryGetOrCreateProjectId, projectId, site, userOpName, invalidateConfig) = - Logger.LogMessage ("InvalidateConfig=" + invalidateConfig.ToString()) LogEditorFunctionId.LanguageService_UpdateProjectInfo + member this.UpdateProjectInfo(projectId, site, userOpName, invalidateConfig) = + Logger.Log(LogEditorFunctionId.LanguageService_UpdateProjectInfo) + + let referencedProjectIds = workspace.GetCurrentProjectReferenceIds(projectId) |> Seq.toArray + projectOptionsTable.AddOrUpdateProject(projectId, (fun isRefresh -> let extraProjectInfo = Some(box workspace) - let tryGetOptionsForReferencedProject f = f |> tryGetOrCreateProjectId |> Option.bind this.TryGetOptionsForProject |> Option.map(fun (_, _, projectOptions) -> projectOptions) - let referencedProjects, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(Settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, site, serviceProvider, (tryGetOrCreateProjectId (site.ProjectFileName)), site.ProjectFileName, extraProjectInfo, Some projectOptionsTable) + let tryGetOptionsForReferencedProject _f = None + let _referencedProjects, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(Settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, site, serviceProvider, Some projectId, site.ProjectFileName, extraProjectInfo, Some projectOptionsTable) if invalidateConfig then checkerProvider.Checker.InvalidateConfiguration(projectOptions, startBackgroundCompileIfAlreadySeen = not isRefresh, userOpName = userOpName + ".UpdateProjectInfo") - let referencedProjectIds = referencedProjects |> Array.choose tryGetOrCreateProjectId let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) referencedProjectIds, parsingOptions, Some site, projectOptions)) @@ -207,13 +208,11 @@ type internal FSharpProjectOptionsManager match singleFileProjectTable.TryGetValue(projectId) with | true, (loadTime, _, _) -> try - let fileName = document.FilePath let! cancellationToken = Async.CancellationToken let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask // NOTE: we don't use FCS cross-project references from scripts to projects. The projects must have been // compiled and #r will refer to files on disk. - let tryGetOrCreateProjectId _ = None - let! _referencedProjectFileNames, parsingOptions, projectOptions = this.ComputeSingleFileOptions (tryGetOrCreateProjectId, fileName, loadTime, sourceText.ToString()) + let! _referencedProjectFileNames, parsingOptions, projectOptions = this.ComputeSingleFileOptions (projectId, loadTime, sourceText.ToString()) this.AddOrUpdateSingleFileProject(projectId, (loadTime, parsingOptions, projectOptions)) return Some (parsingOptions, None, projectOptions) with ex -> @@ -244,7 +243,7 @@ type internal FSharpProjectOptionsManager let siteProvider = this.ProvideProjectSiteProvider(project) let projectSite = siteProvider.GetProjectSite() if projectSite.CompilationSourceFiles.Length <> 0 then - this.UpdateProjectInfo(tryGetOrCreateProjectId, projectId, projectSite, userOpName, invalidateConfig) + this.UpdateProjectInfo(projectId, projectSite, userOpName, invalidateConfig) | _ -> () /// Tell the checker to update the project info for the specified project id @@ -419,14 +418,11 @@ type internal FSharpLanguageService(package : FSharpPackage) = let invalidPathChars = set (Path.GetInvalidPathChars()) let isPathWellFormed (path: string) = not (String.IsNullOrWhiteSpace path) && path |> Seq.forall (fun c -> not (Set.contains c invalidPathChars)) - let tryGetOrCreateProjectId (workspace: VisualStudioWorkspaceImpl) (projectFileName: string) = - let projectDisplayName = projectDisplayNameOf projectFileName - Some (workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName)) - let optionsAssociation = ConditionalWeakTable() member private this.OnProjectAdded(projectId:ProjectId) = projectInfoManager.UpdateProjectInfoWithProjectId(projectId, "OnProjectAdded", invalidateConfig=true) member private this.OnProjectReloaded(projectId:ProjectId) = projectInfoManager.UpdateProjectInfoWithProjectId(projectId, "OnProjectReloaded", invalidateConfig=true) + member private this.OnProjectRemoved(projectId) = projectInfoManager.ClearInfoForProject(projectId) member private this.OnDocumentAdded(projectId:ProjectId, documentId:DocumentId) = projectInfoManager.UpdateDocumentInfoWithProjectId(projectId, documentId, "OnDocumentAdded", invalidateConfig=true) member private this.OnDocumentReloaded(projectId:ProjectId, documentId:DocumentId) = projectInfoManager.UpdateDocumentInfoWithProjectId(projectId, documentId, "OnDocumentReloaded", invalidateConfig=true) @@ -438,10 +434,10 @@ type internal FSharpLanguageService(package : FSharpPackage) = match args.Kind with | WorkspaceChangeKind.ProjectAdded -> this.OnProjectAdded(args.ProjectId) | WorkspaceChangeKind.ProjectReloaded -> this.OnProjectReloaded(args.ProjectId) + | WorkspaceChangeKind.ProjectRemoved -> this.OnProjectRemoved(args.ProjectId) | WorkspaceChangeKind.DocumentAdded -> this.OnDocumentAdded(args.ProjectId, args.DocumentId) | WorkspaceChangeKind.DocumentReloaded -> this.OnDocumentReloaded(args.ProjectId, args.DocumentId) | WorkspaceChangeKind.DocumentRemoved - | WorkspaceChangeKind.ProjectRemoved | WorkspaceChangeKind.AdditionalDocumentAdded | WorkspaceChangeKind.AdditionalDocumentReloaded | WorkspaceChangeKind.AdditionalDocumentRemoved @@ -556,7 +552,7 @@ type internal FSharpLanguageService(package : FSharpPackage) = // update the cached options if updated then - projectInfoManager.UpdateProjectInfo(tryGetOrCreateProjectId workspace, project.Id, site, userOpName + ".SyncProject", invalidateConfig=true) + projectInfoManager.UpdateProjectInfo(project.Id, site, userOpName + ".SyncProject", invalidateConfig=true) member this.SetupProjectFile(siteProvider: IProvideProjectSite, workspace: VisualStudioWorkspaceImpl, userOpName) = let userOpName = userOpName + ".SetupProjectFile" @@ -619,7 +615,7 @@ type internal FSharpLanguageService(package : FSharpPackage) = let projectDisplayName = projectDisplayNameOf projectFileName let projectId = workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName) - let _referencedProjectFileNames, parsingOptions, projectOptions = projectInfoManager.ComputeSingleFileOptions (tryGetOrCreateProjectId workspace, fileName, loadTime, fileContents) |> Async.RunSynchronously + let _referencedProjectFileNames, parsingOptions, projectOptions = projectInfoManager.ComputeSingleFileOptions (projectId, loadTime, fileContents) |> Async.RunSynchronously projectInfoManager.AddOrUpdateSingleFileProject(projectId, (loadTime, parsingOptions, projectOptions)) if isNull (workspace.ProjectTracker.GetProject projectId) then diff --git a/vsintegration/src/FSharp.Editor/LanguageService/ProjectSitesAndFiles.fs b/vsintegration/src/FSharp.Editor/LanguageService/ProjectSitesAndFiles.fs index bebf24a0c9e5..f9c52701224c 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/ProjectSitesAndFiles.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/ProjectSitesAndFiles.fs @@ -274,6 +274,7 @@ type internal ProjectSitesAndFiles() = let option = let newOption () = { ProjectFileName = projectSite.ProjectFileName + ProjectId = projectId |> Option.map (fun x -> x.Id.ToString()) SourceFiles = projectSite.CompilationSourceFiles OtherOptions = projectSite.CompilationOptions ReferencedProjects = referencedProjectOptions diff --git a/vsintegration/src/FSharp.LanguageService/FSharpSource.fs b/vsintegration/src/FSharp.LanguageService/FSharpSource.fs index 079f2d93934c..3146f5d0ba1d 100644 --- a/vsintegration/src/FSharp.LanguageService/FSharpSource.fs +++ b/vsintegration/src/FSharp.LanguageService/FSharpSource.fs @@ -358,6 +358,7 @@ type internal FSharpSource_DEPRECATED(service:LanguageService_DEPRECATED, textLi // get a sync parse of the file let co, _ = { ProjectFileName = fileName + ".dummy.fsproj" + ProjectId = None SourceFiles = [| fileName |] OtherOptions = flags ReferencedProjects = [| |] diff --git a/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs b/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs index 89539b6180eb..bf82290b2ece 100644 --- a/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs +++ b/vsintegration/src/FSharp.LanguageService/ProjectSitesAndFiles.fs @@ -315,6 +315,7 @@ type internal ProjectSitesAndFiles() = let option = let newOption () = { ProjectFileName = projectSite.ProjectFileName + ProjectId = None SourceFiles = projectSite.CompilationSourceFiles OtherOptions = projectSite.CompilationOptions ReferencedProjects = referencedProjectOptions