Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Using MiscellaneousFilesWorkspace to handle single files #5733

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,16 @@ type internal UnusedDeclarationsAnalyzer() =

do Trace.TraceInformation("{0:n3} (start) UnusedDeclarationsAnalyzer", DateTime.Now.TimeOfDay.TotalSeconds)
do! Async.Sleep DefaultTuning.UnusedDeclarationsAnalyzerInitialDelay |> liftAsync // be less intrusive, give other work priority most of the time
match getProjectInfoManager(document).TryGetOptionsForEditingDocumentOrProject(document) with
| Some (_parsingOptions, projectOptions) ->
let! sourceText = document.GetTextAsync()
let checker = getChecker document
let! _, _, checkResults = checker.ParseAndCheckDocument(document, projectOptions, sourceText = sourceText, userOpName = userOpName)
let! allSymbolUsesInFile = checkResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync
let unusedRanges = getUnusedDeclarationRanges allSymbolUsesInFile (isScriptFile document.FilePath)
return
unusedRanges
|> Seq.map (fun m -> Diagnostic.Create(Descriptor, RoslynHelpers.RangeToLocation(m, sourceText, document.FilePath)))
|> Seq.toImmutableArray
| None -> return ImmutableArray.Empty
let! _parsingOptions, projectOptions = getProjectInfoManager(document).TryGetOptionsForEditingDocumentOrProject(document)
let! sourceText = document.GetTextAsync()
let checker = getChecker document
let! _, _, checkResults = checker.ParseAndCheckDocument(document, projectOptions, sourceText = sourceText, userOpName = userOpName)
let! allSymbolUsesInFile = checkResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync
let unusedRanges = getUnusedDeclarationRanges allSymbolUsesInFile (isScriptFile document.FilePath)
return
unusedRanges
|> Seq.map (fun m -> Diagnostic.Create(Descriptor, RoslynHelpers.RangeToLocation(m, sourceText, document.FilePath)))
|> Seq.toImmutableArray
}
|> Async.map (Option.defaultValue ImmutableArray.Empty)
|> RoslynHelpers.StartAsyncAsTask cancellationToken
Expand Down
1 change: 1 addition & 0 deletions vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
<Compile Include="LanguageService\ProjectSitesAndFiles.fs" />
<Compile Include="LanguageService\ProvideFSharpVersionRegistrationAttribute.fs" />
<Compile Include="LanguageService\FSharpCheckerProvider.fs" />
<Compile Include="LanguageService\FSharpProjectOptionsCache.fs" />
<Compile Include="LanguageService\FSharpProjectOptionsManager.fs" />
<Compile Include="LanguageService\LegacyProjectWorkspaceMap.fs" />
<Compile Include="LanguageService\LanguageService.fs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type internal FSharpEditorFormattingService
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
let! options = document.GetOptionsAsync(cancellationToken) |> Async.AwaitTask
let indentStyle = options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName)
let projectOptionsOpt = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
let! projectOptionsOpt = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
let! textChange = FSharpEditorFormattingService.GetFormattingChanges(document.Id, sourceText, document.FilePath, checkerProvider.Checker, indentStyle, projectOptionsOpt, position)

return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ type internal FSharpIndentationService
let! options = document.GetOptionsAsync(cancellationToken) |> Async.AwaitTask
let tabSize = options.GetOption<int>(FormattingOptions.TabSize, FSharpConstants.FSharpLanguageName)
let indentStyle = options.GetOption(FormattingOptions.SmartIndent, FSharpConstants.FSharpLanguageName)
let projectOptionsOpt = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
let! projectOptionsOpt = projectInfoManager.TryGetOptionsForEditingDocumentOrProject document
let indent = FSharpIndentationService.GetDesiredIndentation(document.Id, sourceText, document.FilePath, lineNumber, tabSize, indentStyle, projectOptionsOpt)
return
match indent with
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.

namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Collections.Concurrent
open System.Threading.Tasks
open System.Collections.Immutable
open System.ComponentModel.Composition
open System.IO
open System.Linq
open Microsoft.CodeAnalysis
open Microsoft.FSharp.Compiler.CompileOps
open Microsoft.FSharp.Compiler.SourceCodeServices
open Microsoft.VisualStudio
open Microsoft.VisualStudio.FSharp.Editor
open Microsoft.VisualStudio.FSharp.Editor.SiteProvider
open Microsoft.VisualStudio.LanguageServices
open Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
open Microsoft.VisualStudio.Shell
open System.Threading
open System.Collections

module private FSharpProjectOptions =

/// Get the exact options for a single-file script
let computeSingleFileOptions fileName loadTime fileContents (checkerProvider: FSharpCheckerProvider) (settings: EditorOptions) projectOptionsTable serviceProvider =
async {
// NOTE: we don't use a unique stamp for single files, instead comparing options structurally.
// This is because we repeatedly recompute the options.
let extraProjectInfo = None
let optionsStamp = None
let! options, _diagnostics = checkerProvider.Checker.GetProjectOptionsFromScript(fileName, fileContents, loadTime, [| |], ?extraProjectInfo=extraProjectInfo, ?optionsStamp=optionsStamp)
// NOTE: we don't use FCS cross-project references from scripts to projects. THe projects must have been
// compiled and #r will refer to files on disk
let referencedProjectFileNames = [| |]
let site = ProjectSitesAndFiles.CreateProjectSiteForScript(fileName, referencedProjectFileNames, options)
let deps, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, site, serviceProvider, None, fileName, options.ExtraProjectInfo, Some projectOptionsTable)
let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions)
return (deps, parsingOptions, projectOptions)
}

[<RequireQualifiedAccess>]
type private FSharpProjectOptionsMessage =
| GetSingleFileOptions of Document * AsyncReplyChannel<FSharpParsingOptions * FSharpProjectOptions>

[<Sealed>]
type internal FSharpProjectOptionsCache (checkerProvider, settings, projectOptionsTable, serviceProvider) =

let cancellationTokenSource = new CancellationTokenSource()

let singleFileOptionsCache = ConcurrentDictionary<DocumentId, VersionStamp * FSharpParsingOptions * FSharpProjectOptions>()

let cacheSingleFileOptions (document: Document) =
async {
let! textVersion = document.GetTextVersionAsync() |> Async.AwaitTask
let! sourceText = document.GetTextAsync() |> Async.AwaitTask
let timeStamp = DateTime.UtcNow
let! _referencedProjectFileNames, parsingOptions, projectOptions =
FSharpProjectOptions.computeSingleFileOptions document.FilePath timeStamp (sourceText.ToString()) checkerProvider settings projectOptionsTable serviceProvider
singleFileOptionsCache.[document.Id] <- (textVersion, parsingOptions, projectOptions)
return (parsingOptions, projectOptions)
}

let tryGetSingleFileOptions (document: Document) f =
async {
let! textVersion = document.GetTextVersionAsync() |> Async.AwaitTask

match singleFileOptionsCache.TryGetValue(document.Id) with
| true, (lastTextVersion, _, _) when textVersion <> lastTextVersion ->
return! f
| false, _ ->
return! f
| true, (_, parsingOptions, projectOptions) ->
return (parsingOptions, projectOptions)
}

let loop (agent: MailboxProcessor<FSharpProjectOptionsMessage>) =
async {
while true do
try
match! agent.Receive() with
| FSharpProjectOptionsMessage.GetSingleFileOptions(document, reply) ->
let! (parsingOptions, projectOptions) = tryGetSingleFileOptions document (cacheSingleFileOptions document)
reply.Reply(parsingOptions, projectOptions)
with
| _ -> ()
}

let agent = MailboxProcessor.Start((fun agent -> loop agent), cancellationToken = cancellationTokenSource.Token)

member this.TryGetProjectOptionsAsync(document: Document) =
match projectOptionsTable.TryGetOptionsForProject(document.Project.Id) with
| Some(result) when not (isScriptFile document.FilePath) -> async { return Some(result) }
| _ ->
async {
let! (parsingOptions, projectOptions) =
tryGetSingleFileOptions document (agent.PostAndAsyncReply(fun reply -> FSharpProjectOptionsMessage.GetSingleFileOptions(document, reply)))
return Some(parsingOptions, None, projectOptions)
}

member __.ClearSingleDocumentCacheById(documentId: DocumentId) =
singleFileOptionsCache.TryRemove(documentId) |> ignore

interface IDisposable with

member __.Dispose() =
(agent :> IDisposable).Dispose()
cancellationTokenSource.Cancel()
cancellationTokenSource.Dispose()
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Microsoft.VisualStudio.FSharp.Editor

open System
open System.Collections.Concurrent
open System.Collections.Immutable
open System.ComponentModel.Composition
open System.IO
Expand All @@ -29,57 +28,35 @@ type internal FSharpProjectOptionsManager
(
checkerProvider: FSharpCheckerProvider,
[<Import(typeof<VisualStudioWorkspace>)>] workspace: VisualStudioWorkspaceImpl,
[<Import(typeof<MiscellaneousFilesWorkspace>)>] miscWorkspace: MiscellaneousFilesWorkspace,
[<Import(typeof<SVsServiceProvider>)>] serviceProvider: System.IServiceProvider,
settings: EditorOptions
) =

// A table of information about projects, excluding single-file projects.
let projectOptionsTable = FSharpProjectOptionsTable()

// 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 * FSharpParsingOptions * FSharpProjectOptions>()
let cache = new FSharpProjectOptionsCache(checkerProvider, settings, projectOptionsTable, serviceProvider)

let tryGetOrCreateProjectId (projectFileName:string) =
let projectDisplayName = projectDisplayNameOf projectFileName
Some (workspace.ProjectTracker.GetOrCreateProjectIdForPath(projectFileName, projectDisplayName))

do
workspace.DocumentClosed.Add(fun args ->
cache.ClearSingleDocumentCacheById(args.Document.Id)
)

miscWorkspace.DocumentClosed.Add(fun args ->
cache.ClearSingleDocumentCacheById(args.Document.Id)
)

/// Retrieve the projectOptionsTable
member __.FSharpOptions = projectOptionsTable

/// Clear a project from the project table
member this.ClearInfoForProject(projectId:ProjectId) = projectOptionsTable.ClearInfoForProject(projectId)

/// Clear a project from the single file project table
member this.ClearInfoForSingleFileProject(projectId) =
singleFileProjectTable.TryRemove(projectId) |> ignore

/// Update a project in the single file project table
member this.AddOrUpdateSingleFileProject(projectId, data) = singleFileProjectTable.[projectId] <- data

/// Get the exact options for a single-file script
member this.ComputeSingleFileOptions (tryGetOrCreateProjectId, fileName, loadTime, fileContents) =
async {
let extraProjectInfo = Some(box workspace)
if SourceFile.MustBeSingleFileProject(fileName) then
// NOTE: we don't use a unique stamp for single files, instead comparing options structurally.
// This is because we repeatedly recompute the options.
let optionsStamp = None
let! options, _diagnostics = checkerProvider.Checker.GetProjectOptionsFromScript(fileName, fileContents, loadTime, [| |], ?extraProjectInfo=extraProjectInfo, ?optionsStamp=optionsStamp)
// NOTE: we don't use FCS cross-project references from scripts to projects. THe projects must have been
// compiled and #r will refer to files on disk
let referencedProjectFileNames = [| |]
let site = ProjectSitesAndFiles.CreateProjectSiteForScript(fileName, referencedProjectFileNames, options)
let deps, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, site, serviceProvider, (tryGetOrCreateProjectId fileName), fileName, options.ExtraProjectInfo, Some projectOptionsTable)
let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions)
return (deps, parsingOptions, projectOptions)
else
let site = ProjectSitesAndFiles.ProjectSiteOfSingleFile(fileName)
let deps, projectOptions = ProjectSitesAndFiles.GetProjectOptionsForProjectSite(settings.LanguageServicePerformance.EnableInMemoryCrossProjectReferences, site, serviceProvider, (tryGetOrCreateProjectId fileName), fileName, extraProjectInfo, Some projectOptionsTable)
let parsingOptions, _ = checkerProvider.Checker.GetParsingOptionsFromProjectOptions(projectOptions)
return (deps, parsingOptions, projectOptions)
}

/// Update the info for a project in the project table
member this.UpdateProjectInfo(tryGetOrCreateProjectId, projectId, site, userOpName, invalidateConfig) =
Logger.Log LogEditorFunctionId.LanguageService_UpdateProjectInfo
Expand Down Expand Up @@ -107,37 +84,16 @@ type internal FSharpProjectOptionsManager

/// Get the exact options for a document or project
member this.TryGetOptionsForDocumentOrProject(document: Document) =
async {
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.
match singleFileProjectTable.TryGetValue(projectId) with
| true, (loadTime, _, _) ->
try
let fileName = document.FilePath
let! cancellationToken = Async.CancellationToken
let! sourceText = document.GetTextAsync(cancellationToken) |> Async.AwaitTask
// NOTE: we don't use FCS cross-project references from scripts to projects. The projects must have been
// compiled and #r will refer to files on disk.
let tryGetOrCreateProjectId _ = None
let! _referencedProjectFileNames, parsingOptions, projectOptions = this.ComputeSingleFileOptions (tryGetOrCreateProjectId, fileName, loadTime, sourceText.ToString())
this.AddOrUpdateSingleFileProject(projectId, (loadTime, parsingOptions, projectOptions))
return Some (parsingOptions, None, projectOptions)
with ex ->
Assert.Exception(ex)
return None
| _ -> return this.TryGetOptionsForProject(projectId)
}
cache.TryGetProjectOptionsAsync(document)

/// 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.
member this.TryGetOptionsForEditingDocumentOrProject(document:Document) =
let projectId = document.Project.Id
match singleFileProjectTable.TryGetValue(projectId) with
| true, (_loadTime, parsingOptions, originalOptions) -> Some (parsingOptions, originalOptions)
| _ -> this.TryGetOptionsForProject(projectId) |> Option.map(fun (parsingOptions, _, projectOptions) -> parsingOptions, projectOptions)
member this.TryGetOptionsForEditingDocumentOrProject(document:Document) =
async {
match! this.TryGetOptionsForDocumentOrProject(document) with
| Some(parsingOptions, _, projectOptions) -> return Some(parsingOptions, projectOptions)
| _ -> return None
}

/// get a siteprovider
member this.ProvideProjectSiteProvider(project:Project) = provideProjectSiteProvider(workspace, project, serviceProvider, Some projectOptionsTable)
Expand Down
Loading