From ac832b43d20d66bdea4acf5b59ffd5b30170159c Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 12:18:34 +0200 Subject: [PATCH 1/6] Read a script's caret without waiting for the UI thread The project options reactor looked up the caret itself through ServiceProvider.GlobalProvider, the RDT, IVsTextView and an IVsTextViewEvents connection point, all of which need the UI thread. When the UI thread synchronously waited on project options (breakpoint validation when a document frame is shown), the reactor waited for the UI thread and the UI thread for the reactor. An IWpfTextViewCreationListener now publishes the caret of the focused editor into the text buffer's properties, and the reactor only reads it. Only scripts look for it: FCS uses the caret only to skip the `#r "nuget: ..."` line being typed. Fixes #20522 Co-Authored-By: Claude Opus 5 (1M context) --- .../src/FSharp.Editor/Common/Extensions.fs | 86 ------------------- .../src/FSharp.Editor/FSharp.Editor.fsproj | 1 + .../FSharpProjectOptionsManager.fs | 59 +++++-------- .../LanguageService/FocusedCaret.fs | 77 +++++++++++++++++ 4 files changed, 101 insertions(+), 122 deletions(-) create mode 100644 vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs diff --git a/vsintegration/src/FSharp.Editor/Common/Extensions.fs b/vsintegration/src/FSharp.Editor/Common/Extensions.fs index f9695e68ecf..9d872a47cac 100644 --- a/vsintegration/src/FSharp.Editor/Common/Extensions.fs +++ b/vsintegration/src/FSharp.Editor/Common/Extensions.fs @@ -7,15 +7,9 @@ open System open System.IO open System.Collections.Immutable open System.Collections.Generic -open System.Runtime.InteropServices open System.Threading open System.Threading.Tasks -open Microsoft.VisualStudio -open Microsoft.VisualStudio.Shell -open Microsoft.VisualStudio.Shell.Interop -open Microsoft.VisualStudio.TextManager.Interop - open Microsoft.CodeAnalysis open Microsoft.CodeAnalysis.Text open Microsoft.CodeAnalysis.Host @@ -25,10 +19,6 @@ open FSharp.Compiler.Syntax open FSharp.Compiler.Text open Microsoft.VisualStudio.FSharp.Editor -open Microsoft.VisualStudio.Editor -open Microsoft.VisualStudio.Text.Editor -open Microsoft.VisualStudio -open Microsoft.VisualStudio.OLE.Interop type private FSharpGlyph = FSharp.Compiler.EditorServices.FSharpGlyph type private FSharpRoslynGlyph = Microsoft.CodeAnalysis.ExternalAccess.FSharp.FSharpGlyph @@ -69,56 +59,6 @@ type Project with member this.IsFSharp = this.Language = LanguageNames.FSharp -type TextViewEventsHandler - ( - onChangeCaretHandler: (IVsTextView * int * int -> unit) option, - onKillFocus: (IVsTextView -> unit) option, - onSetFocus: (IVsTextView -> unit) option - ) = - interface IVsTextViewEvents with - member this.OnChangeCaretLine(view: IVsTextView, newline: int, oldline: int) = - onChangeCaretHandler - |> Option.iter (fun handler -> handler (view, newline, oldline)) - - member this.OnChangeScrollInfo - (_view: IVsTextView, _iBar: int, _iMinUnit: int, _iMaxUnits: int, _iVisibleUnits: int, _iFirstVisibleUnit: int) - = - () - - member this.OnKillFocus(view: IVsTextView) = - onKillFocus |> Option.iter (fun handler -> handler (view)) - - member this.OnSetBuffer(_view: IVsTextView, _buffer: IVsTextLines) = () - - member this.OnSetFocus(view: IVsTextView) = - onSetFocus |> Option.iter (fun handler -> handler (view)) - -type ConnectionPointSubscription = System.IDisposable option - -// Usage example: -// If a handler is None, to not handle that event -// let subscription = subscribeToTextViewEvents (textView, onChangeCaretHandler, onKillFocus, onSetFocus) -// Unsubscribe using subscription.Dispose() -let subscribeToTextViewEvents (textView: IVsTextView, onChangeCaretHandler, onKillFocus, onSetFocus) : ConnectionPointSubscription = - let handler = TextViewEventsHandler(onChangeCaretHandler, onKillFocus, onSetFocus) - - match textView with - | :? IConnectionPointContainer as cpContainer -> - let riid = typeof.GUID - let mutable cookie = 0u - - match cpContainer.FindConnectionPoint(ref riid) with - | null -> None - | cp -> - Some( - cp.Advise(handler, &cookie) - - { new IDisposable with - member _.Dispose() = cp.Unadvise(cookie) - } - ) - | _ -> None - type Document with member this.TryGetLanguageService<'T when 'T :> ILanguageService>() = @@ -129,32 +69,6 @@ type Document with | null -> None | languageServices -> languageServices.GetService<'T>() |> Some - member this.TryGetIVsTextView() : IVsTextView option = - match ServiceProvider.GlobalProvider.GetService(typeof) with - | :? IVsTextManager as textManager -> - // Grab IVsRunningDocumentTable - match ServiceProvider.GlobalProvider.GetService(typeof) with - | :? IVsRunningDocumentTable as rdt -> - match rdt.FindAndLockDocument(uint32 _VSRDTFLAGS.RDT_NoLock, this.FilePath) with - | hr, _, _, docData, _ when ErrorHandler.Succeeded(hr) && docData <> IntPtr.Zero -> - match Marshal.GetObjectForIUnknown docData with - | :? IVsTextBuffer as ivsTextBuffer -> - match textManager.GetActiveView(0, ivsTextBuffer) with - | hr, vsTextView when ErrorHandler.Succeeded(hr) -> Some vsTextView - | _ -> None - | _ -> None - | _ -> None - | _ -> None - | _ -> None - - member this.TryGetTextViewAndCaretPos() : (IVsTextView * Position) option = - match this.TryGetIVsTextView() with - | Some textView -> - match textView.GetCaretPos() with - | hr, line, column when ErrorHandler.Succeeded(hr) -> Some(textView, Position.fromZ line column) - | _ -> None - | None -> None - member this.IsFSharpScript = isScriptFile this.FilePath member this.IsFSharpSignatureFile = isSignatureFile this.FilePath diff --git a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj index 319bdd5a264..42bbd8e8aeb 100644 --- a/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj +++ b/vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj @@ -50,6 +50,7 @@ + diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index db73206996b..afc6f73dfc1 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -20,7 +20,6 @@ open Microsoft.VisualStudio.FSharp.Editor.Extensions open System.Windows open Microsoft.VisualStudio open FSharp.Compiler.Text -open Microsoft.VisualStudio.TextManager.Interop #nowarn "57" @@ -129,7 +128,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = ConcurrentDictionary() let singleFileCache = - ConcurrentDictionary() + ConcurrentDictionary() // This is used to not constantly emit the same compilation. let weakPEReferences = ConditionalWeakTable() @@ -204,36 +203,29 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = cancellableTask { let! ct = CancellableTask.getCancellationToken () let! fileStamp = document.GetTextVersionAsync(ct) - let textViewAndCaret () : (IVsTextView * Position) option = document.TryGetTextViewAndCaretPos() match singleFileCache.TryGetValue(document.Id) with | false, _ -> let! sourceText = document.GetTextAsync(ct) - let getProjectOptionsFromScript textViewAndCaret = - let caret = textViewAndCaret () - - match caret with - | None -> - checker.GetProjectOptionsFromScript( - document.FilePath, - sourceText.ToFSharpSourceText(), - previewEnabled = SessionsProperties.fsiPreview, - assumeDotNetFramework = not SessionsProperties.fsiUseNetCore, - userOpName = userOpName - ) - - | Some(_, caret) -> - checker.GetProjectOptionsFromScript( - document.FilePath, - sourceText.ToFSharpSourceText(), - caret, - previewEnabled = SessionsProperties.fsiPreview, - assumeDotNetFramework = not SessionsProperties.fsiUseNetCore, - userOpName = userOpName - ) - - let! scriptProjectOptions, _ = getProjectOptionsFromScript textViewAndCaret + // FCS reads the caret only to skip resolving the `#r "nuget: …"` line being typed, and only scripts have those. + let focusedCaret = + if isScriptFile document.FilePath then + FocusedCaret.TryGet sourceText + else + ValueNone + + let getProjectOptionsFromScript () = + checker.GetProjectOptionsFromScript( + document.FilePath, + sourceText.ToFSharpSourceText(), + ?caret = (focusedCaret |> ValueOption.toOption |> Option.bind _.Position), + previewEnabled = SessionsProperties.fsiPreview, + assumeDotNetFramework = not SessionsProperties.fsiUseNetCore, + userOpName = userOpName + ) + + let! scriptProjectOptions, _ = getProjectOptionsFromScript () let project = document.Project let otherOptions = @@ -270,25 +262,20 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = let updateProjectOptions () = async { - let! scriptProjectOptions, _ = getProjectOptionsFromScript textViewAndCaret + let! scriptProjectOptions, _ = getProjectOptionsFromScript () checker.NotifyFileChanged(document.FilePath, scriptProjectOptions) |> Async.Start } |> Async.Start - let onChangeCaretHandler (_, _newline: int, _oldline: int) = updateProjectOptions () - let onKillFocus (_) = updateProjectOptions () - let onSetFocus (_) = updateProjectOptions () - let addToCacheAndSubscribe value = match value with | projectId, fileStamp, parsingOptions, projectOptions, _ -> let subscription = - match textViewAndCaret () with - | Some(textView, _) -> - subscribeToTextViewEvents (textView, (Some onChangeCaretHandler), (Some onKillFocus), (Some onSetFocus)) - | None -> None + focusedCaret + |> ValueOption.toOption + |> Option.map (fun caret -> caret.LineChanged.Subscribe updateProjectOptions) (projectId, fileStamp, parsingOptions, projectOptions, subscription) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs b/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs new file mode 100644 index 00000000000..6f0021ce78d --- /dev/null +++ b/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. + +namespace Microsoft.VisualStudio.FSharp.Editor + +open System.ComponentModel.Composition + +open Microsoft.CodeAnalysis.Text +open Microsoft.VisualStudio.Text.Editor +open Microsoft.VisualStudio.Utilities + +open FSharp.Compiler.Text + +/// The caret of the focused editor on a text buffer, published by the UI thread for the project options +/// reactor: the UI thread can be blocked waiting for the reactor, so the reactor must never wait for it. +[] +type internal FocusedCaret() = + + // A reference, not a voption: the reactor reads it while the UI thread writes, and must never see a torn struct. + [] + let mutable position: Position option = None + + let lineChanged = Event() + + /// None while no editor on the buffer has focus. + member _.Position = position + + /// Raised on the UI thread when the caret moves to another line, or focus enters or leaves the buffer's editors. + member _.LineChanged = lineChanged.Publish + + member _.Update(newPosition: Position option) = + let hasLineChanged = Option.map _.Line position <> Option.map _.Line newPosition + position <- newPosition + + if hasLineChanged then + lineChanged.Trigger() + + static member TryGet(sourceText: SourceText) = + match sourceText.Container.TryGetTextBuffer() with + | null -> ValueNone + | buffer -> + match buffer.Properties.TryGetProperty(typeof) with + | true, caret -> ValueSome caret + | _ -> ValueNone + +[)>] +[] +[] +type internal FocusedCaretTracker() = + + let caretOf (textView: ITextView) = + let caret = textView.Caret.Position.BufferPosition + let line = caret.GetContainingLine() + Position.fromZ line.LineNumber (caret.Position - line.Start.Position) + + interface IWpfTextViewCreationListener with + member _.TextViewCreated(textView) = + let focusedCaret = + textView.TextBuffer.Properties.GetOrCreateSingletonProperty(fun () -> FocusedCaret()) + + let publish _ = + focusedCaret.Update(Some(caretOf textView)) + + let subscriptions = + [ + textView.Caret.PositionChanged.Subscribe(fun _ -> + if textView.HasAggregateFocus then + publish ()) + textView.GotAggregateFocus.Subscribe publish + textView.LostAggregateFocus.Subscribe(fun _ -> focusedCaret.Update None) + ] + + if textView.HasAggregateFocus then + publish () + + textView.Closed.Add(fun _ -> + for subscription in subscriptions do + subscription.Dispose()) From d926f92ccec0c02ac90a36e31f2bfe48ab9a6f77 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 12:19:03 +0200 Subject: [PATCH 2/6] Release notes for the script caret hang Co-Authored-By: Claude Opus 5 (1M context) --- docs/release-notes/.VisualStudio/18.vNext.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/release-notes/.VisualStudio/18.vNext.md b/docs/release-notes/.VisualStudio/18.vNext.md index e6034dca8df..37a8b94e7f2 100644 --- a/docs/release-notes/.VisualStudio/18.vNext.md +++ b/docs/release-notes/.VisualStudio/18.vNext.md @@ -20,6 +20,7 @@ * Reduce allocations in the VS project options reactor: the command-line options and project options caches and the mailbox reply payloads now hold struct tuples, and `IProjectSite.CompilationBinOutputPath` returns `string voption` picked with a new `Array.tryPickV`. ([PR #20413](https://github.com/dotnet/fsharp/pull/20413)) * Build a single-file project's `OtherOptions` reference flags with one array comprehension instead of two `Array.ofSeq` calls and an `Array.append`. ([PR #20499](https://github.com/dotnet/fsharp/pull/20499)) * Fix syntax coloring being lost for a whole file when one symbol resolves into metadata that could not be read. ([Issue #20269](https://github.com/dotnet/fsharp/issues/20269), [PR #20274](https://github.com/dotnet/fsharp/pull/20274)) +* Fix a hang when the UI thread waits on project options for a script or a file in F# Miscellaneous Files: the project options reactor reads the caret the UI thread publishes instead of asking the UI thread for it, and only for scripts. ([Issue #20522](https://github.com/dotnet/fsharp/issues/20522), [PR #20523](https://github.com/dotnet/fsharp/pull/20523)) ### Changed From e8d791784844429c0b5c6174e4fd9ac5fb66a1f3 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 11 Sep 2026 12:30:51 +0200 Subject: [PATCH 3/6] Keep the caret in voption until GetProjectOptionsFromScript takes it Co-Authored-By: Claude Opus 5 (1M context) --- .../FSharpProjectOptionsManager.fs | 4 ++-- .../FSharp.Editor/LanguageService/FocusedCaret.fs | 15 ++++++++------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index afc6f73dfc1..78accfcd5a0 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -219,7 +219,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = checker.GetProjectOptionsFromScript( document.FilePath, sourceText.ToFSharpSourceText(), - ?caret = (focusedCaret |> ValueOption.toOption |> Option.bind _.Position), + ?caret = (focusedCaret |> ValueOption.bind _.Position |> ValueOption.toOption), previewEnabled = SessionsProperties.fsiPreview, assumeDotNetFramework = not SessionsProperties.fsiUseNetCore, userOpName = userOpName @@ -274,8 +274,8 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | projectId, fileStamp, parsingOptions, projectOptions, _ -> let subscription = focusedCaret + |> ValueOption.map _.LineChanged.Subscribe(updateProjectOptions) |> ValueOption.toOption - |> Option.map (fun caret -> caret.LineChanged.Subscribe updateProjectOptions) (projectId, fileStamp, parsingOptions, projectOptions, subscription) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs b/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs index 6f0021ce78d..70e75d5afb9 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs @@ -15,20 +15,21 @@ open FSharp.Compiler.Text [] type internal FocusedCaret() = - // A reference, not a voption: the reactor reads it while the UI thread writes, and must never see a torn struct. [] - let mutable position: Position option = None + let mutable position: Position voption = ValueNone let lineChanged = Event() - /// None while no editor on the buffer has focus. + /// ValueNone while no editor on the buffer has focus. member _.Position = position /// Raised on the UI thread when the caret moves to another line, or focus enters or leaves the buffer's editors. member _.LineChanged = lineChanged.Publish - member _.Update(newPosition: Position option) = - let hasLineChanged = Option.map _.Line position <> Option.map _.Line newPosition + member _.Update(newPosition: Position voption) = + let hasLineChanged = + (position |> ValueOption.map _.Line) <> (newPosition |> ValueOption.map _.Line) + position <- newPosition if hasLineChanged then @@ -58,7 +59,7 @@ type internal FocusedCaretTracker() = textView.TextBuffer.Properties.GetOrCreateSingletonProperty(fun () -> FocusedCaret()) let publish _ = - focusedCaret.Update(Some(caretOf textView)) + focusedCaret.Update(ValueSome(caretOf textView)) let subscriptions = [ @@ -66,7 +67,7 @@ type internal FocusedCaretTracker() = if textView.HasAggregateFocus then publish ()) textView.GotAggregateFocus.Subscribe publish - textView.LostAggregateFocus.Subscribe(fun _ -> focusedCaret.Update None) + textView.LostAggregateFocus.Subscribe(fun _ -> focusedCaret.Update ValueNone) ] if textView.HasAggregateFocus then From bfee2255ae4f05155e7e4912ace8688ff694846b Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 18 Sep 2026 18:32:15 +0200 Subject: [PATCH 4/6] Avoid torn focused-caret publication Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../LanguageService/FSharpProjectOptionsManager.fs | 11 ++++++----- .../FSharp.Editor/LanguageService/FocusedCaret.fs | 13 +++++++------ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 78accfcd5a0..061b0c24e09 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -211,15 +211,17 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = // FCS reads the caret only to skip resolving the `#r "nuget: …"` line being typed, and only scripts have those. let focusedCaret = if isScriptFile document.FilePath then - FocusedCaret.TryGet sourceText + match FocusedCaret.TryGet sourceText with + | ValueSome caret -> Some caret + | ValueNone -> None else - ValueNone + None let getProjectOptionsFromScript () = checker.GetProjectOptionsFromScript( document.FilePath, sourceText.ToFSharpSourceText(), - ?caret = (focusedCaret |> ValueOption.bind _.Position |> ValueOption.toOption), + ?caret = (focusedCaret |> Option.bind _.Position), previewEnabled = SessionsProperties.fsiPreview, assumeDotNetFramework = not SessionsProperties.fsiUseNetCore, userOpName = userOpName @@ -274,8 +276,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = | projectId, fileStamp, parsingOptions, projectOptions, _ -> let subscription = focusedCaret - |> ValueOption.map _.LineChanged.Subscribe(updateProjectOptions) - |> ValueOption.toOption + |> Option.map _.LineChanged.Subscribe(updateProjectOptions) (projectId, fileStamp, parsingOptions, projectOptions, subscription) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs b/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs index 70e75d5afb9..483b4c44a7d 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs @@ -15,20 +15,21 @@ open FSharp.Compiler.Text [] type internal FocusedCaret() = + // A reference, not an option: the reactor reads it while the UI thread writes, and must never see a torn struct. [] - let mutable position: Position voption = ValueNone + let mutable position: Position option = None let lineChanged = Event() - /// ValueNone while no editor on the buffer has focus. + /// None while no editor on the buffer has focus. member _.Position = position /// Raised on the UI thread when the caret moves to another line, or focus enters or leaves the buffer's editors. member _.LineChanged = lineChanged.Publish - member _.Update(newPosition: Position voption) = + member _.Update(newPosition: Position option) = let hasLineChanged = - (position |> ValueOption.map _.Line) <> (newPosition |> ValueOption.map _.Line) + (position |> Option.map _.Line) <> (newPosition |> Option.map _.Line) position <- newPosition @@ -59,7 +60,7 @@ type internal FocusedCaretTracker() = textView.TextBuffer.Properties.GetOrCreateSingletonProperty(fun () -> FocusedCaret()) let publish _ = - focusedCaret.Update(ValueSome(caretOf textView)) + focusedCaret.Update(Some(caretOf textView)) let subscriptions = [ @@ -67,7 +68,7 @@ type internal FocusedCaretTracker() = if textView.HasAggregateFocus then publish ()) textView.GotAggregateFocus.Subscribe publish - textView.LostAggregateFocus.Subscribe(fun _ -> focusedCaret.Update ValueNone) + textView.LostAggregateFocus.Subscribe(fun _ -> focusedCaret.Update None) ] if textView.HasAggregateFocus then From 5e8e680053a853bb7c733f94c625272e12b3f667 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Fri, 18 Sep 2026 21:39:16 +0200 Subject: [PATCH 5/6] Format project options manager Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../LanguageService/FSharpProjectOptionsManager.fs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 061b0c24e09..25627a28555 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -275,8 +275,7 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = match value with | projectId, fileStamp, parsingOptions, projectOptions, _ -> let subscription = - focusedCaret - |> Option.map _.LineChanged.Subscribe(updateProjectOptions) + focusedCaret |> Option.map _.LineChanged.Subscribe(updateProjectOptions) (projectId, fileStamp, parsingOptions, projectOptions, subscription) From 1cef1f453620a332f55751bbe38f2659a44ee5e1 Mon Sep 17 00:00:00 2001 From: Andrii Chebukin Date: Sun, 20 Sep 2026 18:26:01 +0200 Subject: [PATCH 6/6] Keep the local caret lookup in voption after the torn-read fix Only the shared FocusedCaret.Position field needed to become `option` for the atomic reference write; the reactor's own local binding was converted along with it for no reason. Route it back through ValueOption and land on `option` once, at the two points that need it. Co-Authored-By: Claude Sonnet 5 --- .../LanguageService/FSharpProjectOptionsManager.fs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs index 25627a28555..abd4faab51d 100644 --- a/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs +++ b/vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs @@ -211,17 +211,15 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = // FCS reads the caret only to skip resolving the `#r "nuget: …"` line being typed, and only scripts have those. let focusedCaret = if isScriptFile document.FilePath then - match FocusedCaret.TryGet sourceText with - | ValueSome caret -> Some caret - | ValueNone -> None + FocusedCaret.TryGet sourceText else - None + ValueNone let getProjectOptionsFromScript () = checker.GetProjectOptionsFromScript( document.FilePath, sourceText.ToFSharpSourceText(), - ?caret = (focusedCaret |> Option.bind _.Position), + ?caret = (focusedCaret |> ValueOption.toOption |> Option.bind _.Position), previewEnabled = SessionsProperties.fsiPreview, assumeDotNetFramework = not SessionsProperties.fsiUseNetCore, userOpName = userOpName @@ -275,7 +273,9 @@ type private FSharpProjectOptionsReactor(checker: FSharpChecker) = match value with | projectId, fileStamp, parsingOptions, projectOptions, _ -> let subscription = - focusedCaret |> Option.map _.LineChanged.Subscribe(updateProjectOptions) + focusedCaret + |> ValueOption.map _.LineChanged.Subscribe(updateProjectOptions) + |> ValueOption.toOption (projectId, fileStamp, parsingOptions, projectOptions, subscription)