11// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
22
3- namespace Microsoft.VisualStudio.FSharp.Editor
3+ namespace rec Microsoft.VisualStudio.FSharp.Editor
44
55#nowarn " 40"
66
77open System
88open System.Collections .Concurrent
99open System.Collections .Generic
10+ open System.Collections .Immutable
1011open System.ComponentModel .Composition
12+ open System.Diagnostics
13+ open System.IO
14+ open System.Linq
1115open System.Runtime .CompilerServices
1216open System.Runtime .InteropServices
13- open System.IO
14- open System.Diagnostics
17+ open System.Threading
1518
1619open Microsoft.FSharp .Compiler .CompileOps
1720open Microsoft.FSharp .Compiler .SourceCodeServices
21+ open Microsoft.FSharp .Compiler .AbstractIL .Internal .Library
1822
1923open Microsoft.CodeAnalysis
2024open Microsoft.CodeAnalysis .Diagnostics
@@ -23,6 +27,7 @@ open Microsoft.CodeAnalysis.Options
2327open Microsoft.VisualStudio
2428open Microsoft.VisualStudio .Editor
2529open Microsoft.VisualStudio .TextManager .Interop
30+ open Microsoft.VisualStudio .LanguageServices
2631open Microsoft.VisualStudio .LanguageServices .Implementation .LanguageService
2732open Microsoft.VisualStudio .LanguageServices .Implementation .ProjectSystem
2833open Microsoft.VisualStudio .LanguageServices .Implementation .TaskList
@@ -78,7 +83,6 @@ type internal FSharpCheckerProvider
7883 member this.Checker = checker.Value
7984
8085
81-
8286/// A value and a function to recompute/refresh the value. The function is passed a flag indicating if a refresh is happening.
8387type Refreshable < 'T > = 'T * ( bool -> 'T)
8488
@@ -87,22 +91,33 @@ type Refreshable<'T> = 'T * (bool -> 'T)
8791// This service allows analyzers to get an appropriate FSharpProjectOptions value for a project or single file.
8892// It also allows a 'cheaper' route to get the project options relevant to parsing (e.g. the #define values).
8993// The main entrypoints are TryGetOptionsForDocumentOrProject and TryGetOptionsForEditingDocumentOrProject.
90-
91-
9294[<Export( typeof< FSharpProjectOptionsManager>); Composition.Shared>]
9395type internal FSharpProjectOptionsManager
9496 [<ImportingConstructor>]
9597 (
9698 checkerProvider: FSharpCheckerProvider,
99+ [< Import( typeof< VisualStudioWorkspace>)>] workspace: VisualStudioWorkspaceImpl,
97100 [< Import( typeof< SVsServiceProvider>)>] serviceProvider: System.IServiceProvider
98101 ) =
99- // A table of information about projects, excluding single-file projects.
102+
103+ // A table of information about projects, excluding single-file projects.
100104 let projectTable = ConcurrentDictionary< ProjectId, Refreshable< ProjectId[] * FSharpProjectOptions>>()
101105
102106 // A table of information about single-file projects. Currently we only need the load time of each such file, plus
103107 // the original options for editing
104108 let singleFileProjectTable = ConcurrentDictionary< ProjectId, DateTime * FSharpProjectOptions>()
105109
110+ // Accumulate sources and references for each project file
111+ let projectInfo = new ConcurrentDictionary< string, string[]* string[]* string[]>()
112+
113+ let projectDisplayNameOf projectFileName =
114+ if String.IsNullOrWhiteSpace projectFileName then projectFileName
115+ else Path.GetFileNameWithoutExtension projectFileName
116+
117+ let tryGetOrCreateProjectId ( projectFileName : string ) =
118+ let projectDisplayName = projectDisplayNameOf projectFileName
119+ Some ( workspace.ProjectTracker.GetOrCreateProjectIdForPath( projectFileName, projectDisplayName))
120+
106121 /// Clear a project from the project table
107122 member this.ClearInfoForProject ( projectId : ProjectId ) =
108123 projectTable.TryRemove( projectId) |> ignore
@@ -124,7 +139,7 @@ type internal FSharpProjectOptionsManager
124139
125140 member this.AddOrUpdateSingleFileProject ( projectId , data ) =
126141 singleFileProjectTable.[ projectId] <- data
127-
142+
128143 /// Get the exact options for a single-file script
129144 member this.ComputeSingleFileOptions ( tryGetOrCreateProjectId , fileName , loadTime , fileContents , workspace : Workspace ) = async {
130145 let extraProjectInfo = Some( box workspace)
@@ -145,15 +160,15 @@ type internal FSharpProjectOptionsManager
145160 }
146161
147162 /// Update the info for a project in the project table
148- member this.UpdateProjectInfo ( tryGetOrCreateProjectId , projectId : ProjectId , site : IProjectSite , workspace : Workspace , userOpName ) =
163+ member this.UpdateProjectInfo ( tryGetOrCreateProjectId , projectId : ProjectId , site : IProjectSite , userOpName ) =
149164 this.AddOrUpdateProject( projectId, ( fun isRefresh ->
150165 let extraProjectInfo = Some( box workspace)
151166 let tryGetOptionsForReferencedProject f = f |> tryGetOrCreateProjectId |> Option.bind this.TryGetOptionsForProject
152167 let referencedProjects , options = ProjectSitesAndFiles.GetProjectOptionsForProjectSite( Settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, tryGetOptionsForReferencedProject, site, site.ProjectFileName(), extraProjectInfo, serviceProvider, true )
153168 let referencedProjectIds = referencedProjects |> Array.choose tryGetOrCreateProjectId
154169 checkerProvider.Checker.InvalidateConfiguration( options, startBackgroundCompileIfAlreadySeen = not isRefresh, userOpName= userOpName + " .UpdateProjectInfo" )
155170 referencedProjectIds, options))
156-
171+
157172 /// Get compilation defines relevant for syntax processing.
158173 /// Quicker then TryGetOptionsForDocumentOrProject as it doesn't need to recompute the exact project
159174 /// options for a script.
@@ -204,6 +219,84 @@ type internal FSharpProjectOptionsManager
204219 | true , (_ loadTime, originalOptions) -> Some originalOptions
205220 | _ -> this.TryGetOptionsForProject( projectId)
206221
222+ member this.ProvideProjectSiteProvider ( project : Project ) =
223+ let hier = workspace.GetHierarchy( project.Id)
224+
225+ { new IProvideProjectSite with
226+ member iProvideProjectSite.GetProjectSite () =
227+ let fst ( a , _ , _ ) = a
228+ let thrd ( _ , _ , c ) = c
229+ let mutable errorReporter =
230+ let reporter = ProjectExternalErrorReporter( project.Id, " FS" , serviceProvider)
231+ Some( reporter:> Microsoft .VisualStudio .Shell .Interop .IVsLanguageServiceBuildErrorReporter2 )
232+
233+ { new Microsoft.VisualStudio.FSharp.LanguageService.IProjectSite with
234+ member __.SourceFilesOnDisk () = this.GetProjectInfo( project.FilePath) |> fst
235+ member __.DescriptionOfProject () = project.Name
236+ member __.CompilerFlags () =
237+ let _ , references , options = this.GetProjectInfo( project.FilePath)
238+ Array.concat [ options; references |> Array.map( fun r -> " -r:" + r)]
239+ member __.ProjectFileName () = project.FilePath
240+ member __.AdviseProjectSiteChanges ( _ , _ ) = ()
241+ member __.AdviseProjectSiteCleaned ( _ , _ ) = ()
242+ member __.AdviseProjectSiteClosed ( _ , _ ) = ()
243+ member __.IsIncompleteTypeCheckEnvironment = false
244+ member __.TargetFrameworkMoniker = " "
245+ member __.ProjectGuid = project.Id.Id.ToString()
246+ member __.LoadTime = System.DateTime.Now
247+ member __.ProjectProvider = Some iProvideProjectSite
248+ member __.AssemblyReferences () = this.GetProjectInfo( project.FilePath) |> thrd
249+ member __.BuildErrorReporter with get () = errorReporter and
250+ set ( v ) = errorReporter <- v
251+ }
252+
253+ // TODO: figure out why this is necessary
254+ interface IVsHierarchy with
255+ member __.SetSite ( psp ) = hier.SetSite( psp)
256+ member __.GetSite ( psp ) = hier.GetSite( ref psp)
257+ member __.QueryClose ( pfCanClose ) = hier.QueryClose( ref pfCanClose)
258+ member __.Close () = hier.Close()
259+ member __.GetGuidProperty ( itemid , propid , pguid ) = hier.GetGuidProperty( itemid, propid, ref pguid)
260+ member __.SetGuidProperty ( itemid , propid , rguid ) = hier.SetGuidProperty( itemid, propid, ref rguid)
261+ member __.GetProperty ( itemid , propid , pvar ) = hier.GetProperty( itemid, propid, ref pvar)
262+ member __.SetProperty ( itemid , propid , var ) = hier.SetProperty( itemid, propid, var)
263+ member __.GetNestedHierarchy ( itemid , iidHierarchyNested , ppHierarchyNested , pitemidNested ) = hier.GetNestedHierarchy( itemid, ref iidHierarchyNested, ref ppHierarchyNested, ref pitemidNested)
264+ member __.GetCanonicalName ( itemid , pbstrName ) = hier.GetCanonicalName( itemid, ref pbstrName)
265+ member __.ParseCanonicalName ( pszName , pitemid ) = hier.ParseCanonicalName( pszName, ref pitemid)
266+ member __.Unused0 () = hier.Unused0()
267+ member __.AdviseHierarchyEvents ( pEventSink , pdwCookie ) = hier.AdviseHierarchyEvents( pEventSink, ref pdwCookie)
268+ member __.UnadviseHierarchyEvents ( dwCookie ) = hier.UnadviseHierarchyEvents( dwCookie)
269+ member __.Unused1 () = hier.Unused1()
270+ member __.Unused2 () = hier.Unused2()
271+ member __.Unused3 () = hier.Unused3()
272+ member __.Unused4 () = hier.Unused4()
273+ }
274+
275+ member this.UpdateProjectInfoWithProjectId ( projectId : ProjectId , userOpName ) =
276+ let project = workspace.CurrentSolution.GetProject( projectId)
277+ let siteProvider = this.ProvideProjectSiteProvider( project)
278+ this.UpdateProjectInfo( tryGetOrCreateProjectId, projectId, siteProvider.GetProjectSite(), userOpName)
279+
280+ member this.UpdateProjectInfoWithPath ( path , userOpName ) =
281+ let projectId = workspace.ProjectTracker.GetOrCreateProjectIdForPath( path, projectDisplayNameOf path)
282+ this.UpdateProjectInfoWithProjectId( projectId, userOpName)
283+
284+ [<Export>]
285+ /// This handles commandline change notifications from the Dotnet Project-system
286+ member this.HandleCommandLineChanges ( path : string , sources : ImmutableArray < CommandLineSourceFile >, references : ImmutableArray < CommandLineReference >, options : ImmutableArray < string >) =
287+ let fullPath p =
288+ if Path.IsPathRooted( p) then p
289+ else Path.Combine( Path.GetDirectoryName( path), p)
290+ let sourcePaths = sources |> Seq.map( fun s -> fullPath s.Path) |> Seq.toArray
291+ let referencePaths = references |> Seq.map( fun r -> fullPath r.Reference) |> Seq.toArray
292+ projectInfo.[ path] <- ( sourcePaths, referencePaths, options.ToArray())
293+ this.UpdateProjectInfoWithPath( path, " HandleCommandLineChanges" )
294+
295+ member __.GetProjectInfo ( path : string ) =
296+ match projectInfo.TryGetValue path with
297+ | true , value -> value
298+ | _ -> [||], [||], [||]
299+
207300// Used to expose FSharpChecker/ProjectInfo manager to diagnostic providers
208301// Diagnostic providers can be executed in environment that does not use MEF so they can rely only
209302// on services exposed by the workspace
@@ -217,7 +310,6 @@ type internal RoamingProfileStorageLocation(keyName: string) =
217310
218311 member __.GetKeyNameForLanguage ( languageName : string ) =
219312 let unsubstitutedKeyName = keyName
220-
221313 match languageName with
222314 | null -> unsubstitutedKeyName
223315 | _ ->
@@ -266,21 +358,15 @@ type
266358
267359 override this.Initialize () =
268360 base .Initialize()
269- //initialize settings
270361 this.ComponentModel.GetService< SettingsPersistence.ISettings>() |> ignore
271362
272363 override this.RoslynLanguageName = FSharpConstants.FSharpLanguageName
273-
274364 override this.CreateWorkspace () = this.ComponentModel.GetService< VisualStudioWorkspaceImpl>()
275-
276- override this.CreateLanguageService () =
277- FSharpLanguageService( this)
278-
365+ override this.CreateLanguageService () = FSharpLanguageService( this)
279366 override this.CreateEditorFactories () = Seq.empty< IVsEditorFactory>
280-
281367 override this.RegisterMiscellaneousFilesWorkspaceInformation ( _ ) = ()
282-
283- and
368+
369+ type
284370 [<Guid( FSharpConstants.languageServiceGuidString) >]
285371 [<ProvideLanguageExtension( typeof< FSharpLanguageService>, " .fs" ) >]
286372 [<ProvideLanguageExtension( typeof< FSharpLanguageService>, " .fsi" ) >]
299385
300386 let projectInfoManager = package.ComponentModel.DefaultExportProvider.GetExport< FSharpProjectOptionsManager>() .Value
301387
302- let projectDisplayNameOf projectFileName =
388+ let projectDisplayNameOf projectFileName =
303389 if String.IsNullOrWhiteSpace projectFileName then projectFileName
304390 else Path.GetFileNameWithoutExtension projectFileName
305391
@@ -321,15 +407,20 @@ and
321407
322408 let optionsAssociation = ConditionalWeakTable< IWorkspaceProjectContext, string[]>()
323409
410+ member private this.OnProjectAdded ( projectId : ProjectId , _newSolution : Solution ) = projectInfoManager.UpdateProjectInfoWithProjectId( projectId, " OnProjectAdded" )
324411 override this.Initialize () =
325412 base .Initialize()
326413
414+ let workspaceChanged ( args : WorkspaceChangeEventArgs ) =
415+ match args.Kind with
416+ | WorkspaceChangeKind.ProjectAdded -> this.OnProjectAdded( args.ProjectId, args.NewSolution)
417+ | _ -> ()
418+
327419 this.Workspace.Options <- this.Workspace.Options.WithChangedOption( Completion.CompletionOptions.BlockForCompletionItems, FSharpConstants.FSharpLanguageName, false )
328420 this.Workspace.Options <- this.Workspace.Options.WithChangedOption( Shared.Options.ServiceFeatureOnOffOptions.ClosedFileDiagnostic, FSharpConstants.FSharpLanguageName, Nullable false )
421+ this.Workspace.WorkspaceChanged.Add( workspaceChanged)
422+ this.Workspace.DocumentClosed.Add <| fun args -> tryRemoveSingleFileProject args.Document.Project.Id
329423
330- this.Workspace.DocumentClosed.Add <| fun args ->
331- tryRemoveSingleFileProject args.Document.Project.Id
332-
333424 Events.SolutionEvents.OnAfterCloseSolution.Add <| fun _ ->
334425 //checkerProvider.Checker.StopBackgroundCompile()
335426
342433 singleFileProjects.Keys |> Seq.iter tryRemoveSingleFileProject
343434
344435 let ctx = System.Threading.SynchronizationContext.Current
345-
436+
346437 let rec setupProjectsAfterSolutionOpen () =
347438 async {
348439 use openedProjects = MailboxProcessor.Start <| fun inbox ->
@@ -368,27 +459,27 @@ and
368459
369460 let theme = package.ComponentModel.DefaultExportProvider.GetExport< ISetThemeColors>() .Value
370461 theme.SetColors()
371-
462+
372463 /// Sync the information for the project
373- member this .SyncProject( project : AbstractProject , projectContext : IWorkspaceProjectContext , site : IProjectSite , workspace , forceUpdate , userOpName ) =
464+ member __ .SyncProject( project : AbstractProject , projectContext : IWorkspaceProjectContext , site : IProjectSite , workspace , forceUpdate , userOpName ) =
374465 let wellFormedFilePathSetIgnoreCase ( paths : seq < string >) =
375466 HashSet( paths |> Seq.filter isPathWellFormed |> Seq.map ( fun s -> try System.IO.Path.GetFullPath( s) with _ -> s), StringComparer.OrdinalIgnoreCase)
376467
377468 let updatedFiles = site.SourceFilesOnDisk() |> wellFormedFilePathSetIgnoreCase
378469 let originalFiles = project.GetCurrentDocuments() |> Seq.map ( fun file -> file.FilePath) |> wellFormedFilePathSetIgnoreCase
379-
470+
380471 let mutable updated = forceUpdate
381472
382473 for file in updatedFiles do
383474 if not ( originalFiles.Contains( file)) then
384475 projectContext.AddSourceFile( file)
385476 updated <- true
386-
477+
387478 for file in originalFiles do
388479 if not ( updatedFiles.Contains( file)) then
389480 projectContext.RemoveSourceFile( file)
390481 updated <- true
391-
482+
392483 let updatedRefs = site.AssemblyReferences() |> wellFormedFilePathSetIgnoreCase
393484 let originalRefs = project.GetCurrentMetadataReferences() |> Seq.map ( fun ref -> ref.FilePath) |> wellFormedFilePathSetIgnoreCase
394485
423514
424515 // update the cached options
425516 if updated then
426- projectInfoManager.UpdateProjectInfo( tryGetOrCreateProjectId workspace, project.Id, site, project.Workspace , userOpName + " .SyncProject" )
517+ projectInfoManager.UpdateProjectInfo( tryGetOrCreateProjectId workspace, project.Id, site, userOpName + " .SyncProject" )
427518
428519 member this.SetupProjectFile ( siteProvider : IProvideProjectSite , workspace : VisualStudioWorkspaceImpl , userOpName ) =
429520 let userOpName = userOpName + " .SetupProjectFile"
435526 let projectId = workspace.ProjectTracker.GetOrCreateProjectIdForPath( projectFileName, projectDisplayName)
436527
437528 if isNull ( workspace.ProjectTracker.GetProject projectId) then
438- projectInfoManager.UpdateProjectInfo( tryGetOrCreateProjectId workspace, projectId, site, workspace , userOpName)
529+ projectInfoManager.UpdateProjectInfo( tryGetOrCreateProjectId workspace, projectId, site, userOpName)
439530 let projectContextFactory = package.ComponentModel.GetService< IWorkspaceProjectContextFactory>();
440531 let errorReporter = ProjectExternalErrorReporter( projectId, " FS" , this.SystemServiceProvider)
441532
474565 setup ( siteProvider.GetProjectSite()) |> ignore
475566
476567 member this.SetupStandAloneFile ( fileName : string , fileContents : string , workspace : VisualStudioWorkspaceImpl , hier : IVsHierarchy ) =
477-
478568 let loadTime = DateTime.Now
479569 let projectFileName = fileName
480570 let projectDisplayName = projectDisplayNameOf projectFileName
@@ -506,19 +596,30 @@ and
506596 base .SetupNewTextView( textView)
507597
508598 let textViewAdapter = package.ComponentModel.GetService< IVsEditorAdaptersFactoryService>()
509-
599+
510600 match textView.GetBuffer() with
511601 | ( VSConstants.S_ OK, textLines) ->
512602 let filename = VsTextLines.GetFilename textLines
603+
604+ // CPS projects don't implement IProvideProjectSite and IVSProjectHierarchy
605+ // Simple explanation:
606+ // Legacy projects have IVSHierarchy and IPRojectSite
607+ // CPS Projects and loose script files don't
513608 match VsRunningDocumentTable.FindDocumentWithoutLocking( package.RunningDocumentTable, filename) with
514609 | Some ( hier, _) ->
515610 match hier with
516- | :? IProvideProjectSite as siteProvider when not ( IsScript( filename)) ->
611+ | :? IProvideProjectSite as siteProvider when not ( IsScript( filename)) ->
517612 this.SetupProjectFile( siteProvider, this.Workspace, " SetupNewTextView" )
518- | _ ->
613+ | _ when not ( IsScript( filename)) ->
614+ let docId = this.Workspace.CurrentSolution.GetDocumentIdsWithFilePath( filename) .FirstOrDefault()
615+ match docId with
616+ | null ->
617+ let fileContents = VsTextLines.GetFileContents( textLines, textViewAdapter)
618+ this.SetupStandAloneFile( filename, fileContents, this.Workspace, hier)
619+ | id ->
620+ projectInfoManager.UpdateProjectInfoWithProjectId( id.ProjectId, " SetupNewTextView" )
621+ | _ ->
519622 let fileContents = VsTextLines.GetFileContents( textLines, textViewAdapter)
520623 this.SetupStandAloneFile( filename, fileContents, this.Workspace, hier)
521624 | _ -> ()
522625 | _ -> ()
523-
524-
0 commit comments