-
Notifications
You must be signed in to change notification settings - Fork 880
Read script carets without blocking the UI thread #20523
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
base: main
Are you sure you want to change the base?
Changes from all commits
6bc8cf1
5b15163
b6e602f
57bc4ca
6e9472d
bdd422e
1826a7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| // 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. | ||
| [<Sealed>] | ||
| type internal FocusedCaret() = | ||
|
|
||
| // A reference, not an option: the reactor reads it while the UI thread writes, and must never see a torn struct. | ||
| [<VolatileField>] | ||
| let mutable position: Position option = None | ||
|
|
||
| let lineChanged = Event<unit>() | ||
|
|
||
| /// 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 = | ||
| (position |> Option.map _.Line) <> (newPosition |> Option.map _.Line) | ||
|
|
||
| position <- newPosition | ||
|
|
||
| if hasLineChanged then | ||
| lineChanged.Trigger() | ||
|
Comment on lines
+30
to
+37
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in The tracker itself is not tested: it is an |
||
|
|
||
| static member TryGet(sourceText: SourceText) = | ||
| match sourceText.Container.TryGetTextBuffer() with | ||
| | null -> ValueNone | ||
| | buffer -> | ||
| match buffer.Properties.TryGetProperty<FocusedCaret>(typeof<FocusedCaret>) with | ||
| | true, caret -> ValueSome caret | ||
| | _ -> ValueNone | ||
|
|
||
| [<Export(typeof<IWpfTextViewCreationListener>)>] | ||
| [<ContentType(FSharpConstants.FSharpContentTypeName)>] | ||
| [<TextViewRole(PredefinedTextViewRoles.Editable)>] | ||
| 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() | ||
|
|
||
| // The caret belongs to the buffer, which outlives the view. A view closed while it held focus | ||
| // would leave its line published, and the reactor would go on skipping the `#r` on a line no | ||
| // editor is on. A view that does not hold focus published nothing to clear. | ||
| if textView.HasAggregateFocus then | ||
| focusedCaret.Update None) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| // Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information. | ||
|
|
||
| /// What the project options reactor reads off the focused editor, and when it is told to read it again. | ||
| /// Only the line matters: the caret decides which `#r "nuget: …"` line is still being typed, and a script's | ||
| /// references are resolved again once it leaves that line or the editors lose focus. | ||
| module FSharp.Editor.Tests.FocusedCaretTests | ||
|
|
||
| open Xunit | ||
| open Microsoft.CodeAnalysis.Text | ||
| open Microsoft.VisualStudio.FSharp.Editor | ||
| open FSharp.Compiler.Text | ||
|
|
||
| let private caretWithRecordedChanges () = | ||
| let caret = FocusedCaret() | ||
| let changes = ResizeArray() | ||
| caret.LineChanged.Add(fun () -> changes.Add caret.Position) | ||
| caret, changes | ||
|
|
||
| [<Fact>] | ||
| let ``a caret that moves to another line is published as a line change`` () = | ||
| let caret, changes = caretWithRecordedChanges () | ||
|
|
||
| caret.Update(Some(Position.mkPos 7 0)) | ||
| caret.Update(Some(Position.mkPos 8 4)) | ||
|
|
||
| Assert.Equal<int list>([ 7; 8 ], [ for position in changes -> position.Value.Line ]) | ||
| Assert.Equal(8, caret.Position.Value.Line) | ||
|
|
||
| [<Fact>] | ||
| let ``a caret that moves along its line is not a line change`` () = | ||
| let caret, changes = caretWithRecordedChanges () | ||
|
|
||
| caret.Update(Some(Position.mkPos 7 0)) | ||
| caret.Update(Some(Position.mkPos 7 12)) | ||
|
|
||
| Assert.Equal(1, changes.Count) | ||
| // The position is still published: the reactor reads the column of the line it skips. | ||
| Assert.Equal(12, caret.Position.Value.Column) | ||
|
|
||
| [<Fact>] | ||
| let ``editors losing focus is a line change, and the caret is gone`` () = | ||
| let caret, changes = caretWithRecordedChanges () | ||
|
|
||
| caret.Update(Some(Position.mkPos 7 0)) | ||
| caret.Update None | ||
|
|
||
| Assert.Equal(2, changes.Count) | ||
| Assert.True(caret.Position.IsNone) | ||
|
|
||
| [<Fact>] | ||
| let ``no editor has focus twice over is one line change`` () = | ||
| let caret, changes = caretWithRecordedChanges () | ||
|
|
||
| caret.Update None | ||
| caret.Update None | ||
|
|
||
| Assert.Empty changes | ||
| Assert.True(caret.Position.IsNone) | ||
|
|
||
| [<Fact>] | ||
| let ``a text with no editor behind it has no caret`` () = | ||
| // What the reactor sees for a document Visual Studio has not opened a view on. | ||
| Assert.True((FocusedCaret.TryGet(SourceText.From "#r \"nuget: Newtonsoft.Json\"\n")).IsNone) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 sameFocusedCaretobject whoseLineChangedthe entry subscribes to. WhenTryGetanswersValueNone— 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 byfileStamp <> 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#rthere stayed unresolved until some other view moved a caret.Closednow 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.