LanguageService.fs 20.8 KB
Newer Older
1
// Copyright (c) Microsoft Corporation.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.
2

O
Omar Tawfik 已提交
3
namespace Microsoft.VisualStudio.FSharp.Editor
4

5 6
#nowarn "40"

7
open System
D
Don Syme 已提交
8
open System.Collections.Concurrent
9
open System.Collections.Generic
10
open System.ComponentModel.Composition
11
open System.Runtime.InteropServices
O
Omar Tawfik 已提交
12
open System.IO
13

D
Don Syme 已提交
14
open Microsoft.FSharp.Compiler.CompileOps
15
open Microsoft.FSharp.Compiler.SourceCodeServices
16

17
open Microsoft.CodeAnalysis
18
open Microsoft.CodeAnalysis.Diagnostics
19 20
open Microsoft.CodeAnalysis.Completion
open Microsoft.CodeAnalysis.Options
21
open Microsoft.VisualStudio
D
Don Syme 已提交
22
open Microsoft.VisualStudio.Editor
23
open Microsoft.VisualStudio.Text
D
Don Syme 已提交
24
open Microsoft.VisualStudio.TextManager.Interop
25 26
open Microsoft.VisualStudio.LanguageServices.Implementation.LanguageService
open Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
O
Omar Tawfik 已提交
27 28
open Microsoft.VisualStudio.LanguageServices.Implementation.TaskList
open Microsoft.VisualStudio.LanguageServices.ProjectSystem
29 30
open Microsoft.VisualStudio.Shell
open Microsoft.VisualStudio.Shell.Interop
O
Omar Tawfik 已提交
31
open Microsoft.VisualStudio.FSharp.LanguageService
32
open Microsoft.VisualStudio.ComponentModelHost
33

34 35
// Workaround to access non-public settings persistence type.
// GetService( ) with this will work as long as the GUID matches the real type.
O
Omar Tawfik 已提交
36
[<Guid(FSharpCommonConstants.svsSettingsPersistenceManagerGuidString)>]
37
type internal SVsSettingsPersistenceManager = class end
38

39 40 41 42 43 44 45 46 47
// Exposes FSharpChecker as MEF export
[<Export(typeof<FSharpCheckerProvider>); Composition.Shared>]
type internal FSharpCheckerProvider 
    [<ImportingConstructor>]
    (
        analyzerService: IDiagnosticAnalyzerService
    ) =
    let checker = 
        lazy
48
            let checker = FSharpChecker.Create(projectCacheSize = 200, keepAllBackgroundResolutions = false)
49 50 51 52 53 54 55

            // This is one half of the bridge between the F# background builder and the Roslyn analysis engine.
            // When the F# background builder refreshes the background semantic build context for a file,
            // we request Roslyn to reanalyze that individual file.
            checker.BeforeBackgroundFileCheck.Add(fun (fileName, extraProjectInfo) ->  
               async {
                try 
56 57
                    match extraProjectInfo with 
                    | Some (:? Workspace as workspace) -> 
58 59 60 61 62 63 64 65 66 67
                        let solution = workspace.CurrentSolution
                        let documentIds = solution.GetDocumentIdsWithFilePath(fileName)
                        if not documentIds.IsEmpty then 
                            analyzerService.Reanalyze(workspace,documentIds=documentIds)
                    | _ -> ()
                with ex -> 
                    Assert.Exception(ex)
                } |> Async.StartImmediate
            )
            checker
D
Don Syme 已提交
68

69
    member this.Checker = checker.Value
D
Don Syme 已提交
70

71 72 73 74 75
// Exposes project information as MEF component
[<Export(typeof<ProjectInfoManager>); Composition.Shared>]
type internal ProjectInfoManager 
    [<ImportingConstructor>]
    (
76 77
        checkerProvider: FSharpCheckerProvider,
        [<Import(typeof<SVsServiceProvider>)>] serviceProvider: System.IServiceProvider
78 79 80 81 82 83 84 85 86 87 88 89 90
    ) =
    // A table of information about projects, excluding single-file projects.  
    let projectTable = ConcurrentDictionary<ProjectId, FSharpProjectOptions>()

    // A table of information about single-file projects.  Currently we only need the load time of each such file, plus
    // the original options for editing
    let singleFileProjectTable = ConcurrentDictionary<ProjectId, DateTime * FSharpProjectOptions>()

    member this.AddSingleFileProject(projectId, timeStampAndOptions) =
        singleFileProjectTable.TryAdd(projectId, timeStampAndOptions) |> ignore

    member this.RemoveSingleFileProject(projectId) =
        singleFileProjectTable.TryRemove(projectId) |> ignore
D
Don Syme 已提交
91 92

    /// Clear a project from the project table
93
    member this.ClearProjectInfo(projectId: ProjectId) =
D
Don Syme 已提交
94 95 96
        projectTable.TryRemove(projectId) |> ignore
        
    /// Get the exact options for a single-file script
97
    member this.ComputeSingleFileOptions (fileName, loadTime, fileContents, workspace: Workspace) = async {
D
Don Syme 已提交
98 99
        let extraProjectInfo = Some(box workspace)
        if SourceFile.MustBeSingleFileProject(fileName) then 
100
            let! options, _diagnostics = checkerProvider.Checker.GetProjectOptionsFromScript(fileName, fileContents, loadTime, [| |], ?extraProjectInfo=extraProjectInfo) 
D
Don Syme 已提交
101
            let site = ProjectSitesAndFiles.CreateProjectSiteForScript(fileName, options)
102
            return ProjectSitesAndFiles.GetProjectOptionsForProjectSite(site,fileName,options.ExtraProjectInfo,serviceProvider)
D
Don Syme 已提交
103 104
        else
            let site = ProjectSitesAndFiles.ProjectSiteOfSingleFile(fileName)
105
            return ProjectSitesAndFiles.GetProjectOptionsForProjectSite(site,fileName,extraProjectInfo,serviceProvider)
D
Don Syme 已提交
106 107
      }

108 109 110
    /// Update the info for a project in the project table
    member this.UpdateProjectInfo(projectId: ProjectId, site: IProjectSite, workspace: Workspace) =
        let extraProjectInfo = Some(box workspace)
111
        let options = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(site, site.ProjectFileName(), extraProjectInfo, serviceProvider)
112 113 114 115 116 117 118 119 120 121 122 123 124 125
        checkerProvider.Checker.InvalidateConfiguration(options)
        projectTable.[projectId] <- options

    /// Get compilation defines relevant for syntax processing.  
    /// Quicker then TryGetOptionsForDocumentOrProject as it doesn't need to recompute the exact project 
    /// options for a script.
    member this.GetCompilationDefinesForEditingDocument(document: Document) = 
        let projectOptionsOpt = this.TryGetOptionsForProject(document.Project.Id)  
        let otherOptions = 
            match projectOptionsOpt with 
            | None -> []
            | Some options -> options.OtherOptions |> Array.toList
        CompilerEnvironment.GetCompilationDefinesForEditing(document.Name, otherOptions)

D
Don Syme 已提交
126
    /// Get the options for a project
127
    member this.TryGetOptionsForProject(projectId: ProjectId) = 
128 129 130
        match projectTable.TryGetValue(projectId) with
        | true, options -> Some options 
        | _ -> None
131

D
Don Syme 已提交
132
    /// Get the exact options for a document or project
133
    member this.TryGetOptionsForDocumentOrProject(document: Document) = async { 
D
Don Syme 已提交
134 135 136 137 138
        let projectId = document.Project.Id
        
        // The options for a single-file script project are re-requested each time the file is analyzed.  This is because the
        // single-file project may contain #load and #r references which are changing as the user edits, and we may need to re-analyze
        // to determine the latest settings.  FCS keeps a cache to help ensure these are up-to-date.
139 140
        match singleFileProjectTable.TryGetValue(projectId) with
        | true, (loadTime,_) ->
D
Don Syme 已提交
141 142 143
          try
            let fileName = document.FilePath
            let! cancellationToken = Async.CancellationToken
144
            let! sourceText = document.GetTextAsync(cancellationToken)
145
            let! options = this.ComputeSingleFileOptions (fileName, loadTime, sourceText.ToString(), document.Project.Solution.Workspace)
D
Don Syme 已提交
146 147 148 149 150
            singleFileProjectTable.[projectId] <- (loadTime, options)
            return Some options
          with ex -> 
            Assert.Exception(ex)
            return None
151
        | _ -> return this.TryGetOptionsForProject(projectId) 
D
Don Syme 已提交
152 153 154 155
     }

    /// Get the options for a document or project relevant for syntax processing.
    /// Quicker then TryGetOptionsForDocumentOrProject as it doesn't need to recompute the exact project options for a script.
156
    member this.TryGetOptionsForEditingDocumentOrProject(document: Document) = 
D
Don Syme 已提交
157
        let projectId = document.Project.Id
158 159 160
        match singleFileProjectTable.TryGetValue(projectId) with 
        | true, (_loadTime, originalOptions) -> Some originalOptions
        | _ -> this.TryGetOptionsForProject(projectId) 
161 162 163 164 165 166 167 168 169

// Used to expose FSharpChecker/ProjectInfo manager to diagnostic providers
// Diagnostic providers can be executed in environment that does not use MEF so they can rely only
// on services exposed by the workspace
type internal FSharpCheckerWorkspaceService =
    inherit Microsoft.CodeAnalysis.Host.IWorkspaceService
    abstract Checker: FSharpChecker
    abstract ProjectInfoManager: ProjectInfoManager

170 171 172 173 174 175 176 177 178 179 180 181
type internal RoamingProfileStorageLocation(keyName: string) =
    inherit OptionStorageLocation()
    
    member __.GetKeyNameForLanguage(languageName: string) =
        let unsubstitutedKeyName = keyName
 
        match languageName with
        | null -> unsubstitutedKeyName
        | _ ->
            let substituteLanguageName = if languageName = FSharpCommonConstants.FSharpLanguageName then "FSharp" else languageName
            unsubstitutedKeyName.Replace("%LANGUAGE%", substituteLanguageName)
 
182 183 184 185 186 187 188 189 190 191 192 193 194
[<Composition.Shared>]
[<Microsoft.CodeAnalysis.Host.Mef.ExportWorkspaceServiceFactory(typeof<FSharpCheckerWorkspaceService>, Microsoft.CodeAnalysis.Host.Mef.ServiceLayer.Default)>]
type internal FSharpCheckerWorkspaceServiceFactory
    [<Composition.ImportingConstructor>]
    (
        checkerProvider: FSharpCheckerProvider,
        projectInfoManager: ProjectInfoManager
    ) =
    interface Microsoft.CodeAnalysis.Host.Mef.IWorkspaceServiceFactory with
        member this.CreateService(_workspaceServices) =
            upcast { new FSharpCheckerWorkspaceService with
                member this.Checker = checkerProvider.Checker
                member this.ProjectInfoManager = projectInfoManager }
D
Don Syme 已提交
195

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
type
    [<Guid(FSharpCommonConstants.packageGuidString)>]
    [<ProvideLanguageService(languageService = typeof<FSharpLanguageService>,
                             strLanguageName = FSharpCommonConstants.FSharpLanguageName,
                             languageResourceID = 100,
                             MatchBraces = true,
                             MatchBracesAtCaret = true,
                             ShowCompletion = true,
                             ShowMatchingBrace = true,
                             ShowSmartIndent = true,
                             EnableAsyncCompletion = true,
                             QuickInfo = true,
                             DefaultToInsertSpaces = true,
                             CodeSense = true,
                             DefaultToNonHotURLs = true,
211
                             RequestStockColors = true,
212
                             EnableCommenting = true,
213 214
                             CodeSenseDelay = 100,
                             ShowDropDownOptions = true)>]
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
    internal FSharpPackage() =
    inherit AbstractPackage<FSharpPackage, FSharpLanguageService>()
    
    override this.RoslynLanguageName = FSharpCommonConstants.FSharpLanguageName

    override this.CreateWorkspace() = this.ComponentModel.GetService<VisualStudioWorkspaceImpl>()

    override this.CreateLanguageService() = 
        FSharpLanguageService(this)        

    override this.CreateEditorFactories() = Seq.empty<IVsEditorFactory>

    override this.RegisterMiscellaneousFilesWorkspaceInformation(_) = ()
    
and 
    [<Guid(FSharpCommonConstants.languageServiceGuidString)>]
    [<ProvideLanguageExtension(typeof<FSharpLanguageService>, ".fs")>]
    [<ProvideLanguageExtension(typeof<FSharpLanguageService>, ".fsi")>]
    [<ProvideLanguageExtension(typeof<FSharpLanguageService>, ".fsx")>]
    [<ProvideLanguageExtension(typeof<FSharpLanguageService>, ".fsscript")>]
    [<ProvideLanguageExtension(typeof<FSharpLanguageService>, ".ml")>]
    [<ProvideLanguageExtension(typeof<FSharpLanguageService>, ".mli")>]
    [<ProvideEditorExtension(FSharpCommonConstants.editorFactoryGuidString, ".fs", 97)>]
    [<ProvideEditorExtension(FSharpCommonConstants.editorFactoryGuidString, ".fsi", 97)>]
    [<ProvideEditorExtension(FSharpCommonConstants.editorFactoryGuidString, ".fsx", 97)>]
    [<ProvideEditorExtension(FSharpCommonConstants.editorFactoryGuidString, ".fsscript", 97)>]
    [<ProvideEditorExtension(FSharpCommonConstants.editorFactoryGuidString, ".ml", 97)>]
    [<ProvideEditorExtension(FSharpCommonConstants.editorFactoryGuidString, ".mli", 97)>]
    internal FSharpLanguageService(package : FSharpPackage) =
244
    inherit AbstractLanguageService<FSharpPackage, FSharpLanguageService>(package)
D
Don Syme 已提交
245

246
    let projectInfoManager = package.ComponentModel.DefaultExportProvider.GetExport<ProjectInfoManager>().Value
247

248 249 250 251
    let projectDisplayNameOf projectFileName = 
        if String.IsNullOrWhiteSpace projectFileName then projectFileName
        else Path.GetFileNameWithoutExtension projectFileName

252 253 254 255 256 257 258 259 260 261 262
    let singleFileProjects = ConcurrentDictionary<_, AbstractProject>()

    let tryRemoveSingleFileProject projectId =
        match singleFileProjects.TryRemove(projectId) with
        | true, project ->
            projectInfoManager.RemoveSingleFileProject(projectId)
            project.Disconnect()
        | _ -> ()

    override this.Initialize() =
        base.Initialize()
263

264 265 266 267 268 269 270 271 272 273
        this.Workspace.Options <- this.Workspace.Options.WithChangedOption(Completion.CompletionOptions.BlockForCompletionItems, FSharpCommonConstants.FSharpLanguageName, false)
        this.Workspace.Options <- this.Workspace.Options.WithChangedOption(Shared.Options.ServiceFeatureOnOffOptions.ClosedFileDiagnostic, FSharpCommonConstants.FSharpLanguageName, Nullable false)

        this.Workspace.DocumentClosed.Add <| fun args ->
            tryRemoveSingleFileProject args.Document.Project.Id 
            
        Events.SolutionEvents.OnAfterCloseSolution.Add <| fun _ ->
            singleFileProjects.Keys |> Seq.iter tryRemoveSingleFileProject

        let ctx = System.Threading.SynchronizationContext.Current
274
        
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
        let rec setupProjectsAfterSolutionOpen() =
            async {
                use 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()
                            do! Async.SwitchToContext ctx
                            this.SetupProjectFile(siteProvider, this.Workspace) }

                use _ = Events.SolutionEvents.OnAfterOpenProject |> Observable.subscribe ( fun args ->
                    match args.Hierarchy with
                    | :? IProvideProjectSite as siteProvider -> openedProjects.Post(siteProvider)
                    | _ -> () )

                do! Async.AwaitEvent Events.SolutionEvents.OnAfterCloseSolution |> Async.Ignore
                do! setupProjectsAfterSolutionOpen() 
            }
        setupProjectsAfterSolutionOpen() |> Async.StartImmediate
295 296 297

        let theme = package.ComponentModel.DefaultExportProvider.GetExport<ISetThemeColors>().Value
        theme.SetColors()
298
        
D
Don Syme 已提交
299
    /// Sync the information for the project 
300
    member this.SyncProject(project: AbstractProject, projectContext: IWorkspaceProjectContext, site: IProjectSite, forceUpdate) =
301 302 303 304
        let hashSetIgnoreCase x = new HashSet<string>(x, StringComparer.OrdinalIgnoreCase)
        let updatedFiles = site.SourceFilesOnDisk() |> hashSetIgnoreCase
        let workspaceFiles = project.GetCurrentDocuments() |> Seq.map(fun file -> file.FilePath) |> hashSetIgnoreCase

305 306 307
        // If syncing project upon some reference changes, we don't have a mechanism to recognize which references have been added/removed.
        // Hence, the current solution is to force update current project options.
        let mutable updated = forceUpdate
308 309 310 311 312 313 314 315 316 317 318
        for file in updatedFiles do
            if not(workspaceFiles.Contains(file)) then
                projectContext.AddSourceFile(file)
                updated <- true
        for file in workspaceFiles do
            if not(updatedFiles.Contains(file)) then
                projectContext.RemoveSourceFile(file)
                updated <- true

        // update the cached options
        if updated then
319
            projectInfoManager.UpdateProjectInfo(project.Id, site, project.Workspace)
320

D
Don Syme 已提交
321
    member this.SetupProjectFile(siteProvider: IProvideProjectSite, workspace: VisualStudioWorkspaceImpl) =
322 323 324
        let  rec setup (site: IProjectSite) =
            let projectGuid = Guid(site.ProjectGuid)
            let projectFileName = site.ProjectFileName()
325
            let projectDisplayName = projectDisplayNameOf projectFileName
326 327
            let projectId = workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName)

328
            if isNull (workspace.ProjectTracker.GetProject projectId) then
329
                projectInfoManager.UpdateProjectInfo(projectId, site, workspace)
330
                let projectContextFactory = package.ComponentModel.GetService<IWorkspaceProjectContextFactory>();
331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
                let errorReporter = ProjectExternalErrorReporter(projectId, "FS", this.SystemServiceProvider)
                
                let projectContext = 
                    projectContextFactory.CreateProjectContext(
                        FSharpCommonConstants.FSharpLanguageName, projectDisplayName, projectFileName, projectGuid, siteProvider, null, errorReporter)

                let project = projectContext :?> AbstractProject

                this.SyncProject(project, projectContext, site, forceUpdate=false)
                site.AdviseProjectSiteChanges(FSharpCommonConstants.FSharpLanguageServiceCallbackName, 
                                              AdviseProjectSiteChanges(fun () -> this.SyncProject(project, projectContext, site, forceUpdate=true)))
                site.AdviseProjectSiteClosed(FSharpCommonConstants.FSharpLanguageServiceCallbackName, 
                                             AdviseProjectSiteChanges(fun () -> 
                                                projectInfoManager.ClearProjectInfo(project.Id)
                                                project.Disconnect()))
                for referencedSite in ProjectSitesAndFiles.GetReferencedProjectSites (site, this.SystemServiceProvider) do
                    let referencedProjectId = setup referencedSite                    
                    project.AddProjectReference(ProjectReference referencedProjectId)
            projectId
        setup (siteProvider.GetProjectSite()) |> ignore
D
Don Syme 已提交
351 352 353

    member this.SetupStandAloneFile(fileName: string, fileContents: string, workspace: VisualStudioWorkspaceImpl, hier: IVsHierarchy) =

D
Don Syme 已提交
354
        let loadTime = DateTime.Now
355
        let options = projectInfoManager.ComputeSingleFileOptions (fileName, loadTime, fileContents, workspace) |> Async.RunSynchronously
D
Don Syme 已提交
356

357 358 359 360
        let projectFileName = fileName
        let projectDisplayName = projectDisplayNameOf projectFileName

        let projectId = workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName)
361
        projectInfoManager.AddSingleFileProject(projectId, (loadTime, options))
D
Don Syme 已提交
362

363
        if isNull (workspace.ProjectTracker.GetProject projectId) then
364
            let projectContextFactory = package.ComponentModel.GetService<IWorkspaceProjectContextFactory>();
D
Don Syme 已提交
365 366
            let errorReporter = ProjectExternalErrorReporter(projectId, "FS", this.SystemServiceProvider)

367
            let projectContext = projectContextFactory.CreateProjectContext(FSharpCommonConstants.FSharpLanguageName, projectDisplayName, projectFileName, projectId.Id, hier, null, errorReporter)
D
Don Syme 已提交
368 369 370
            projectContext.AddSourceFile(fileName)
            
            let project = projectContext :?> AbstractProject
371
            singleFileProjects.[projectId] <- project
D
Don Syme 已提交
372 373 374 375 376 377 378 379 380 381 382 383

    override this.ContentTypeName = FSharpCommonConstants.FSharpContentTypeName
    override this.LanguageName = FSharpCommonConstants.FSharpLanguageName
    override this.RoslynLanguageName = FSharpCommonConstants.FSharpLanguageName

    override this.LanguageServiceId = new Guid(FSharpCommonConstants.languageServiceGuidString)
    override this.DebuggerLanguageId = DebuggerEnvironment.GetLanguageID()

    override this.CreateContext(_,_,_,_,_) = raise(System.NotImplementedException())

    override this.SetupNewTextView(textView) =
        base.SetupNewTextView(textView)
384

385 386
        let textViewAdapter = package.ComponentModel.GetService<IVsEditorAdaptersFactoryService>()
               
D
Don Syme 已提交
387 388 389 390 391 392 393
        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)) -> 
394
                    this.SetupProjectFile(siteProvider, this.Workspace)
D
Don Syme 已提交
395
                | _ -> 
396
                    let fileContents = VsTextLines.GetFileContents(textLines, textViewAdapter)
397
                    this.SetupStandAloneFile(filename, fileContents, this.Workspace, hier)
D
Don Syme 已提交
398 399
            | _ -> ()
        | _ -> ()
D
Don Syme 已提交
400

401