The studio doesn't know languages by name. A language is one module (a class deriving from LanguageDefinition, and the services it hands out) registered in one place. Everything else is generic and adapts by what the module says it supports:
- the Explorer's New File menu, file pickers and Hub badges;
- Code Studio's run, stop, stdin, Problems and toolchain picker;
- notebook cells, their kernels,
#!shareand package lines; - the Hub's STUDIO ENVIRONMENT row.
Python (Services/Languages/Python/) is the example to copy for an interpreted language. Rust (Services/Languages/Rust/) is the most complete compiled one: a Cargo package per file, crates from comments, static completion and hover, a lldb-dap debugger, an in-process notebook kernel. Tests/TestSupport/FakeLanguage.cs is the smallest complete module: a language that exists only in tests, which prove the studio works with a language it has never heard of.
Create Services/Languages/<Name>/<Name>Language.cs deriving from LanguageDefinition (Services/Languages/ILanguageDefinition.cs).
| Member | What it's for | Python |
|---|---|---|
Id, DisplayName, ShortName |
The id in notebooks (#!python, NotebookCellItem.Language); names in menus; the cell chip |
python, Python, PY |
Aliases |
Other names for #!alias and #!share --from alias |
py, python3 |
FileExtensions, Storage |
Which files are this language. SourceFile keeps them as plain files in the workspace; FryDocument is only for C#'s .frycs |
.py, SourceFile |
Capabilities |
The features to switch on (see below) | StandardInput | NotebookCells | ValueSharing | Packages |
IconKind, AccentHex |
Material icon name and color for tabs, the Explorer and the Hub | LanguagePython, #4B8BBE |
LineCommentPrefix |
Toggle comment; comments in a cell's directive block; the new-cell template | # |
NewFileTemplate |
A new file's content | prints the Python version |
RuntimeDescription |
The breadcrumbs' runtime label when no toolchain is known | Python |
GetHighlighting(isDark) |
An AvaloniaEdit IHighlightingDefinition: an inline XSHD with Dark+ and Light+ colors, as in PythonSyntaxHighlighting.cs |
|
CreateIndentationStrategy, Folding |
Enter and folding behaviour (folding is optional) | indent after : |
EditorAssistants |
Where completion, hover and live diagnostics attach, e.g. an LSP client later. Attached when a file of the language is shown and disposed when it isn't | none yet |
Capabilities, and what each turns on:
| Flag | Turns on |
|---|---|
Completion, QuickInfo, LiveDiagnostics, Formatting |
Say what the language's editor helpers provide. A language that supplies its own EditorAssistants (completion and hover controllers) makes the Roslyn ones stand aside for its files (LanguageDefinitionExtensions.UsesRoslynHelper), so a .rs file never gets a C# popup on top of Rust's. Without EditorAssistants the flags turn on the C# helpers, which is right only for C# |
Debugging, Breakpoints |
F5 debugging, the breakpoint gutter and F9. Without them F5 runs the file |
TestCases, Templates, ExecutionModes |
The Test Cases panel, the template gallery and C#'s run modes |
StandardInput |
The Terminal's input row while a file runs |
NotebookCells |
The language can be picked for notebook cells (needs NotebookKernels) |
ValueSharing |
#!share in and out of its kernel |
Packages |
Package lines in cells and "Install …" offers (needs Packages) |
A language that runs with something installed needs a way to find it, say what it found, and say what's missing.
ResolveAsync(ToolchainQuery): return the toolchain to use for a file or notebook folder. If there isn't one, returnToolchainResolution.NotFound(new MissingToolchainGuidance(...))with per-OS install steps; the Terminal, notebook cells and the Hub all show them.- Look the way a terminal would: read the machine through
IHostEnvironment(Services/Toolchains/HostEnvironment.cs), never throughFileorProcessdirectly, so tests can fake a machine (Tests/TestSupport/FakeHostEnvironment.cs).GetLoginShellPathAsync()gives the login shell's PATH, which an app started from the Finder doesn't have.ExecutableSearchwalks it. - Probe before trusting a candidate: run it with a short timeout and parse its answer. Cache the results;
Refresh()forgets them. - The user's choice: save it with
ToolchainSettingsStore(per language, intoolchains.json), throughSelect. The status bar listsListAsyncand yourActions(e.g. "Create studio environment"), which run throughRunActionAsync. - Order: the saved choice first, then the project's own environment, then what's installed. That way a project's pinned toolchain wins.
PlanAsync(ScriptRunContext)returns aScriptRunPlanofProcessSteps:- Interpreted scripts (Python, JS): single run step (e.g.
python -u file.pyornode file.js). - Compiled languages (Java, C++, C): two-phase execution plan:
- Build step (
IsBuildStep = true): compiler invocation (e.g.,javac -d <temp> file.java,clang++ -std=c++20 -o <bin> file.cpp, orclang -o <bin> file.c). If compilation fails with non-zero exit code,ScriptRunExecutorstops immediately, parses errors viaIDiagnosticParser, and does not run the executable. - Run step (
IsBuildStep = false): execution of the generated artifact (e.g.,java -cp <temp> Mainor<bin>).
- Build step (
- Interpreted scripts (Python, JS): single run step (e.g.
- Start programs only through
IProcessLauncher(StudioLanguageServices.Processes). It registers them withProcessRegistry, which kills them when the plugin unloads or the app exits, and Stop kills the whole process tree. IDiagnosticParser.Parse(output, file)turns a failed run or build's output into Problems (line, column, message). It understands compiler formats (javac,clang/gcc, or Python tracebacks). It can also name aMissingDependency, which Problems offers to install.
Write a kernel program that speaks the Fry kernel protocol, embed it, and return new ProtocolKernel(id, name, launcher, services.Processes, context) from the factory.
- Embedding: add the program's files to
CSharpEditorPlugin.csprojasEmbeddedResources with aLogicalNameprefix, asPythonKernel.%(Filename)%(Extension)does. YourIKernelLauncherextracts them withEmbeddedKernelFiles.Extract(once per content hash). - Launching: the launcher resolves the toolchain for
context.WorkingDirectory()(the notebook's folder) and returns the command. It throwsKernelUnavailableExceptionwith the guidance when the toolchain is missing. - What the notebook does with it: each notebook tab's
NotebookKernelRoutercreates one kernel per language the first time a cell of it runs, and ends it when the tab closes. AProtocolKernelrestarts its program after a crash, with a note that the variables are gone. Cells route to it by their language or a#!<id>first line. Output, rich output (MIME bundles),input(), Stop and Variables need nothing more. - In-process kernels: a kernel can live in the studio instead, as C#'s does. Implement
INotebookKerneldirectly. The compiled languages (Go, C++, F#, SQL, Rust) do it by building each cell into a small program and running it, replaying the items earlier cells defined; Rust's (RustNotebookKernel) is the model:- A cell's process is waited for with
IManagedProcess.WaitForExitOrKillAsync(ct), which kills it where the wait is cancelled. Don't registerKillon the token and thenWaitAsync(ct): the cancelled wait's continuation can dispose the registration before it runs, and the process keeps going. - No time limit of the kernel's own: how long a cell may take is the studio's
ExecutionTimeoutSecondssetting, which arrives as the cancellation token. - Close the program's standard input (
CloseInput()), so a cell that reads input sees the end of it instead of waiting for good. - Report a build failure once, after the last attempt (a kernel may try a cell two ways); put compiler messages on the cell's own lines, not the generated program's.
- Text the program prints as a
text/plaindisplay (__FRY_DISPLAY__) is console text;ExternalOutputProcessoralso reads__FRY_SHARE__lines, a value a cell offers to other kernels.
- A cell's process is waited for with
TryParseDirective(line): recognize a cell's package lines (%pip install x, say%npm install x).RunAsync(command, toolchain, output): run the command, streaming its output into the cell. It returnsPackageCommandResult:SwitchedToolchainwhen it created a new environment to install into (as pip does for an externally managed Python);AddedSearchPathwhen a running kernel should look there (ISearchPathKernel).
InstallCommand(package)andPackageForMissingDependency(name): power the "Install …" buttons. Python'scv2maps toopencv-python, for example.- A dependency that belongs to a file: when there is no global install (Cargo's crates, Java's
//DEPS), returnPackageCommandResult.DirectiveToInsert(e.g.// #crate: rand = "0.8"); the studio adds that line to the open file and says so.
Most debuggers speak the Debug Adapter Protocol; DapAdapterManager starts one and drives it (Services/Debugging/Dap/). Check the adapter's real behaviour with the real adapter before trusting its documentation or your memory of it; each of these was learned that way:
- How it is reached.
LaunchStdioAdapterAsyncfor adapters that talk over their standard streams (lldb-dap,netcoredbg);LaunchSocketAdapterAsyncfor TCP servers (debugpy, and Delve, whosedlv daphas no stdio mode at all: it prints its port and waits, so start it with--listen=127.0.0.1:<port>).LaunchBridgeAdapterAsyncis for an adapter the studio implements itself: the Java bridge overjdb, and the Node one (NodeInspectorDapAdapter), which translates DAP into the Chrome DevTools Protocol thatnode --inspect-brkspeaks over a WebSocket (CdpClient). Node keeps its inspector open after the script ends ("Waiting for the debugger to disconnect..."), so that bridge lets go itself when the main execution context is destroyed. - The order. Pass
DapHandshake.Standardunless the adapter was written for the old order:initialize, thenlaunch(not awaited:lldb-dapanddebugpyanswer it only afterconfigurationDone), then the adapter'sinitializedevent,setBreakpoints,configurationDone. Delve refusessetBreakpointsandconfigurationDoneuntil it has been asked to launch. - Check every answer. Throw
DapExceptionwhenlaunchorattachsayssuccess: false. The handshake already reports a refusedconfigurationDone, asetBreakpointsthe adapter refused (shown in the Output) and an adapter that never answersinitialize(a timeout) or dies while the session starts (its exit code). - Requests carry no
null. An optional field is left out;netcoredbgand V8's inspector both refuse"condition": null, and then no breakpoint is set at all, so the program runs straight through. - Name the source as the debug info does. Breakpoints match the document name in the debug info (
script.csfor a C# script, from its#line), not the editor's title;DapDebugSession.SourcePathOverridesays so. - Where the program's output goes. Some adapters send it as
outputevents (netcoredbg,lldb-dap), some through their own standard output (Delve), which arrives as live output. Some report exit code 0 whatever it was (netcoredbg3.2): setDapDebugSession.ExitCodeOverridewhen the language can learn the real code another way (the C# debug build writes it to a file as the process exits). - Find the adapter for the user. Search the folders it is installed in (
LldbDapLocator,~/.netcoredbg), and when it is missing say where to get it; don't recommend a package manager that doesn't have it. - See the conversation. Set
DapClient.Trace(internal; tests can) to see every message that crosses a connection when a session misbehaves.
Add one line to the StudioLanguageServices constructor (Services/Languages/StudioLanguageServices.cs):
Registry.Register(new PythonLanguage(this));LanguageRegistry.Register refuses an id, alias or extension that's already taken.
- Fakes first: unit tests with fakes, as
PythonToolchainProviderTests(fake machine),PipPackageManagerTestsandPythonTracebackParserTestsdo. - Real toolchain: tests in the
RealPythoncollection style. Write a[<Name>Fact]attribute that skips when the toolchain is missing unless aFRY_REQUIRE_<NAME>=1variable says it must be there, and set that toolchain up in CI (.github/workflows/release.yml, test job). A debugger test does the same with its adapter (FRY_REQUIRE_DELVE,FRY_REQUIRE_NETCOREDBG), and a stand-in adapter (MockDapServer,FakeDapAdapter) pins the order of the requests without one. - Prove a fix with the real tool. A stand-in only says what you believe the tool does. Run the real one, and keep the test that failed before the fix.
- Temp folders only: every test builds
new StudioLanguageServices(tempFolder), so it never reads or changes the user's choices or environments. - Snapshots: render the UI with
tools/UiSnapshots(see headless-ui-snapshots.md).studio --file hello.<ext> --runshows a run, andnotebook --python-demoshows how a demo notebook is built.
- Never branch on a language's id outside its own module. Check a capability, or ask the language for the piece you need.
- Public types go in the
Services.Languages.<Name>namespace. The plainServicesnamespace is imported into every C# script, so anything put there shows up in users' completion. - Nothing slow runs on the UI thread: probing, starting programs and installing packages all run in the background, with cancellation.
- Messages are for the person using the studio. Say what happened and what to do, e.g. "Python 3.9 or newer is needed; brew install python".