From eedfcea5c3416b49f3c30bd35ed0a9dab5df95f3 Mon Sep 17 00:00:00 2001 From: Saul Rennison Date: Tue, 25 Apr 2017 17:10:53 +0100 Subject: [PATCH] Refactor single-file script loading, fix Roslyn workspace syncronisation (add support changing project references) Fixes #2916, fixes #1944 --- .../src/FSharp.Editor/FSharp.Editor.fsproj | 4 + .../LanguageService/LanguageService.fs | 214 ++++++++++-------- .../Project/AssemblyReferenceNode.cs | 2 +- .../Project/ProjectReferenceNode.cs | 2 +- .../ProjectFlagsService.fs | 67 +++--- .../ProjectSystem.fsproj | 3 + 6 files changed, 168 insertions(+), 124 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 73d9498cc3c..87a91bdae2c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -222,6 +222,10 @@ $(FSharpSourcesRoot)\..\packages\Microsoft.VisualStudio.Shell.Interop.11.0.11.0.61030\lib\Microsoft.VisualStudio.Shell.Interop.11.0.dll True + + $(FSharpSourcesRoot)\..\packages\Microsoft.VisualStudio.Shell.Interop.12.0.12.0.30110\lib\Microsoft.VisualStudio.Shell.Interop.12.0.dll + True + $(FSharpSourcesRoot)\..\packages\Microsoft.VisualStudio.TextManager.Interop.7.10.6070\lib\Microsoft.VisualStudio.TextManager.Interop.dll True diff --git a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs index 2a6b5fc25ca..297507d89a2 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/LanguageService.fs @@ -189,7 +189,7 @@ type internal FSharpCheckerWorkspaceServiceFactory upcast { new FSharpCheckerWorkspaceService with member this.Checker = checkerProvider.Checker member this.ProjectInfoManager = projectInfoManager } - + type [] [, "F#", null, "IntelliSense", "6008")>] @@ -248,65 +248,102 @@ and inherit AbstractLanguageService(package) let projectInfoManager = package.ComponentModel.DefaultExportProvider.GetExport().Value + let textViewAdapter = package.ComponentModel.GetService() + let runningDocumentTable = package.GetService() :?> IVsRunningDocumentTable4 let projectDisplayNameOf projectFileName = - if String.IsNullOrWhiteSpace projectFileName then projectFileName + if String.IsNullOrWhiteSpace projectFileName + then projectFileName else Path.GetFileNameWithoutExtension projectFileName + + let projectContexts = ConcurrentDictionary() + let singleFileProjects = ConcurrentDictionary() - let singleFileProjects = ConcurrentDictionary<_, AbstractProject>() - - let tryRemoveSingleFileProject projectId = + let tryRemoveSingleFileProjectById projectId = match singleFileProjects.TryRemove(projectId) with | true, project -> projectInfoManager.RemoveSingleFileProject(projectId) project.Disconnect() - | _ -> () + | _ -> + () + + let tryRemoveSingleFileProjectByName fileName = + singleFileProjects + |> Seq.tryFind (fun (KeyValue(_, project)) -> + project.GetCurrentDocuments() + |> Seq.exists (fun doc -> doc.FilePath.Equals(fileName, StringComparison.OrdinalIgnoreCase)) + ) + |> Option.iter (fun (KeyValue(id, _)) -> + tryRemoveSingleFileProjectById id + ) + + // Ideally we'd move this over to Roslyn's MiscellaneousFilesWorkspace, but we can't do that + // until 15.3. We can just implement the barebones of what we need with respect to file + // tracking with the running document table. + + interface IVsRunningDocTableEvents with + member this.OnAfterAttributeChange(_docCookie: uint32, _grfAttribs: uint32): int = + VSConstants.S_OK + member this.OnAfterDocumentWindowHide(_docCookie: uint32, _pFrame: IVsWindowFrame): int = + VSConstants.E_NOTIMPL + member this.OnAfterFirstDocumentLock(_docCookie: uint32, _dwRDTLockType: uint32, _dwReadLocksRemaining: uint32, _dwEditLocksRemaining: uint32): int = + VSConstants.E_NOTIMPL + member this.OnAfterSave(_docCookie: uint32): int = + VSConstants.E_NOTIMPL + member this.OnBeforeDocumentWindowShow(_docCookie: uint32, _fFirstShow: int, _pFrame: IVsWindowFrame): int = + VSConstants.E_NOTIMPL + member this.OnBeforeLastDocumentUnlock(docCookie: uint32, _dwRDTLockType: uint32, dwReadLocksRemaining: uint32, dwEditLocksRemaining: uint32): int = + if dwReadLocksRemaining + dwEditLocksRemaining = 0u then + runningDocumentTable.GetDocumentMoniker docCookie + |> tryRemoveSingleFileProjectByName + |> ignore + + VSConstants.S_OK + + interface IVsRunningDocTableEvents2 with + member this.OnAfterAttributeChange(_docCookie: uint32, _grfAttribs: uint32): int = + VSConstants.S_OK + member this.OnAfterDocumentWindowHide(_docCookie: uint32, _pFrame: IVsWindowFrame): int = + VSConstants.E_NOTIMPL + member this.OnAfterFirstDocumentLock(_docCookie: uint32, _dwRDTLockType: uint32, _dwReadLocksRemaining: uint32, _dwEditLocksRemaining: uint32): int = + VSConstants.E_NOTIMPL + member this.OnAfterSave(_docCookie: uint32): int = + VSConstants.E_NOTIMPL + member this.OnBeforeDocumentWindowShow(_docCookie: uint32, _fFirstShow: int, _pFrame: IVsWindowFrame): int = + VSConstants.E_NOTIMPL + member this.OnBeforeLastDocumentUnlock(_docCookie: uint32, _dwRDTLockType: uint32, _dwReadLocksRemaining: uint32, _dwEditLocksRemaining: uint32): int = + VSConstants.S_OK + member this.OnAfterAttributeChangeEx(docCookie: uint32, grfAttribs: uint32, _pHierOld: IVsHierarchy, _itemidOld: uint32, pszMkDocumentOld: string, _pHierNew: IVsHierarchy, _itemidNew: uint32, pszMkDocumentNew: string): int = + // Did we rename? + if grfAttribs &&& uint32 __VSRDTATTRIB.RDTA_MkDocument <> 0u then + tryRemoveSingleFileProjectByName pszMkDocumentOld + this.TrackDocument pszMkDocumentNew + + // The document is now initialized, we should try tracking it + if grfAttribs &&& uint32 __VSRDTATTRIB3.RDTA_DocumentInitialized <> 0u then + runningDocumentTable.GetDocumentMoniker docCookie + |> this.TrackDocument + + VSConstants.S_OK override this.Initialize() = base.Initialize() - + this.Workspace.Options <- this.Workspace.Options.WithChangedOption(Completion.CompletionOptions.BlockForCompletionItems, FSharpConstants.FSharpLanguageName, false) this.Workspace.Options <- this.Workspace.Options.WithChangedOption(Shared.Options.ServiceFeatureOnOffOptions.ClosedFileDiagnostic, FSharpConstants.FSharpLanguageName, Nullable false) - - this.Workspace.DocumentClosed.Add <| fun args -> - tryRemoveSingleFileProject args.Document.Project.Id + + let cookie = ref 0u + (runningDocumentTable :?> IVsRunningDocumentTable).AdviseRunningDocTableEvents(this, cookie) |> ignore - Events.SolutionEvents.OnAfterCloseSolution.Add <| fun _ -> - singleFileProjects.Keys |> Seq.iter tryRemoveSingleFileProject + Events.SolutionEvents.OnAfterOpenProject.Add <| fun args -> + match args.Hierarchy with + | :? IProvideProjectSite as siteProvider -> + this.SetupProjectFile(siteProvider, this.Workspace) + | _ -> () - let rec setupProjectsAfterSolutionOpen() = - async { - let openedProjects = MailboxProcessor.Start <| fun inbox -> - async { - // waits for AfterOpenSolution and then starts projects setup - do! Async.AwaitEvent Events.SolutionEvents.OnAfterOpenSolution |> Async.Ignore - - while true do - let! siteProvider = inbox.Receive() - this.SetupProjectFile(siteProvider, this.Workspace) - } - - // Post F# project opens to the mailbox processor - let projectOpenListener = - Events.SolutionEvents.OnAfterOpenProject - |> Observable.subscribe (fun args -> - match args.Hierarchy with - | :? IProvideProjectSite as siteProvider -> - openedProjects.Post siteProvider - | _ -> () - ) - - // Wait until the solution closes to keep listening to these events - // Then cleanup the processor and the listener and recurse to run again - do! Async.AwaitEvent Events.SolutionEvents.OnAfterCloseSolution |> Async.Ignore - - (openedProjects :> IDisposable).Dispose() - projectOpenListener.Dispose() - - do! setupProjectsAfterSolutionOpen() - } - - setupProjectsAfterSolutionOpen() |> Async.StartImmediate + Events.SolutionEvents.OnAfterCloseSolution.Add <| fun _ -> + singleFileProjects.Keys |> Seq.iter tryRemoveSingleFileProjectById + projectContexts.Clear() let theme = package.ComponentModel.DefaultExportProvider.GetExport().Value theme.SetColors() @@ -314,11 +351,13 @@ and /// Sync the information for the project member this.SyncProject(project: AbstractProject, projectContext: IWorkspaceProjectContext, site: IProjectSite, forceUpdate) = let hashSetIgnoreCase x = HashSet(x, StringComparer.OrdinalIgnoreCase) - let updatedFiles = site.SourceFilesOnDisk() |> hashSetIgnoreCase - let workspaceFiles = project.GetCurrentDocuments() |> Seq.map(fun file -> file.FilePath) |> hashSetIgnoreCase let mutable updated = forceUpdate + // Synchronise source files + let updatedFiles = site.SourceFilesOnDisk() |> hashSetIgnoreCase + let workspaceFiles = project.GetCurrentDocuments() |> Seq.map(fun file -> file.FilePath) |> hashSetIgnoreCase + for file in updatedFiles do if not(workspaceFiles.Contains(file)) then projectContext.AddSourceFile(file) @@ -328,7 +367,7 @@ and projectContext.RemoveSourceFile(file) updated <- true - (* + // Synchronise assembly references let updatedRefs = site.AssemblyReferences() |> Seq.filter (String.IsNullOrEmpty >> not) |> hashSetIgnoreCase let workspaceRefs = project.GetCurrentMetadataReferences() |> Seq.map(fun ref -> ref.FilePath) |> hashSetIgnoreCase @@ -340,39 +379,43 @@ and if not(updatedRefs.Contains(ref)) then projectContext.RemoveMetadataReference(ref) updated <- true - *) - - (* - let getProject = project.Workspace.CurrentSolution.GetProject - + // Synchronise project references + let getProjectContextByName name = + projectContexts.Values + |> Seq.find (fun context -> context.ProjectFilePath.Equals(name, StringComparison.OrdinalIgnoreCase)) + let updatedProjects = ProjectSitesAndFiles.GetReferencedProjectSites (site, this.SystemServiceProvider) |> Seq.map (fun s -> s.ProjectFileName()) |> hashSetIgnoreCase let workspaceProjects = project.GetCurrentProjectReferences() - |> Seq.map(fun ref -> (getProject ref.ProjectId).FilePath) + |> Seq.map(fun ref -> projectContexts.[ref.ProjectId].ProjectFilePath) |> hashSetIgnoreCase - for referencedSite in do - let referencedProjectId = setup referencedSite - project.AddProjectReference(ProjectReference referencedProjectId) - *) - - // update the cached options + for projectName in updatedProjects do + if not(workspaceProjects.Contains(projectName)) then + projectContext.AddProjectReference(getProjectContextByName projectName, MetadataReferenceProperties()) + updated <- true + for projectName in workspaceProjects do + if not(updatedProjects.Contains(projectName)) then + projectContext.RemoveProjectReference(getProjectContextByName projectName) + updated <- true + if updated then projectInfoManager.UpdateProjectInfo(project.Id, site, project.Workspace) member this.SetupProjectFile(siteProvider: IProvideProjectSite, workspace: VisualStudioWorkspaceImpl) = + let projectContextFactory = package.ComponentModel.GetService() + let rec setup (site: IProjectSite) = - let projectGuid = Guid(site.ProjectGuid) + let projectGuid = Guid site.ProjectGuid let projectFileName = site.ProjectFileName() let projectDisplayName = projectDisplayNameOf projectFileName let projectId = workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName) if isNull (workspace.ProjectTracker.GetProject projectId) then - let projectContextFactory = package.ComponentModel.GetService(); let errorReporter = ProjectExternalErrorReporter(projectId, "FS", this.SystemServiceProvider) let hierarchy = @@ -390,6 +433,12 @@ and FSharpConstants.FSharpLanguageName, projectDisplayName, projectFileName, projectGuid, hierarchy, null, errorReporter) let project = projectContext :?> AbstractProject + projectContexts.[projectId] <- projectContext + + // Recursively create any referenced projects. + // We need the project IDs in the projectContexts dictionary for SyncProject + ProjectSitesAndFiles.GetReferencedProjectSites (site, this.SystemServiceProvider) + |> Seq.iter setup // Syncronise the project with Roslyn and update the cached project info // if necessary. @@ -412,21 +461,15 @@ and ) ) - for referencedSite in ProjectSitesAndFiles.GetReferencedProjectSites (site, this.SystemServiceProvider) do - let referencedProjectId = setup referencedSite - project.AddProjectReference(ProjectReference referencedProjectId) - - projectId - setup (siteProvider.GetProjectSite()) |> ignore + setup (siteProvider.GetProjectSite()) member this.SetupStandAloneFile(fileName: string, fileContents: string, workspace: VisualStudioWorkspaceImpl, hier: IVsHierarchy) = - + let loadTime = DateTime.Now let options = projectInfoManager.ComputeSingleFileOptions (fileName, loadTime, fileContents, workspace) |> Async.RunSynchronously - let projectFileName = fileName let projectDisplayName = projectDisplayNameOf projectFileName - + let projectId = workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName) projectInfoManager.AddSingleFileProject(projectId, (loadTime, options)) @@ -448,24 +491,13 @@ and override this.DebuggerLanguageId = DebuggerEnvironment.GetLanguageID() override this.CreateContext(_,_,_,_,_) = raise(System.NotImplementedException()) - - override this.SetupNewTextView(textView) = - base.SetupNewTextView(textView) - - let textViewAdapter = package.ComponentModel.GetService() - - match textView.GetBuffer() with - | (VSConstants.S_OK, textLines) -> - let filename = VsTextLines.GetFilename textLines - match VsRunningDocumentTable.FindDocumentWithoutLocking(package.RunningDocumentTable,filename) with - | Some (hier, _) -> - match hier with - | :? IProvideProjectSite as siteProvider when not (IsScript(filename)) -> - this.SetupProjectFile(siteProvider, this.Workspace) - | _ -> - let fileContents = VsTextLines.GetFileContents(textLines, textViewAdapter) - this.SetupStandAloneFile(filename, fileContents, this.Workspace, hier) - | _ -> () + + member this.TrackDocument(fileName) = + match VsRunningDocumentTable.FindDocumentWithoutLocking(package.RunningDocumentTable, fileName) with + | Some (:? IProvideProjectSite as siteProvider, _) when not (IsScript fileName) -> + this.SetupProjectFile(siteProvider, this.Workspace) + | Some (hier, textLines) when IsScript fileName -> + let fileContents = VsTextLines.GetFileContents(textLines, textViewAdapter) + this.SetupStandAloneFile(fileName, fileContents, this.Workspace, hier) | _ -> () - - + \ No newline at end of file diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/AssemblyReferenceNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/AssemblyReferenceNode.cs index 78958291985..412005a79b0 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/AssemblyReferenceNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/AssemblyReferenceNode.cs @@ -76,7 +76,7 @@ public override string Url { get { - return this.myAssemblyPath; + return Path.GetFullPath(this.myAssemblyPath); } } diff --git a/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectReferenceNode.cs b/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectReferenceNode.cs index 25af68e0fc0..a438b1e264b 100644 --- a/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectReferenceNode.cs +++ b/vsintegration/src/FSharp.ProjectSystem.Base/Project/ProjectReferenceNode.cs @@ -483,7 +483,7 @@ private string GetReferencedProjectOutputPath() // build the full path adding the name of the assembly to the output path. outputPath = System.IO.Path.Combine(outputPath, assemblyName); - return outputPath; + return System.IO.Path.GetFullPath(outputPath); } diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectFlagsService.fs b/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectFlagsService.fs index 8f19e72effc..78e1a30f095 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectFlagsService.fs +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectFlagsService.fs @@ -25,43 +25,26 @@ type internal ProjectFlagsService ) = let queueLock = obj() - let projectQueue = Stack() + let projectAdded = Event() let dirtyProjects = Dictionary() /// Insert a project into the front of the queue to be processed. member __.QueueProject(project : ProjectNode) = lock queueLock <| fun () -> dirtyProjects.[project] <- true - projectQueue.Push project + projectAdded.Trigger project /// Moves a project to the front of the queue if it is already in the queue. member __.PrioritiseProject(project : ProjectNode) = lock queueLock <| fun () -> match dirtyProjects.TryGetValue project with | true, true -> - projectQueue.Push project + projectAdded.Trigger project | _ -> () member __.Clear() = lock queueLock <| fun () -> dirtyProjects.Clear() - projectQueue.Clear() - - member __.TryPop() = - lock queueLock <| fun () -> - if projectQueue.Count > 0 then - let project = projectQueue.Pop() - - // Only process the project if it is dirty. - // A project can be in the queue and clean if it was in the queue multiple times - // and it's already been processed. - if dirtyProjects.[project] then - dirtyProjects.[project] <- false - Some project - else - None - else - None member this.StartProcessing() = let processProject (project : ProjectNode) = async { @@ -106,18 +89,40 @@ type internal ProjectFlagsService async { while true do - match this.TryPop() with - | None -> () - | Some project -> - do! processProject project + use processor = MailboxProcessor.Start <| fun inbox -> async { + // Wait for AfterOpenSolution and then start processing + do! Async.AwaitEvent Events.SolutionEvents.OnAfterOpenSolution |> Async.Ignore - // Having an exclusive lock on design-time builds means the user can't initiate - // any builds themselves. Wait a few seconds before trying to grab one again. - do! Async.Sleep 2000 + while true do + let! project = inbox.Receive() + + // Set the project to clean and keep track of whether it was dirty + let wasDirty = + lock queueLock <| fun () -> + let oldDirty = dirtyProjects.[project] + dirtyProjects.[project] <- false + oldDirty + + // Only process the project if it was still dirty + if wasDirty then + do! processProject project + + // Having an exclusive lock on design-time builds means the user + // can't initiate any builds themselves. Wait a few seconds before + // trying to grab one again. + do! Async.Sleep 2000 + } + + // Post F# project opens to the mailbox processor + use _ = projectAdded.Publish |> Observable.subscribe processor.Post + + // Wait until the solution closes to keep listening to these events + // Then cleanup the processor and the listener and recurse to run again + do! Async.AwaitEvent Events.SolutionEvents.OnAfterCloseSolution |> Async.Ignore + + // Clear after solution is closed + this.Clear() } member this.Initialize() = - this.StartProcessing() |> Async.StartImmediate - - // Clear the queue when the solution is closed - Events.SolutionEvents.OnAfterCloseSolution.Add (ignore >> this.Clear) \ No newline at end of file + this.StartProcessing() |> Async.StartImmediate \ No newline at end of file diff --git a/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectSystem.fsproj b/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectSystem.fsproj index 8d60717b506..2aec0591e70 100644 --- a/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectSystem.fsproj +++ b/vsintegration/src/FSharp.ProjectSystem.FSharp/ProjectSystem.fsproj @@ -186,6 +186,9 @@ $(FSharpSourcesRoot)\..\packages\Microsoft.VisualStudio.Threading.15.0.240\lib\net45\Microsoft.VisualStudio.Threading.dll + + $(FSharpSourcesRoot)\..\packages\Microsoft.VisualStudio.ComponentModelHost.15.0.26201-alpha\lib\net46\Microsoft.VisualStudio.ComponentModelHost.dll + $(FSharpSourcesRoot)\..\packages\Microsoft.VisualFSharp.Msbuild.15.0.1.0.1\lib\net45\Microsoft.Build.Framework.dll