From 4030d76acb95cb519cef0f3483e1fe4e8f0e96a3 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 2 Aug 2026 19:36:49 +0200 Subject: [PATCH] Gate expensive F# background analyzers to the active document only - Add ActiveDocumentDetection module (IVsMonitorSelection-based helper) - Gate UnusedDeclarationsAnalyzer to active document - Gate SimplifyNameDiagnosticAnalyzer to active document - Gate FSharpInlayHintsService to active document - Gate UnusedOpensDiagnosticAnalyzer to active document - Add ActiveDocumentDetection.fs to FSharp.Editor.fsproj Fixes #20114 --- .../Diagnostics/ActiveDocumentDetection.fs | 63 +++++++++++++++++++ .../SimplifyNameDiagnosticAnalyzer.fs | 5 +- .../Diagnostics/UnusedDeclarationsAnalyzer.fs | 5 +- .../UnusedOpensDiagnosticAnalyzer.fs | 9 ++- .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../Hints/FSharpInlayHintsService.fs | 8 ++- 6 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/Diagnostics/ActiveDocumentDetection.fs diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/ActiveDocumentDetection.fs b/vsintegration/src/FSharp.Editor/Diagnostics/ActiveDocumentDetection.fs new file mode 100644 index 00000000000..6b420362d94 --- /dev/null +++ b/vsintegration/src/FSharp.Editor/Diagnostics/ActiveDocumentDetection.fs @@ -0,0 +1,63 @@ +// 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 Microsoft.CodeAnalysis +open Microsoft.VisualStudio +open Microsoft.VisualStudio.Shell +open Microsoft.VisualStudio.Shell.Interop + +/// Helpers for determining whether a Roslyn Document corresponds to the document +/// currently active (focused) in the Visual Studio shell. +/// +/// Background expensive analyzers (UnusedOpens, UnusedDeclarations, SimplifyName, +/// InlayHints) should run only for the active document, mirroring how C# restricts +/// "remove unnecessary usings" and similar live analyzers. +/// +/// Roslyn's BackgroundAnalysisScope lets a host choose "open documents" or +/// "entire solution" but has no built-in "active document only" tier, so we +/// determine the truly active document ourselves via the VS shell. +[] +module internal ActiveDocumentDetection = + + /// Returns the document moniker (full file path) of the currently focused + /// editor window, or ValueNone if it cannot be determined. + let tryGetActiveDocumentMoniker (serviceProvider: IServiceProvider) : string voption = + match serviceProvider.GetService(typeof) with + | :? IVsMonitorSelection as monitorSelection -> + let mutable frameObj = null + + if + ErrorHandler.Succeeded( + monitorSelection.GetCurrentElementValue(uint32 VSConstants.VSSELELEMID.SEID_DocumentFrame, &frameObj) + ) + then + match frameObj with + | :? IVsWindowFrame as frame -> + let mutable monikerObj = null + + if + ErrorHandler.Succeeded(frame.GetProperty(int32 __VSFPROPID.VSFPROPID_pszMkDocument, &monikerObj)) + then + match monikerObj with + | :? string as moniker -> ValueSome moniker + | _ -> ValueNone + else + ValueNone + | _ -> ValueNone + else + ValueNone + | _ -> ValueNone + + /// Returns true when the given document is the currently active editor document. + /// + /// Falls back to true (= do not suppress analysis) when the active document + /// cannot be determined, so analysis is never silently lost. + let isActiveDocument (serviceProvider: IServiceProvider) (document: Document) : bool = + match document.FilePath with + | null -> true + | filePath -> + match tryGetActiveDocumentMoniker serviceProvider with + | ValueNone -> true // couldn't determine the active document, don't suppress analysis + | ValueSome activeMoniker -> String.Equals(activeMoniker, filePath, StringComparison.OrdinalIgnoreCase) diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/SimplifyNameDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/SimplifyNameDiagnosticAnalyzer.fs index 6db264c5938..387ed8145db 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/SimplifyNameDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/SimplifyNameDiagnosticAnalyzer.fs @@ -22,7 +22,9 @@ type private PerDocumentSavedData = } [)>] -type internal SimplifyNameDiagnosticAnalyzer [] () = +type internal SimplifyNameDiagnosticAnalyzer + [] + ([] serviceProvider: IServiceProvider) = static let userOpName = "SimplifyNameDiagnosticAnalyzer" static let cache = new MemoryCache("FSharp.Editor." + userOpName) @@ -37,6 +39,7 @@ type internal SimplifyNameDiagnosticAnalyzer [] () = asyncMaybe { do! Option.guard document.Project.IsFSharpCodeFixesSimplifyNameEnabled + do! Option.guard (ActiveDocumentDetection.isActiveDocument serviceProvider document) do Trace.TraceInformation("{0:n3} (start) SimplifyName", DateTime.Now.TimeOfDay.TotalSeconds) let! textVersion = document.GetTextVersionAsync(cancellationToken) let textVersionHash = textVersion.GetHashCode() diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnusedDeclarationsAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnusedDeclarationsAnalyzer.fs index 899fb151a7a..3bd20f82c0e 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnusedDeclarationsAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnusedDeclarationsAnalyzer.fs @@ -13,7 +13,9 @@ open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics open CancellableTasks [)>] -type internal UnusedDeclarationsAnalyzer [] () = +type internal UnusedDeclarationsAnalyzer + [] + ([] serviceProvider: IServiceProvider) = interface IFSharpUnusedDeclarationsDiagnosticAnalyzer with @@ -21,6 +23,7 @@ type internal UnusedDeclarationsAnalyzer [] () = if (document.Project.IsFSharpMiscellaneousOrMetadata && not document.IsFSharpScript) || not document.Project.IsFSharpCodeFixesUnusedDeclarationsEnabled + || not (ActiveDocumentDetection.isActiveDocument serviceProvider document) then Threading.Tasks.Task.FromResult(ImmutableArray.Empty) else diff --git a/vsintegration/src/FSharp.Editor/Diagnostics/UnusedOpensDiagnosticAnalyzer.fs b/vsintegration/src/FSharp.Editor/Diagnostics/UnusedOpensDiagnosticAnalyzer.fs index 79e853c32af..6204b187d31 100644 --- a/vsintegration/src/FSharp.Editor/Diagnostics/UnusedOpensDiagnosticAnalyzer.fs +++ b/vsintegration/src/FSharp.Editor/Diagnostics/UnusedOpensDiagnosticAnalyzer.fs @@ -17,7 +17,9 @@ open Microsoft.CodeAnalysis.ExternalAccess.FSharp.Diagnostics open CancellableTasks [)>] -type internal UnusedOpensDiagnosticAnalyzer [] () = +type internal UnusedOpensDiagnosticAnalyzer + [] + ([] serviceProvider: IServiceProvider) = static member GetUnusedOpenRanges(document: Document) = cancellableTask { @@ -41,7 +43,10 @@ type internal UnusedOpensDiagnosticAnalyzer [] () = interface IFSharpUnusedOpensDiagnosticAnalyzer with member _.AnalyzeSemanticsAsync(descriptor, document: Document, cancellationToken: CancellationToken) = - if document.Project.IsFSharpMiscellaneousOrMetadata && not document.IsFSharpScript then + if + (document.Project.IsFSharpMiscellaneousOrMetadata && not document.IsFSharpScript) + || not (ActiveDocumentDetection.isActiveDocument serviceProvider document) + then Tasks.Task.FromResult(ImmutableArray.Empty) else cancellableTask { diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 68206f698bd..be26292d05c 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -72,6 +72,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/Hints/FSharpInlayHintsService.fs b/vsintegration/src/FSharp.Editor/Hints/FSharpInlayHintsService.fs index 85ac8ef6edc..a20ebc3ada3 100644 --- a/vsintegration/src/FSharp.Editor/Hints/FSharpInlayHintsService.fs +++ b/vsintegration/src/FSharp.Editor/Hints/FSharpInlayHintsService.fs @@ -2,6 +2,7 @@ namespace Microsoft.VisualStudio.FSharp.Editor.Hints +open System open System.Collections.Immutable open System.ComponentModel.Composition open System.Threading.Tasks @@ -15,7 +16,10 @@ open CancellableTasks // e.g. signature hints above the line, pipeline hints on the side and so on. [)>] -type internal FSharpInlayHintsService [] (settings: EditorOptions) = +type internal FSharpInlayHintsService + [] + (settings: EditorOptions, + [] serviceProvider: IServiceProvider) = static let userOpName = "Hints" @@ -27,7 +31,7 @@ type internal FSharpInlayHintsService [] (settings: Editor else OptionParser.getHintKinds settings.Advanced - if hintKinds.IsEmpty then + if hintKinds.IsEmpty || not (ActiveDocumentDetection.isActiveDocument serviceProvider document) then Task.FromResult ImmutableArray.Empty else cancellableTask {