From 76068c405d1da3c9e4da085d9904330c2bbfc3d1 Mon Sep 17 00:00:00 2001 From: Phillip Carter Date: Fri, 9 Aug 2019 15:22:48 -0700 Subject: [PATCH] Use GetFullPath when determining the full path in handling command line options (#7376) --- .../FSharpProjectOptionsManager.fs | 183 +++++++++++++----- 1 file changed, 139 insertions(+), 44 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 3e3944cb83f..5a2c3eed966 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -14,13 +14,42 @@ open FSharp.Compiler.SourceCodeServices open Microsoft.VisualStudio open Microsoft.VisualStudio.FSharp.Editor open Microsoft.VisualStudio.LanguageServices +open Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem +open Microsoft.VisualStudio.Shell open System.Threading -open MonoDevelop.FSharp -open MonoDevelop.Ide -open MonoDevelop.Ide.TypeSystem +open Microsoft.VisualStudio.Shell.Interop +open Microsoft.VisualStudio.LanguageServices.Implementation.TaskList +open Microsoft.CodeAnalysis.ExternalAccess.FSharp.LanguageServices [] module private FSharpProjectOptionsHelpers = + + let mapCpsProjectToSite(project:Project, cpsCommandLineOptions: IDictionary) = + let sourcePaths, referencePaths, options = + match cpsCommandLineOptions.TryGetValue(project.Id) with + | true, (sourcePaths, options) -> sourcePaths, [||], options + | false, _ -> [||], [||], [||] + let mutable errorReporter = Unchecked.defaultof<_> + { + new IProjectSite with + member __.Description = project.Name + member __.CompilationSourceFiles = sourcePaths + member __.CompilationOptions = + Array.concat [options; referencePaths |> Array.map(fun r -> "-r:" + r)] + member __.CompilationReferences = referencePaths + member site.CompilationBinOutputPath = site.CompilationOptions |> Array.tryPick (fun s -> if s.StartsWith("-o:") then Some s.[3..] else None) + member __.ProjectFileName = project.FilePath + member __.AdviseProjectSiteChanges(_,_) = () + member __.AdviseProjectSiteCleaned(_,_) = () + member __.AdviseProjectSiteClosed(_,_) = () + member __.IsIncompleteTypeCheckEnvironment = false + member __.TargetFrameworkMoniker = "" + member __.ProjectGuid = project.Id.Id.ToString() + member __.LoadTime = System.DateTime.Now + member __.ProjectProvider = None + member __.BuildErrorReporter with get () = errorReporter and set (v) = errorReporter <- v + } + let hasProjectVersionChanged (oldProject: Project) (newProject: Project) = oldProject.Version <> newProject.Version @@ -51,13 +80,13 @@ type private FSharpProjectOptionsMessage = | ClearSingleFileOptionsCache of DocumentId [] -type private FSharpProjectOptionsReactor (settings: EditorOptions, checkerProvider: FSharpCheckerProvider) = +type private FSharpProjectOptionsReactor (_workspace: VisualStudioWorkspace, settings: EditorOptions, _serviceProvider, checkerProvider: FSharpCheckerProvider) = let cancellationTokenSource = new CancellationTokenSource() // Hack to store command line options from HandleCommandLineChanges let cpsCommandLineOptions = ConcurrentDictionary() - //let legacyProjectSites = ConcurrentDictionary() + let legacyProjectSites = ConcurrentDictionary() let cache = Dictionary() let singleFileCache = Dictionary() @@ -71,22 +100,7 @@ type private FSharpProjectOptionsReactor (settings: EditorOptions, checkerProvid let! scriptProjectOptions, _ = checkerProvider.Checker.GetProjectOptionsFromScript(document.FilePath, sourceText.ToFSharpSourceText()) let projectOptions = if isScriptFile document.FilePath then - - if scriptProjectOptions.OtherOptions |> Seq.exists (fun s -> s.Contains("FSharp.Core.dll")) then scriptProjectOptions - else - // Add assemblies that may be missing in the standard assembly resolution - LoggingService.logDebug "LanguageService: GetScriptCheckerOptions: Adding missing core assemblies." - let dirs = FSharpEnvironment.getDefaultDirectories (None, FSharpTargetFramework.NET_4_5 ) - { scriptProjectOptions with - OtherOptions = - [| yield! scriptProjectOptions.OtherOptions - match FSharpEnvironment.resolveAssembly dirs "FSharp.Core" with - | Some fn -> yield String.Format ("-r:{0}", fn) - | None -> - LoggingService.logDebug "LanguageService: Resolution: FSharp.Core assembly resolution failed!" - match FSharpEnvironment.resolveAssembly dirs "FSharp.Compiler.Interactive.Settings" with - | Some fn -> yield String.Format ("-r:{0}", fn) - | None -> LoggingService.logDebug "LanguageService: Resolution: FSharp.Compiler.Interactive.Settings assembly resolution failed!" |]} + scriptProjectOptions else { ProjectFileName = document.FilePath @@ -102,7 +116,7 @@ type private FSharpProjectOptionsReactor (settings: EditorOptions, checkerProvid ExtraProjectInfo= None Stamp = Some(int64 (fileStamp.GetHashCode())) } - + checkerProvider.Checker.CheckProjectInBackground(projectOptions, userOpName="checkOptions") let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) @@ -119,8 +133,16 @@ type private FSharpProjectOptionsReactor (settings: EditorOptions, checkerProvid return Some(parsingOptions, projectOptions) } - let mdLanguageService = new MonoDevelop.FSharp.LanguageService(checkerProvider.Checker, (fun (changedfile, _) -> ()), None) - + let tryGetProjectSite (project: Project) = + // Cps + if cpsCommandLineOptions.ContainsKey project.Id then + Some (mapCpsProjectToSite(project, cpsCommandLineOptions)) + else + // Legacy + match legacyProjectSites.TryGetValue project.Id with + | true, site -> Some site + | _ -> None + let rec tryComputeOptions (project: Project) = async { let projectId = project.Id @@ -130,23 +152,68 @@ type private FSharpProjectOptionsReactor (settings: EditorOptions, checkerProvid // Because this code can be kicked off before the hack, HandleCommandLineChanges, occurs, // the command line options will not be available and we should bail if one of the project references does not give us anything. let mutable canBail = false + + let referencedProjects = ResizeArray() + + if settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences then + for projectReference in project.ProjectReferences do + let referencedProject = project.Solution.GetProject(projectReference.ProjectId) + if referencedProject.Language = FSharpConstants.FSharpLanguageName then + match! tryComputeOptions referencedProject with + | None -> canBail <- true + | Some(_, projectOptions) -> referencedProjects.Add(referencedProject.OutputFilePath, projectOptions) - let fsharpProject = MonoDevelop.Ide.IdeApp.TypeSystemService.GetMonoProject(project) :?> MonoDevelop.FSharp.FSharpProject - let! refs = fsharpProject.GetReferences(MonoDevelop.FSharp.CompilerArguments.getConfig()) |> Async.AwaitTask - canBail <- refs.Count = 0 if canBail then return None else - let projectOpts = mdLanguageService.GetProjectCheckerOptions(project.FilePath, [], refs) - return projectOpts |> Option.bind(fun opts -> - // This can happen if we didn't receive the callback from HandleCommandLineChanges yet. - if Array.isEmpty opts.SourceFiles then - None - else - checkerProvider.Checker.InvalidateConfiguration(opts, startBackgroundCompileIfAlreadySeen = false, userOpName = "computeOptions") - let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(opts) - cache.[projectId] <- (project, parsingOptions, opts) - Some(parsingOptions, opts)) + + match tryGetProjectSite project with + | None -> return None + | Some projectSite -> + + let otherOptions = + project.ProjectReferences + |> Seq.map (fun x -> "-r:" + project.Solution.GetProject(x.ProjectId).OutputFilePath) + |> Array.ofSeq + |> Array.append ( + project.MetadataReferences.OfType() + |> Seq.map (fun x -> "-r:" + x.FilePath) + |> Array.ofSeq + |> Array.append ( + // Clear any references from CompilationOptions. + // We get the references from Project.ProjectReferences/Project.MetadataReferences. + projectSite.CompilationOptions + |> Array.filter (fun x -> not (x.Contains("-r:"))) + ) + ) + + let projectOptions = + { + ProjectFileName = projectSite.ProjectFileName + ProjectId = Some(projectId.ToFSharpProjectIdString()) + SourceFiles = projectSite.CompilationSourceFiles + OtherOptions = otherOptions + ReferencedProjects = referencedProjects.ToArray() + IsIncompleteTypeCheckEnvironment = projectSite.IsIncompleteTypeCheckEnvironment + UseScriptResolutionRules = SourceFile.MustBeSingleFileProject (Path.GetFileName(project.FilePath)) + LoadTime = projectSite.LoadTime + UnresolvedReferences = None + OriginalLoadReferences = [] + ExtraProjectInfo= None + Stamp = Some(int64 (project.Version.GetHashCode())) + } + + // This can happen if we didn't receive the callback from HandleCommandLineChanges yet. + if Array.isEmpty projectOptions.SourceFiles then + return None + else + checkerProvider.Checker.InvalidateConfiguration(projectOptions, startBackgroundCompileIfAlreadySeen = false, userOpName = "computeOptions") + + let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) + + cache.[projectId] <- (project, parsingOptions, projectOptions) + + return Some(parsingOptions, projectOptions) | true, (oldProject, parsingOptions, projectOptions) -> if isProjectInvalidated oldProject project settings then @@ -165,9 +232,9 @@ type private FSharpProjectOptionsReactor (settings: EditorOptions, checkerProvid reply.Reply None else try + // For now, disallow miscellaneous workspace since we are using the hacky F# miscellaneous files project. if document.Project.Solution.Workspace.Kind = WorkspaceKind.MiscellaneousFiles then - let! options = tryComputeOptionsByFile document ct - reply.Reply options + reply.Reply None elif document.Project.Name = FSharpConstants.FSharpMiscellaneousFilesName then let! options = tryComputeOptionsByFile document ct reply.Reply options @@ -194,6 +261,7 @@ type private FSharpProjectOptionsReactor (settings: EditorOptions, checkerProvid | FSharpProjectOptionsMessage.ClearOptions(projectId) -> cache.Remove(projectId) |> ignore + legacyProjectSites.TryRemove(projectId) |> ignore | FSharpProjectOptionsMessage.ClearSingleFileOptionsCache(documentId) -> singleFileCache.Remove(documentId) |> ignore } @@ -215,8 +283,8 @@ type private FSharpProjectOptionsReactor (settings: EditorOptions, checkerProvid member __.SetCpsCommandLineOptions(projectId, sourcePaths, options) = cpsCommandLineOptions.[projectId] <- (sourcePaths, options) - member __.SetLegacyProjectSite (projectId, projectSite) = () - //legacyProjectSites.[projectId] <- projectSite + member __.SetLegacyProjectSite (projectId, projectSite) = + legacyProjectSites.[projectId] <- projectSite member __.TryGetCachedOptionsByProjectId(projectId) = match cache.TryGetValue(projectId) with @@ -239,14 +307,18 @@ type internal FSharpProjectOptionsManager [] ( checkerProvider: FSharpCheckerProvider, + [)>] workspace: VisualStudioWorkspace, + [)>] serviceProvider: System.IServiceProvider, settings: EditorOptions ) = - let reactor = new FSharpProjectOptionsReactor(settings, checkerProvider) + let projectDisplayNameOf projectFileName = + if String.IsNullOrWhiteSpace projectFileName then projectFileName + else Path.GetFileNameWithoutExtension projectFileName - do - let workspace = IdeApp.TypeSystemService.Workspace + let reactor = new FSharpProjectOptionsReactor(workspace, settings, serviceProvider, checkerProvider) + do // We need to listen to this event for lifecycle purposes. workspace.WorkspaceChanged.Add(fun args -> match args.Kind with @@ -295,4 +367,27 @@ type internal FSharpProjectOptionsManager return result |> Option.map(fun (parsingOptions, _, projectOptions) -> parsingOptions, projectOptions) } + [] + /// This handles commandline change notifications from the Dotnet Project-system + /// Prior to VS 15.7 path contained path to project file, post 15.7 contains target binpath + /// binpath is more accurate because a project file can have multiple in memory projects based on configuration + member __.HandleCommandLineChanges(path:string, sources:ImmutableArray, _references:ImmutableArray, options:ImmutableArray) = + use _logBlock = Logger.LogBlock(LogEditorFunctionId.LanguageService_HandleCommandLineArgs) + + let projectId = + match Microsoft.CodeAnalysis.ExternalAccess.FSharp.LanguageServices.FSharpVisualStudioWorkspaceExtensions.TryGetProjectIdByBinPath(workspace, path) with + | true, projectId -> projectId + | false, _ -> Microsoft.CodeAnalysis.ExternalAccess.FSharp.LanguageServices.FSharpVisualStudioWorkspaceExtensions.GetOrCreateProjectIdForPath(workspace, path, projectDisplayNameOf path) + let path = Microsoft.CodeAnalysis.ExternalAccess.FSharp.LanguageServices.FSharpVisualStudioWorkspaceExtensions.GetProjectFilePath(workspace, projectId) + + let getFullPath p = + let p' = + if Path.IsPathRooted(p) || path = null then p + else Path.Combine(Path.GetDirectoryName(path), p) + Path.GetFullPathSafe(p') + + let sourcePaths = sources |> Seq.map(fun s -> getFullPath s.Path) |> Seq.toArray + + reactor.SetCpsCommandLineOptions(projectId, sourcePaths, options.ToArray()) + member __.Checker = checkerProvider.Checker