Read script carets without blocking the UI thread - #20523
xperiandri wants to merge 7 commits into
Conversation
✅ Release notes checked
|
This comment has been minimized.
This comment has been minimized.
89eaf2a to
550abf6
Compare
T-Gro
left a comment
There was a problem hiding this comment.
🤖🕵️ Please shorten the description using this guidance. Focus on the problem and why the change is needed, in simplified technical English. Leave the implementation inventory to the Files tab and retain necessary caveats.
550abf6 to
29779b0
Compare
86f3ffa to
4c110d6
Compare
xperiandri
left a comment
There was a problem hiding this comment.
Addressing the inline review feedback in the updated branch.
4c110d6 to
40c0cb9
Compare
This comment has been minimized.
This comment has been minimized.
Head branch was pushed to by a user without write access
530c52d to
75fa3cb
Compare
75fa3cb to
1cef1f4
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
1cef1f4 to
8b54a3c
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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 dotnet#20522 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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 <noreply@anthropic.com>
8b54a3c to
bdd422e
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Unresolved moderate issues could leave stale caret or project-option state.
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
Updates F# project-option handling to publish editor caret state without blocking the UI thread, preventing Visual Studio hangs.
Changes:
- Adds focused-caret tracking through text-buffer state.
- Uses caret state for script option resolution.
- Removes obsolete COM caret access and documents the fix.
| File | Summary |
|---|---|
vsintegration/src/FSharp.Editor/LanguageService/FSharpProjectOptionsManager.fs |
Consumes published caret state; cache subscription and stale source-text issues remain. |
vsintegration/src/FSharp.Editor/LanguageService/FocusedCaret.fs |
Tracks caret and focus changes; direct view closure may leave stale state, and coverage is missing. |
vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj |
Includes the focused-caret tracker. |
vsintegration/src/FSharp.Editor/Common/Extensions.fs |
Removes obsolete COM helpers. |
docs/release-notes/.VisualStudio/18.vNext.md |
Documents the Visual Studio hang fix. |
| focusedCaret | ||
| |> ValueOption.map _.LineChanged.Subscribe(updateProjectOptions) | ||
| |> ValueOption.toOption |
There was a problem hiding this comment.
The lifecycle is real but it cannot leave a reference pending, because the suppression and the subscription have the same precondition. FCS skips a #r "nuget: …" line only when it is given a caret, and a caret can only come from the same FocusedCaret object whose LineChanged the entry subscribes to. When TryGet answers ValueNone — no view yet — no caret is passed, so that computation resolves every reference rather than holding one back, and there is nothing for a later notification to re-resolve. Once the user does type in the view that appears, the text version changes, the entry is dropped by fileStamp <> oldFileStamp, and the recomputation finds the caret and subscribes.
What the same lifecycle does break is the other direction, and that is fixed in 1826a7e2a6: the caret lives on the buffer, which outlives the view, so a view closed while it held focus left its line published. Options computed after that were given a caret on a line no editor was on, and the #r there stayed unresolved until some other view moved a caret. Closed now clears the caret when the view being closed is the one holding focus — a view that does not hold focus published nothing to clear, and a sibling view's caret is left alone.
| member _.Update(newPosition: Position option) = | ||
| let hasLineChanged = | ||
| (position |> Option.map _.Line) <> (newPosition |> Option.map _.Line) | ||
|
|
||
| position <- newPosition | ||
|
|
||
| if hasLineChanged then | ||
| lineChanged.Trigger() |
There was a problem hiding this comment.
Added in 1826a7e2a6: FocusedCaretTests covers what the reactor reads and when it is told to read again — a move to another line raises the change and publishes the position, a move along the same line publishes the column without raising it, focus leaving raises it and clears the caret, focus already gone raises nothing, and a text no view is open on has no caret at all. Five tests, all passing.
The tracker itself is not tested: it is an IWpfTextViewCreationListener whose whole body is subscriptions to ITextView events, and standing up a text view in these tests would test the editor rather than this code — the same line the repository draws around the other OLE and view plumbing. What it does with what it observes is the Update behaviour above, and the reactor side of it is the subscription in tryComputeOptionsBySingleScriptOrFile, which needs a real FSharpChecker script resolution to observe.
The caret belongs to the buffer, which outlives the view. A view closed while it held focus left its line published, and the reactor went on skipping the `#r "nuget: …"` on a line no editor was on. `FocusedCaretTests` covers what the reactor reads and when it is told to read again: a move to another line, a move along one, focus leaving, focus already gone, and a text no view is open on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
🔍 Tooling Safety Check — Affects-Design-Time, Affects-Restore
|


Fixes #20522
Visual Studio could hang for good: the project options reactor asked the UI thread for a script's caret (to know whether an
#r "nuget: …"line was still being typed) while the UI thread was itself synchronously waiting on the reactor, resolving a breakpoint in the same file. The UI thread now publishes the caret itself, through the text buffer the reactor already reads; the reactor never waits on the UI thread again, and only scripts look for a caret at all.Moving focus away from a script now submits its pending
#r "nuget: …"line, where before only moving the caret to another line did — the caret is unknown once nothing has focus.