|
| 1 | +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. |
| 2 | + |
| 3 | +namespaceMicrosoft.VisualStudio.FSharp.Editor |
| 4 | + |
| 5 | +openSystem |
| 6 | +openSystem.Collections.Concurrent |
| 7 | +openSystem.Collections.Immutable |
| 8 | +openSystem.ComponentModel.Composition |
| 9 | +openSystem.IO |
| 10 | +openSystem.Linq |
| 11 | +openMicrosoft.CodeAnalysis |
| 12 | +openMicrosoft.FSharp.Compiler.CompileOps |
| 13 | +openMicrosoft.FSharp.Compiler.SourceCodeServices |
| 14 | +openMicrosoft.VisualStudio |
| 15 | +openMicrosoft.VisualStudio.FSharp.Editor |
| 16 | +openMicrosoft.VisualStudio.FSharp.Editor.SiteProvider |
| 17 | +openMicrosoft.VisualStudio.LanguageServices |
| 18 | +openMicrosoft.VisualStudio.LanguageServices.Implementation.ProjectSystem |
| 19 | +openMicrosoft.VisualStudio.Shell |
| 20 | + |
| 21 | +/// Exposes FCS FSharpProjectOptions information management as MEF component. |
| 22 | +// |
| 23 | +// This service allows analyzers to get an appropriate FSharpProjectOptions value for a project or single file. |
| 24 | +// It also allows a 'cheaper' route to get the project options relevant to parsing (e.g. the #define values). |
| 25 | +// The main entrypoints are TryGetOptionsForDocumentOrProject and TryGetOptionsForEditingDocumentOrProject. |
| 26 | +[<Export(typeof<FSharpProjectOptionsManager>); Composition.Shared>] |
| 27 | +typeinternalFSharpProjectOptionsManager |
| 28 | +[<ImportingConstructor>] |
| 29 | +( |
| 30 | + checkerProvider: FSharpCheckerProvider, |
| 31 | +[<Import(typeof<VisualStudioWorkspace>)>] workspace: VisualStudioWorkspaceImpl, |
| 32 | +[<Import(typeof<SVsServiceProvider>)>] serviceProvider: System.IServiceProvider, |
| 33 | + settings: EditorOptions |
| 34 | +)= |
| 35 | + |
| 36 | +// A table of information about projects, excluding single-file projects. |
| 37 | +letprojectOptionsTable= FSharpProjectOptionsTable() |
| 38 | + |
| 39 | +// A table of information about single-file projects. Currently we only need the load time of each such file, plus |
| 40 | +// the original options for editing |
| 41 | +letsingleFileProjectTable= ConcurrentDictionary<ProjectId, DateTime* FSharpParsingOptions* FSharpProjectOptions>() |
| 42 | + |
| 43 | +lettryGetOrCreateProjectId(projectFileName:string)= |
| 44 | +letprojectDisplayName= projectDisplayNameOf projectFileName |
| 45 | + Some(workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName)) |
| 46 | + |
| 47 | +/// Retrieve the projectOptionsTable |
| 48 | +member__.FSharpOptions= projectOptionsTable |
| 49 | + |
| 50 | +/// Clear a project from the project table |
| 51 | +memberthis.ClearInfoForProject(projectId:ProjectId)= projectOptionsTable.ClearInfoForProject(projectId) |
| 52 | + |
| 53 | +/// Clear a project from the single file project table |
| 54 | +memberthis.ClearInfoForSingleFileProject(projectId)= |
| 55 | + singleFileProjectTable.TryRemove(projectId)|> ignore |
| 56 | + |
| 57 | +/// Update a project in the single file project table |
| 58 | +memberthis.AddOrUpdateSingleFileProject(projectId,data)= singleFileProjectTable.[projectId]<- data |
| 59 | + |
| 60 | +/// Get the exact options for a single-file script |
| 61 | +memberthis.ComputeSingleFileOptions(tryGetOrCreateProjectId,fileName,loadTime,fileContents)= |
| 62 | +async{ |
| 63 | +letextraProjectInfo= Some(box workspace) |
| 64 | +if SourceFile.MustBeSingleFileProject(fileName)then |
| 65 | +// NOTE: we don't use a unique stamp for single files, instead comparing options structurally. |
| 66 | +// This is because we repeatedly recompute the options. |
| 67 | +letoptionsStamp= None |
| 68 | +let!options,_diagnostics= checkerProvider.Checker.GetProjectOptionsFromScript(fileName, fileContents, loadTime,[||], ?extraProjectInfo=extraProjectInfo, ?optionsStamp=optionsStamp) |
| 69 | +// NOTE: we don't use FCS cross-project references from scripts to projects. THe projects must have been |
| 70 | +// compiled and #r will refer to files on disk |
| 71 | +letreferencedProjectFileNames=[||] |
| 72 | +letsite= ProjectSitesAndFiles.CreateProjectSiteForScript(fileName, referencedProjectFileNames, options) |
| 73 | +letdeps,projectOptions= ProjectSitesAndFiles.GetProjectOptionsForProjectSite(settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, site, serviceProvider,(tryGetOrCreateProjectId fileName), fileName, options.ExtraProjectInfo, Some projectOptionsTable) |
| 74 | +letparsingOptions,_= checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) |
| 75 | +return(deps, parsingOptions, projectOptions) |
| 76 | +else |
| 77 | +letsite= ProjectSitesAndFiles.ProjectSiteOfSingleFile(fileName) |
| 78 | +letdeps,projectOptions= ProjectSitesAndFiles.GetProjectOptionsForProjectSite(settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, site, serviceProvider,(tryGetOrCreateProjectId fileName), fileName, extraProjectInfo, Some projectOptionsTable) |
| 79 | +letparsingOptions,_= checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) |
| 80 | +return(deps, parsingOptions, projectOptions) |
| 81 | +} |
| 82 | + |
| 83 | +/// Update the info for a project in the project table |
| 84 | +memberthis.UpdateProjectInfo(tryGetOrCreateProjectId,projectId,site,userOpName,invalidateConfig)= |
| 85 | + Logger.Log LogEditorFunctionId.LanguageService_UpdateProjectInfo |
| 86 | + projectOptionsTable.AddOrUpdateProject(projectId,(fun isRefresh-> |
| 87 | +letextraProjectInfo= Some(box workspace) |
| 88 | +letreferencedProjects,projectOptions= ProjectSitesAndFiles.GetProjectOptionsForProjectSite(settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, site, serviceProvider, Some(projectId), site.ProjectFileName, extraProjectInfo, Some projectOptionsTable) |
| 89 | +if invalidateConfigthen checkerProvider.Checker.InvalidateConfiguration(projectOptions, startBackgroundCompileIfAlreadySeen=not isRefresh, userOpName= userOpName+".UpdateProjectInfo") |
| 90 | +letreferencedProjectIds= referencedProjects|> Array.choose tryGetOrCreateProjectId |
| 91 | +letparsingOptions,_= checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions) |
| 92 | + referencedProjectIds, parsingOptions, Some site, projectOptions)) |
| 93 | + |
| 94 | +/// Get compilation defines relevant for syntax processing. |
| 95 | +/// Quicker then TryGetOptionsForDocumentOrProject as it doesn't need to recompute the exact project |
| 96 | +/// options for a script. |
| 97 | +memberthis.GetCompilationDefinesForEditingDocument(document:Document)= |
| 98 | +letprojectOptionsOpt= this.TryGetOptionsForProject(document.Project.Id) |
| 99 | +letparsingOptions= |
| 100 | +match projectOptionsOptwith |
| 101 | +| Some(parsingOptions,_site,_projectOptions)-> parsingOptions |
| 102 | +|_->{ FSharpParsingOptions.Defaultwith IsInteractive= IsScript document.Name} |
| 103 | + CompilerEnvironment.GetCompilationDefinesForEditing parsingOptions |
| 104 | + |
| 105 | +/// Try and get the Options for a project |
| 106 | +memberthis.TryGetOptionsForProject(projectId:ProjectId)= projectOptionsTable.TryGetOptionsForProject(projectId) |
| 107 | + |
| 108 | +/// Get the exact options for a document or project |
| 109 | +memberthis.TryGetOptionsForDocumentOrProject(document:Document)= |
| 110 | +async{ |
| 111 | +letprojectId= document.Project.Id |
| 112 | + |
| 113 | +// The options for a single-file script project are re-requested each time the file is analyzed. This is because the |
| 114 | +// single-file project may contain #load and #r references which are changing as the user edits, and we may need to re-analyze |
| 115 | +// to determine the latest settings. FCS keeps a cache to help ensure these are up-to-date. |
| 116 | +match singleFileProjectTable.TryGetValue(projectId)with |
| 117 | +|true,(loadTime,_,_)-> |
| 118 | +try |
| 119 | +letfileName= document.FilePath |
| 120 | +let!cancellationToken= Async.CancellationToken |
| 121 | +let!sourceText= document.GetTextAsync(cancellationToken)|> Async.AwaitTask |
| 122 | +// NOTE: we don't use FCS cross-project references from scripts to projects. The projects must have been |
| 123 | +// compiled and #r will refer to files on disk. |
| 124 | +lettryGetOrCreateProjectId _= None |
| 125 | +let!_referencedProjectFileNames,parsingOptions,projectOptions= this.ComputeSingleFileOptions(tryGetOrCreateProjectId, fileName, loadTime, sourceText.ToString()) |
| 126 | + this.AddOrUpdateSingleFileProject(projectId,(loadTime, parsingOptions, projectOptions)) |
| 127 | +return Some(parsingOptions, None, projectOptions) |
| 128 | +with ex-> |
| 129 | + Assert.Exception(ex) |
| 130 | +return None |
| 131 | +|_->return this.TryGetOptionsForProject(projectId) |
| 132 | +} |
| 133 | + |
| 134 | +/// Get the options for a document or project relevant for syntax processing. |
| 135 | +/// Quicker then TryGetOptionsForDocumentOrProject as it doesn't need to recompute the exact project options for a script. |
| 136 | +memberthis.TryGetOptionsForEditingDocumentOrProject(document:Document)= |
| 137 | +letprojectId= document.Project.Id |
| 138 | +match singleFileProjectTable.TryGetValue(projectId)with |
| 139 | +|true,(_loadTime, parsingOptions, originalOptions)-> Some(parsingOptions, originalOptions) |
| 140 | +|_-> this.TryGetOptionsForProject(projectId)|> Option.map(fun(parsingOptions,_,projectOptions)-> parsingOptions, projectOptions) |
| 141 | + |
| 142 | +/// get a siteprovider |
| 143 | +memberthis.ProvideProjectSiteProvider(project:Project)= provideProjectSiteProvider(workspace, project, serviceProvider, Some projectOptionsTable) |
| 144 | + |
| 145 | +/// Tell the checker to update the project info for the specified project id |
| 146 | +memberthis.UpdateProjectInfoWithProjectId(projectId:ProjectId,userOpName,invalidateConfig)= |
| 147 | +lethier= workspace.GetHierarchy(projectId) |
| 148 | +match hierwith |
| 149 | +|null->() |
| 150 | +| hwhen(h.IsCapabilityMatch("CPS"))-> |
| 151 | +letproject= workspace.CurrentSolution.GetProject(projectId) |
| 152 | +ifnot(isNull project)then |
| 153 | +letsiteProvider= this.ProvideProjectSiteProvider(project) |
| 154 | +letprojectSite= siteProvider.GetProjectSite() |
| 155 | +if projectSite.CompilationSourceFiles.Length<>0then |
| 156 | + this.UpdateProjectInfo(tryGetOrCreateProjectId, projectId, projectSite, userOpName, invalidateConfig) |
| 157 | +|_->() |
| 158 | + |
| 159 | +/// Tell the checker to update the project info for the specified project id |
| 160 | +memberthis.UpdateDocumentInfoWithProjectId(projectId:ProjectId,documentId:DocumentId,userOpName,invalidateConfig)= |
| 161 | +if workspace.IsDocumentOpen(documentId)then |
| 162 | + this.UpdateProjectInfoWithProjectId(projectId, userOpName, invalidateConfig) |
| 163 | + |
| 164 | +[<Export>] |
| 165 | +/// This handles commandline change notifications from the Dotnet Project-system |
| 166 | +/// Prior to VS 15.7 path contained path to project file, post 15.7 contains target binpath |
| 167 | +/// binpath is more accurate because a project file can have multiple in memory projects based on configuration |
| 168 | +memberthis.HandleCommandLineChanges(path:string,sources:ImmutableArray<CommandLineSourceFile>,references:ImmutableArray<CommandLineReference>,options:ImmutableArray<string>)= |
| 169 | +use _logBlock= Logger.LogBlock(LogEditorFunctionId.LanguageService_HandleCommandLineArgs) |
| 170 | + |
| 171 | +letprojectId= |
| 172 | +match workspace.ProjectTracker.TryGetProjectByBinPath(path)with |
| 173 | +|true, project-> project.Id |
| 174 | +|false,_-> workspace.ProjectTracker.GetOrCreateProjectIdForPath(path, projectDisplayNameOf path) |
| 175 | +letproject= workspace.ProjectTracker.GetProject(projectId) |
| 176 | +letpath= project.ProjectFilePath |
| 177 | +letfullPath p= |
| 178 | +if Path.IsPathRooted(p)|| path=nullthen p |
| 179 | +else Path.Combine(Path.GetDirectoryName(path), p) |
| 180 | +letsourcePaths= sources|> Seq.map(fun s-> fullPath s.Path)|> Seq.toArray |
| 181 | +letreferencePaths= references|> Seq.map(fun r-> fullPath r.Reference)|> Seq.toArray |
| 182 | + |
| 183 | + projectOptionsTable.SetOptionsWithProjectId(projectId, sourcePaths, referencePaths, options.ToArray()) |
| 184 | + this.UpdateProjectInfoWithProjectId(projectId,"HandleCommandLineChanges", invalidateConfig=true) |
| 185 | + |
| 186 | +member__.Checker= checkerProvider.Checker |