F# Interactive window: IntelliSense and semantic colouring in the input buffer

Open
#20,608 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active

Research direction

Start with #20565 and the submission service interface in FSharp.Interactive.Window, then inspect the corresponding FSharp.Editor workspace and FSharpProjectOptionsManager entry points. Run or extend the listed FSharp.Editor.Tests cases for chained submissions, shadowing, failures, and reset. Done means the input buffer provides completion and semantic colouring while preserving the documented session and reset behavior.

Written by the indexing model from the issue text.

Description

Needs-Triage

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

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>]
module Submission2
let x = 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

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)

#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

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 #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.
Dominant language
F#
Stars
4.3k
Forks
877
Avg merge
6d 5h
Merged PRs (30d)
163

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from dotnet/fsharp

All issues in dotnet/fsharp

Similar issues

More DevTools issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.