You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implementation plan for #2161, on top of the new F# Interactive window in #20565.
#20565 hosts F# Interactive in Visual Studio's Interactive Window (the engine C# Interactive uses) and drives dotnet fsi over the JSON-RPC mode from #20396. Its input buffer already has the F# content type, but no Roslyn document behind it, so it only gets lexical colour. This issue covers the next step: completion, quick info, signature help and semantic colouring in the input buffer, checked against everything the session has already run.
How the pieces fit
The window stays free of the compiler, as it is in #20565: it talks to the session over JSON-RPC and asks FSharp.Editor for everything language-related through small interfaces it declares itself. FSharp.Editor already runs on the compiler Visual Studio ships and already provides every IDE feature for .fsx files, so the plan is to make each input buffer look to it like one more open script.
flowchart LR
subgraph VS["devenv.exe"]
IW["Interactive Window package<br/>(buffers, prompts, history)"]
subgraph Window["FSharp.Interactive.Window"]
Eval["FSharpInteractiveEvaluator"]
Host["InteractiveHostClient"]
end
subgraph Editor["FSharp.Editor"]
Svc["Interactive submission service<br/>(new)"]
WS["Interactive workspace<br/>(new, kind 'Interactive')"]
Opts["FSharpProjectOptionsManager"]
Checker["FSharpChecker"]
Features["Completion, QuickInfo,<br/>SignatureHelp, Classification"]
end
Roslyn["Roslyn editor features"]
end
subgraph FSI["dotnet fsi --fsi-server-jsonrpc"]
Server["JSON-RPC server"]
Session["FsiEvaluationSession"]
end
Disk[("per-session temp dir<br/>Submission1.fsx … SubmissionN.fsx")]
IW <--> Eval
Eval --> Host
Host <-->|"named pipe: JSON-RPC<br/>execute / interrupt / reset"| Server
Server --> Session
Session -.->|"stdout / stderr"| Host
Eval -->|"buffer added / submission succeeded / reset"| Svc
Svc --> WS
Svc --> Disk
Roslyn -->|"document for buffer"| WS
Roslyn --> Features
Features --> Opts
Opts -->|"#load earlier submissions"| Disk
Features --> Checker
Loading
Two processes, two channels, as in #20565: evaluation happens in dotnet fsi (the SDK the solution's global.json selects), while IntelliSense happens in Visual Studio with the IDE's own checker. They never share state directly. What the IDE knows about the session comes from the submissions that succeeded, kept on disk.
Chaining submissions: #load of earlier ones
fsi compiles every interaction into its own module and opens it for the next. The IDE can reproduce that with plain script features: each successful submission is written to SubmissionN.fsx with a one-line header, and the current input is checked as a script that loads all of them.
// Submission2.fsx, written after the second submission ran[<AutoOpen>]moduleSubmission2letx= x +1
Verified with dotnet fsi: with the header, a loaded submission's bindings are visible unqualified and later ones shadow earlier ones (let x = 41 then let x = x + 1 reads 42); #r "nuget: …" inside a loaded submission works. Without the header, #loaded script bindings are not visible unqualified (FS0039).
flowchart TB
S1["Submission1.fsx<br/>AutoOpen module Submission1<br/>let x = 41"]
S2["Submission2.fsx<br/>AutoOpen module Submission2<br/>let x = x + 1"]
F["failed submission<br/>(never written)"]
Cur["input buffer = Submission3.fsx<br/>x. ← completion sees x = 42"]
S1 --> S2 --> Cur
F -.-x Cur
Loading
FSharpProjectOptionsManager builds options for a script with GetProjectOptionsFromScript(path, text), where the text is used only to find #load/#r. For a document of the interactive workspace it passes #load "Submission1.fsx" "Submission2.fsx" in front of the buffer's text. The loaded files become earlier source files of the check, and the input document itself is unchanged, so positions in it map 1:1. That replaces the "concatenated prelude with offset mapping" the design doc (docs/ide/FSI-Modern-Interactive-Window-Plan.md §2.3) proposed.
What happens when
sequenceDiagram
actor User
participant IW as Interactive Window
participant Eval as FSharpInteractiveEvaluator
participant Svc as Submission service (FSharp.Editor)
participant WS as Interactive workspace
participant Roslyn as Roslyn editor features
participant FS as FSharp.Editor language services
participant FSI as dotnet fsi (JSON-RPC)
IW->>Eval: new input buffer
Eval->>Svc: BufferAdded(buffer)
Svc->>WS: add SubmissionN.fsx, open on buffer.AsTextContainer()
User->>IW: types "x."
IW->>Roslyn: completion requested
Roslyn->>WS: document for buffer
Roslyn->>FS: GetCompletions(document)
FS->>FS: options = GetProjectOptionsFromScript(<br/>"#load Submission1.fsx … N-1" + buffer text)
FS-->>Roslyn: items incl. bindings from earlier submissions
User->>IW: Enter
IW->>Eval: CanExecuteCode / ExecuteCodeAsync
Eval->>FSI: fsi/execute
FSI-->>Eval: ExecutionResult { success }
alt success
Eval->>Svc: SubmissionSucceeded(text)
Svc->>Svc: write SubmissionN.fsx with AutoOpen header
else failure
Eval->>Svc: SubmissionFailed
Note over Svc: nothing written, the chain is unchanged
end
IW->>Eval: next input buffer (SubmissionN+1)
Loading
#reset restarts the session and clears the chain:
sequenceDiagram
participant IW as Interactive Window
participant Eval as FSharpInteractiveEvaluator
participant Svc as Submission service
participant WS as Interactive workspace
participant FSI as dotnet fsi
IW->>Eval: ResetAsync
Eval->>Svc: Reset
Svc->>Svc: snapshot classification of executed buffers<br/>(scrollback keeps its colours)
Svc->>WS: close and remove submission documents
Svc->>Svc: delete the session's SubmissionN.fsx files
Eval->>FSI: shut down, start a new session
Loading
Because the IDE's view is built from files rather than from the running session, a buffer created before dotnet fsi has finished starting needs no special handling: it simply loads no earlier submissions yet.
Work items
Submission service interface in FSharp.Interactive.Window, implemented and exported by FSharp.Editor (the same pattern as ILexicalScannerFactory in Add the F# Interactive window on the JSON-RPC protocol #20565), so the window keeps no reference to the compiler: BufferAdded, SubmissionSucceeded, SubmissionFailed, Reset.
Interactive workspace in FSharp.Editor: a Roslyn Workspace of kind Interactive created with the Visual Studio host services, one project named as the F# miscellaneous-files project (so the options manager takes the script path), one SubmissionN.fsx document per input buffer, opened on the buffer's text container.
Session directory: a per-session temp directory for SubmissionN.fsx, written on success with the [<AutoOpen>] module SubmissionN header, deleted on reset and when the window closes.
Options: in FSharpProjectOptionsManager, prefix the text passed to GetProjectOptionsFromScript with a #load of the earlier submissions for documents of the interactive workspace.
Colouring: retire the window's lexical classifier for the input buffer once the buffer has a document (F# syntactic and semantic classification take over); keep the lexical one for output.
Scrollback after reset: snapshot the classification of executed buffers before their documents close and replay it (Roslyn's inert-classifier approach).
One checker: FSharpWorkspaceServiceFactory creates an FSharpChecker per workspace; the interactive workspace should share the host workspace's instead of loading a second copy of every referenced assembly.
Tests in FSharp.Editor.Tests: options for a submission document include the earlier submissions; completion sees a binding from an earlier submission; shadowing resolves to the latest; a failed submission is not part of the chain; reset empties it.
Open questions
Whether Roslyn's diagnostic tagger runs for a non-host workspace. If it does not, squiggles in the input buffer need their own tagger; completion and colouring do not depend on it.
Go to Definition into an earlier submission opens its temp file, two lines off because of the header. Navigating into the window's own scrollback instead, and disabling rename and code fixes there, belong to a follow-up (the "interactive workspace-kind overrides" item of the design doc).
The longer-term alternative is an FCS API that checks an interaction against fsi's own accumulated type-checking state (design doc §2.3, Phase B). The #load chain needs no compiler changes, so it does not wait on that.
Implementation plan for #2161, on top of the new F# Interactive window in #20565.
#20565 hosts F# Interactive in Visual Studio's Interactive Window (the engine C# Interactive uses) and drives
dotnet fsiover the JSON-RPC mode from #20396. Its input buffer already has the F# content type, but no Roslyn document behind it, so it only gets lexical colour. This issue covers the next step: completion, quick info, signature help and semantic colouring in the input buffer, checked against everything the session has already run.How the pieces fit
The window stays free of the compiler, as it is in #20565: it talks to the session over JSON-RPC and asks
FSharp.Editorfor everything language-related through small interfaces it declares itself.FSharp.Editoralready runs on the compiler Visual Studio ships and already provides every IDE feature for.fsxfiles, so the plan is to make each input buffer look to it like one more open script.flowchart LR subgraph VS["devenv.exe"] IW["Interactive Window package<br/>(buffers, prompts, history)"] subgraph Window["FSharp.Interactive.Window"] Eval["FSharpInteractiveEvaluator"] Host["InteractiveHostClient"] end subgraph Editor["FSharp.Editor"] Svc["Interactive submission service<br/>(new)"] WS["Interactive workspace<br/>(new, kind 'Interactive')"] Opts["FSharpProjectOptionsManager"] Checker["FSharpChecker"] Features["Completion, QuickInfo,<br/>SignatureHelp, Classification"] end Roslyn["Roslyn editor features"] end subgraph FSI["dotnet fsi --fsi-server-jsonrpc"] Server["JSON-RPC server"] Session["FsiEvaluationSession"] end Disk[("per-session temp dir<br/>Submission1.fsx … SubmissionN.fsx")] IW <--> Eval Eval --> Host Host <-->|"named pipe: JSON-RPC<br/>execute / interrupt / reset"| Server Server --> Session Session -.->|"stdout / stderr"| Host Eval -->|"buffer added / submission succeeded / reset"| Svc Svc --> WS Svc --> Disk Roslyn -->|"document for buffer"| WS Roslyn --> Features Features --> Opts Opts -->|"#load earlier submissions"| Disk Features --> CheckerTwo processes, two channels, as in #20565: evaluation happens in
dotnet fsi(the SDK the solution'sglobal.jsonselects), while IntelliSense happens in Visual Studio with the IDE's own checker. They never share state directly. What the IDE knows about the session comes from the submissions that succeeded, kept on disk.Chaining submissions:
#loadof earlier onesfsi compiles every interaction into its own module and opens it for the next. The IDE can reproduce that with plain script features: each successful submission is written to
SubmissionN.fsxwith a one-line header, and the current input is checked as a script that loads all of them.Verified with
dotnet fsi: with the header, a loaded submission's bindings are visible unqualified and later ones shadow earlier ones (let x = 41thenlet x = x + 1reads 42);#r "nuget: …"inside a loaded submission works. Without the header,#loaded script bindings are not visible unqualified (FS0039).flowchart TB S1["Submission1.fsx<br/>AutoOpen module Submission1<br/>let x = 41"] S2["Submission2.fsx<br/>AutoOpen module Submission2<br/>let x = x + 1"] F["failed submission<br/>(never written)"] Cur["input buffer = Submission3.fsx<br/>x. ← completion sees x = 42"] S1 --> S2 --> Cur F -.-x CurFSharpProjectOptionsManagerbuilds options for a script withGetProjectOptionsFromScript(path, text), where the text is used only to find#load/#r. For a document of the interactive workspace it passes#load "Submission1.fsx" "Submission2.fsx"in front of the buffer's text. The loaded files become earlier source files of the check, and the input document itself is unchanged, so positions in it map 1:1. That replaces the "concatenated prelude with offset mapping" the design doc (docs/ide/FSI-Modern-Interactive-Window-Plan.md§2.3) proposed.What happens when
sequenceDiagram actor User participant IW as Interactive Window participant Eval as FSharpInteractiveEvaluator participant Svc as Submission service (FSharp.Editor) participant WS as Interactive workspace participant Roslyn as Roslyn editor features participant FS as FSharp.Editor language services participant FSI as dotnet fsi (JSON-RPC) IW->>Eval: new input buffer Eval->>Svc: BufferAdded(buffer) Svc->>WS: add SubmissionN.fsx, open on buffer.AsTextContainer() User->>IW: types "x." IW->>Roslyn: completion requested Roslyn->>WS: document for buffer Roslyn->>FS: GetCompletions(document) FS->>FS: options = GetProjectOptionsFromScript(<br/>"#load Submission1.fsx … N-1" + buffer text) FS-->>Roslyn: items incl. bindings from earlier submissions User->>IW: Enter IW->>Eval: CanExecuteCode / ExecuteCodeAsync Eval->>FSI: fsi/execute FSI-->>Eval: ExecutionResult { success } alt success Eval->>Svc: SubmissionSucceeded(text) Svc->>Svc: write SubmissionN.fsx with AutoOpen header else failure Eval->>Svc: SubmissionFailed Note over Svc: nothing written, the chain is unchanged end IW->>Eval: next input buffer (SubmissionN+1)#resetrestarts the session and clears the chain:sequenceDiagram participant IW as Interactive Window participant Eval as FSharpInteractiveEvaluator participant Svc as Submission service participant WS as Interactive workspace participant FSI as dotnet fsi IW->>Eval: ResetAsync Eval->>Svc: Reset Svc->>Svc: snapshot classification of executed buffers<br/>(scrollback keeps its colours) Svc->>WS: close and remove submission documents Svc->>Svc: delete the session's SubmissionN.fsx files Eval->>FSI: shut down, start a new sessionBecause the IDE's view is built from files rather than from the running session, a buffer created before
dotnet fsihas finished starting needs no special handling: it simply loads no earlier submissions yet.Work items
FSharp.Interactive.Window, implemented and exported byFSharp.Editor(the same pattern asILexicalScannerFactoryin Add the F# Interactive window on the JSON-RPC protocol #20565), so the window keeps no reference to the compiler:BufferAdded,SubmissionSucceeded,SubmissionFailed,Reset.FSharp.Editor: a RoslynWorkspaceof kindInteractivecreated with the Visual Studio host services, one project named as the F# miscellaneous-files project (so the options manager takes the script path), oneSubmissionN.fsxdocument per input buffer, opened on the buffer's text container.SubmissionN.fsx, written on success with the[<AutoOpen>] module SubmissionNheader, deleted on reset and when the window closes.FSharpProjectOptionsManager, prefix the text passed toGetProjectOptionsFromScriptwith a#loadof the earlier submissions for documents of the interactive workspace.FSharpWorkspaceServiceFactorycreates anFSharpCheckerper workspace; the interactive workspace should share the host workspace's instead of loading a second copy of every referenced assembly.FSharp.Editor.Tests: options for a submission document include the earlier submissions; completion sees a binding from an earlier submission; shadowing resolves to the latest; a failed submission is not part of the chain; reset empties it.Open questions
#loadchain needs no compiler changes, so it does not wait on that.