Add the F# Interactive window on the JSON-RPC protocol - #20565
xperiandri wants to merge 25 commits into
Conversation
✅ Release notes checked
|
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="$(FSharpSourcesRoot)\FSharp.Core\FSharp.Core.fsproj" /> | ||
| <ProjectReference Include="$(FSharpSourcesRoot)\Compiler\FSharp.Compiler.Service.fsproj" /> |
There was a problem hiding this comment.
How would the architecture need to change for the window to locate the SDK, and start the JSON-RPC server from the SDK? i.e. the Window NOT having a reference to FSharp.Compiler.Service.fsproj
That way, we decouple VS versions and SDK versions (steered via global.json).
We would also gain parity of CLI fsi, CLI build VS FSI - guaranteed to use same compiler bits.
And more importantly, the VS FSI could then be powered by net11 F# compiler, benefiting from any perf improvements on the .NET side.
There was a problem hiding this comment.
Done in the last commit: the session starts in the solution folder with the dotnet a shell there would run, so the host resolves the SDK from global.json exactly as dotnet fsi does — nothing in the window re-implements that, and the fsi it evaluates on is the SDK's. The FCS reference that remains serves the tokenizer only (Enter-vs-newline in SubmissionAnalysis, lexical colouring) — the same FCS FSharp.Editor already loads; it plays no part in evaluation.
There was a problem hiding this comment.
🤖🕵️ @xperiandri This is still occurring.
<ProjectReference Include="$(FSharpSourcesRoot)\Compiler\FSharp.Compiler.Service.fsproj" />SDK selection is fixed; the requested no-FCS window boundary is not.
There was a problem hiding this comment.
You are right, and my earlier answer only covered evaluation. The window no longer references FSharp.Compiler.Service.fsproj, and cannot compile against it: DisableTransitiveProjectReferences stops FSharp.VS.FSI (which keeps the compiler for the old window) from handing it on. What the window used the compiler for was its lexer, to colour input and output and to decide when Enter submits. That is now ILexicalScannerFactory in LexicalScanning.fs, and FSharp.Editor, which already runs on the compiler Visual Studio ships, exports the implementation. The session's own compiler stays the SDK's, in the dotnet fsi process. The submission rule is still tested, with the editor's scanner, in SubmissionAnalysisTests.
| UICultureLcid: int | ||
| } | ||
|
|
||
| module internal FsiLocator = |
There was a problem hiding this comment.
This should use the SDK locator as if you launched dotnet fsi from the solution folder (i.e. following global.json).
Also as long as we keep the legacy FSI in current shape, the new window via JSON-RPC could be netcoreapp only and not offer any of the .NET Framework options.
There was a problem hiding this comment.
Both done: the platform choice, the desktop executables and the shadow-copy switch are gone from this window (they stay with the old one), and the session starts in the solution folder so global.json picks the SDK. When a session comes up the window prints which fsi answered, on what runtime and where.
0bdeb79 to
1990f9e
Compare
|
Re-stacked on xperiandri/fsharp@c5501123 (#20396 after its second pass: the server is .NET-only, the pipe is Two things from the review are still open and are the next steps for this PR rather than for #20396:
One limit worth knowing: the client side cannot ask for |
1db5c63 to
0e32fd1
Compare
|
@xperiandri Current head |
| addSwitch $"{CommandLine.ServerOption}{pipeName}" | ||
| // How the host names itself: a session whose host dies, even before the handshake, exits instead of | ||
| // waiting on the pipe forever. | ||
| addSwitch $"--fsi-server-client-pid:{clientProcessId}" |
There was a problem hiding this comment.
🤖🕵️ @xperiandri user arguments can override the trusted owner PID; closing VS leaves FSI alive.
trusted: --fsi-server-client-pid:<VS pid>
UserArguments: --fsi-server-client-pid:0
owner exits -> server_alive=true
// Reject duplicate ownership switches, or append the trusted PID after UserArguments.There was a problem hiding this comment.
Fixed. The window's own switches now come last, after the user's arguments, and for --fsi-server-jsonrpc and --fsi-server-client-pid the last occurrence wins, so --fsi-server-client-pid:0 in the arguments no longer names the owner. the last owner named on the command line is the one that counts starts a session with two owners and closes the first; the session keeps serving.
1ca49bc to
7609924
Compare
Builds the Visual Studio side of the replacement for the legacy F# Interactive window against Microsoft.VisualStudio.InteractiveWindow, the REPL engine C# Interactive and Python Interactive already run on. InteractiveHost owns the fsi process and speaks the protocol to it. Because the handshake reports the process that actually evaluates code, the temporary-file dance the old window used to discover it under "dotnet fsi" is gone, and so is the line counting that hid the output that discovery produced. FSharpInteractiveEvaluator implements IInteractiveEvaluator. Diagnostics arrive as data and are rendered in the compiler's own layout. AbortExecution maps to the protocol's interrupt, which the session serves as it arrives; the equivalent in C# Interactive is still an empty method. SubmissionAnalysis decides whether Enter submits or adds a line. The window asks on every keystroke on the UI thread, so the judgement is lexical: an explicit ';;' always submits, and without one the submission goes when nothing is left visibly open. Brackets and terminators inside strings and comments do not count. FSharpVsInteractiveWindowProvider creates the tool window and gives its input buffer the F# content type and language service, so the ordinary editor features apply to what the user types. Session settings come from the SessionsProperties the existing Tools, Options page already writes, so no second options page is needed. FSharpVsInteractiveWindowPackage registers the window so Visual Studio can restore it from a persisted layout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Its pkgdef, its MEF component, and the Interactive Window prerequisite that supplies the REPL engine the window runs on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"Send to Interactive" (Alt+Enter), "Send line" (Alt+') and the "F# Interactive" command now open and drive the window built on the interactive window package, not the legacy tool window. The command filter moves to the window's own project, where Roslyn also keeps this code, and reads the submission from the text view rather than through DTE: the selection, or the caret's line when there is none, in which case the caret advances so that repeated Alt+Enter walks down a script. Text sent from an editor carries its file and line to the session, so its diagnostics land on the user's own source. The interactive window submits text without saying where it came from, so the evaluator takes the origin from the command that is about to submit. The legacy filter keeps only "Debug in Interactive", which has not been ported yet, and the legacy window is no longer reachable from any command. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The window launched "dotnet fsi", which resolves to whatever the installed SDK ships. That fsi does not know --fsi-server-jsonrpc, so every session died with FS0243 before the handshake. The extension carries no fsi of its own to fall back on: the VSIX ships none, and never has. FSHARP_INTERACTIVE_PATH now names the fsi to run. Given a .dll it runs under a .NET host found beside it - a build of fsi from a repository needs the runtime that repository provisions, which is usually newer than any machine-wide one - and given an executable it runs that directly. A session that exits before the handshake now says which executable exited and points at the variable, instead of leaving the compiler's FS0243 as the only clue. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The token sets become ordinal HashSets, the terminator check an explicit ordinal String.Equals, and the tests say Ordinal where they assert on prefixes, suffixes, containment and severity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce guid The F# language service guid was written out here for the fifth time in this repository. FSharp.VS.FSI already declares it and this project references that, so it is used from there instead. The identities module becomes public so that it is the one place to reference rather than a copy every component keeps. Roslyn publishes no equivalent: its Guids expose only the F# package id, which is what it uses to force the package to load. The content type name stays a literal here. FSharp.Editor declares it too, but it now references this project, so the name cannot be shared in that direction. Also finishes the voption conversion of the session state, which the build was waiting on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Converted where the option is ours: the submission scan and its last-token and colour state, the editor submission the command filter reads, the escaping exception in an execution result, and the pipe name the entry point looks for. Left as Option where the language or an API requires it: F# optional parameters, Array.tryPick, and the FCS tokenizer, whose result is converted at the boundary with ValueOption.ofOption instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The escaping exception in an execution result, the platform name parser, and the two harness readers the tests match on. The previous commit claimed the first of these but carried only the pipe name. Reflowed the result description so the two interpolated pieces split on a word boundary rather than mid-sentence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The window was written with `| null` annotations but never turned the checking on, so they were decoration. Enabling it named four wire fields that really do arrive null, and found the places that read a nullable value without narrowing it first. The protocol records now say which fields are optional over the wire. fsi compiles the same file with checking off, where the annotations are simply carried. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`isNull` answers the question without narrowing the type, so the value stays nullable for the rest of the expression and every use after it has to be defended separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The other half of the pair: this filter forwards the commands the interactive one does not claim, and was left on isNull when that one moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The platform name now dispatches on the name itself rather than through a comparison helper called four times, the submission rule reads the scan it already produced as a record pattern, and the two guards before an interaction runs are stated as the pair of conditions they are. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neither a shell command group nor set membership can be written as a pattern, so the remaining ladders became active patterns that classify once and give the cases names. The command one also drops the group check that both Exec branches were repeating. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fsi already named it, but in a module only fsi compiles, so the window and the test harness spelled the switch out again. It moves next to the method names, which both ends already take from the shared file for the same reason. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewriting the ladder as a match traded the comparison for a ToLowerInvariant, which is a different rule and not one that was asked for. The ladder is the lesser cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The debug profile already redirects the compiler to the artifacts build; the interactive window needs the same treatment, because the fsi an installed SDK resolves to does not know the JSON-RPC option yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The input buffer belongs to no project, so the editor's semantic classification never sees it, and the window was as plain as the legacy one. A tokenizer-based classifier fills the gap until Phase 3 makes submissions workspace documents. It serves only buffers carrying the interactive window property, which the window sets on each submission buffer it creates; a buffer belonging to a document keeps its project-driven colour. Confirmed against the package binary: AddLanguageBuffer stores the window on every submission buffer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The value printer answers in F# signature syntax, so the same tokenizer serves. Output differs from input in shape rather than language: it grows for the life of the session and interleaves printed values with console writes, so each line is coloured on its own and an edit invalidates only the lines it touched. Both interactive content types are shared with every language the window package hosts, so the classifier now claims a buffer only when the window it belongs to evaluates F#. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The window package records itself in the properties of the buffers it creates for input but not for output, so asking a buffer which window it belongs to could never answer for output. It also replaces the output buffer on reset, which rules out stamping one. The windows evaluating F# now keep a register, and an output buffer is ours when one of them currently owns it. Ownership is also settled later than before. The buffer reaches the provider before the window has finished claiming it, so deciding at that moment decided once and for all against us; it is now asked on each request until known. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The session used to learn its host from the handshake alone, so a Visual Studio that died between launching fsi and connecting left it waiting on the pipe for good. `--fsi-server-client-pid` names the process before the pipe opens; the handshake still carries it for a session started some other way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The window picked its own fsi: `dotnet.exe` from Program Files, or one of the desktop executables the extension ships, chosen through the options page the old window reads. The desktop ones cannot serve the protocol at all — the server exists in the .NET fsi alone — and the machine-wide `dotnet` ignored the SDK a solution pins. The session now starts in the open solution's folder with the `dotnet` a shell there would run, so the host resolves the SDK from that folder's `global.json` exactly as `dotnet fsi` typed at a prompt does, and the window runs the same compiler bits as `dotnet build`. The platform choice, the desktop executables and the shadow-copy switch stay with the old window. When a session comes up the window prints which fsi answered, on what runtime and where, so a pinned SDK is seen rather than guessed at. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
For --fsi-server-jsonrpc and --fsi-server-client-pid the last occurrence wins, so the user's own arguments could name another pipe or another process as the session's owner, and the session would outlive the window. The window's switches now come last; a test pins the last-wins rule they rely on. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…the F# compiler The window used the compiler for one thing, its lexer, to colour input and output and to decide when Enter submits. It now asks an ILexicalScannerFactory, which FSharp.Editor, already running on the compiler Visual Studio ships, exports. The window drops its reference to FSharp.Compiler.Service, and DisableTransitiveProjectReferences keeps FSharp.VS.FSI from handing it back; the session's own compiler remains the SDK's, in the dotnet fsi process. The submission rule is tested with the editor's scanner. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7609924 to
5aeed62
Compare
|
🔍 Tooling Safety Check — Affects-Agent-Config, Affects-Build-Infra, Affects-Design-Time, Affects-Restore, Affects-Test-Tooling
|
Replaces the Visual Studio F# Interactive window with one hosted on
Microsoft.VisualStudio.InteractiveWindow— the same REPL engine C# Interactive and Python Interactive use — driving fsi over the JSON-RPC protocol fsi gained in #20396 instead of parsingSERVER-PROMPT>text on stdin.