Tesserae.Monaco
26.9.5658
dotnet add package Tesserae.Monaco --version 26.9.5658
NuGet\Install-Package Tesserae.Monaco -Version 26.9.5658
<PackageReference Include="Tesserae.Monaco" Version="26.9.5658" />
<PackageVersion Include="Tesserae.Monaco" Version="26.9.5658" />
<PackageReference Include="Tesserae.Monaco" />
paket add Tesserae.Monaco --version 26.9.5658
#r "nuget: Tesserae.Monaco, 26.9.5658"
#:package Tesserae.Monaco@26.9.5658
#addin nuget:?package=Tesserae.Monaco&version=26.9.5658
#tool nuget:?package=Tesserae.Monaco&version=26.9.5658
Tesserae.Monaco
Tesserae.Monaco is a Tesserae (Transpose C#-to-JavaScript) wrapper around the Monaco code editor — the editor that powers VS Code.
It lets you drop a code editor, a code viewer or a diff view into a Tesserae app from C#, with no JavaScript. Monaco ships inside this NuGet package and is copied into your app's output on build, so there is no preload step and no CDN dependency — referencing the package is enough, and it works offline.
The package depends on Tesserae only. It ships no language intelligence of its own: completion, hover and formatting are delegates you supply, so the same components work against a server-side compiler, a client-side analyser, or a static word list.
Usage
using Tesserae.Monaco;
using static Tesserae.UI;
var editor = MonacoEditor.Editor()
.SetLanguage("csharp")
.SetText("public class Hello { }")
.WordWrap()
.OnChanged(() => Console.WriteLine("changed"));
document.body.appendChild(
Stack().WS().HS().Children(editor).Render()
);
The three components are regular IComponents, so the usual Tesserae sizing helpers (.W(), .H(),
.WS(), .HS(), .S(), .Grow(), …) apply. They implement ISpecialCaseStyling, so those styles
land directly on the editor container — which matters, because Monaco needs a sized container to
measure itself against.
Components
| Factory | Component | Use it for |
|---|---|---|
MonacoEditor.Editor(autoHeight) |
CodeEditor |
Editing: completion, hover, formatting, diagnostics |
MonacoEditor.Viewer(autoHeight) |
CodeViewer |
Displaying code — highlighting and selection, no editing affordances |
MonacoEditor.Diff() |
DiffViewer |
Comparing two documents, side-by-side or inline |
MonacoEditor.MultiEditor() |
MultiEditor |
An editor shell: a tree of documents, a tab per open one, unsaved-changes handling, Ctrl+S, the open set in the URL |
Pass autoHeight: true to grow the component to fit its content instead of scrolling vertically (the
parent has to be able to grow too).
How the wrapper reaches Monaco
Monaco is declared, not scripted. src/Interop/ describes the global monaco object with
[External] interfaces and src/Types/ the [ObjectLiteral] payloads that cross the boundary, so a
call site compiles straight to the JavaScript it names and a wrong name or argument is a build error.
Nothing is emitted for an [External] type.
Two consequences worth knowing if you extend it:
asandisdo not work on an[External]interface — a type test needs runtime metadata that is never emitted for one, so it throws rather than answering. Use a direct cast.- The same goes for a BCL generic over one:
List<IJsDisposable>fails to construct.DisposableBagholds release closures for exactly this reason.
Shared editor API
CodeEditor and CodeViewer share everything below, and each returns its own type so a chain can mix
shared and specific calls. All of it is safe to call before the component is mounted: standing
configuration (options, events, actions, widgets) is recorded and replayed, and transient acts
(focusing, revealing) are dropped rather than replayed at the wrong moment.
| Area | Members |
|---|---|
| Content | Text, SetText, ApplyEdits, PushUndoStop, Undo, Redo, LineCount, VersionId, GetLineContent, GetValueInRange, GetOffsetAt, GetPositionAt, GetWordAt, FindMatches, Indentation, EndOfLine |
| Language | SetLanguage(string), SetLanguage(LanguageDefinition), SetLanguageByExtension |
| Models | Model, SetModel, SaveViewState, RestoreViewState |
| History | PersistHistory(options), PersistHistory(scope, documentId), History, ShowHistory() |
| Selection | GetPosition, SetPosition, GetSelection(s), SetSelection(s), GetSelectedText, SelectAll |
| Scrolling | RevealLine, EnsureLineVisible, RevealLineInCenter[IfOutsideViewport], RevealLineNearTop, RevealPosition[InCenter], RevealRange…, Get/SetScrollTop, Get/SetScrollLeft, GetScrollHeight, GetContentHeight, GetContentWidth |
| Decorations | Decorate, ClearDecorations, GetDecorationRanges, CreateDecorations |
| Widgets | AddContentWidget, LayoutContentWidget, RemoveContentWidget, AddOverlayWidget, RemoveOverlayWidget, AddViewZone, RemoveViewZone |
| Markers | SetMarkers, SetDiagnostics, ClearMarkers, GetMarkers, OnMarkersChanged |
| Commands | AddAction, AddCommand, CreateContextKey, Trigger, RunAction, IsActionSupported |
| Built-ins | Format, FormatSelection, ShowFind, ShowReplace, ToggleLineComment, ShowSuggestions, GoToDefinition, ShowReferences, StartRename, ShowOutline, ShowQuickFixes, ShowParameterHints |
| Events | OnFocused, OnBlurred, OnWidgetFocused/Blurred, OnKeyDown/Up, OnMouseDown/Up/Move/Leave, OnContextMenu, OnPaste, OnScrollChanged, OnCursorPositionChanged, OnSelectionChanged, OnContentChanged, OnModelChanged, OnLanguageChanged, OnConfigurationChanged, OnLayoutChanged, OnContentSizeChanged, OnAttemptReadOnlyEdit, OnEditorDisposed, OnRendered |
| Options | ReadOnly, WordWrap, Minimap, LineNumbers, GlyphMargin, Folding, StickyScroll, IndentGuides, Rulers, RenderWhitespace, RenderControlCharacters, RenderLineHighlight, OccurrencesHighlight, FontSize, FontFamily, LineHeight, LetterSpacing, FontLigatures, CursorStyle, CursorBlinking, Padding, Placeholder, ReadOnlyMessage, DomReadOnly, Links, MouseWheelZoom, SmoothScrolling, UnicodeHighlight, ScrollBeyondLastLine, BracketPairColorization, ContextMenu, QuickSuggestions, QuickSuggestionsDelay, AcceptSuggestionOnEnter, TabCompletion, SemanticHighlighting, AriaLabel, AccessibilitySupport, Theme, AutomaticLayout, SetOption(name, value) |
| Escape hatch | Surface — an EditorSurface over the live editor; Editor — the declared IStandaloneCodeEditor; SetRawOption(name, value) for an option EditorOptions does not name; Options(o => …), Layout(), Dispose() |
Trigger vs RunAction. Trigger(id) reaches everything, including commands Monaco binds by
keybinding rule; RunAction(id) only sees the editor's own actions but tells you whether the id
matched. The navigation commands are keybinding rules, so RunAction("editor.action.revealDefinition")
returns false while Trigger on the same id works — hence the GoToDefinition() shorthands above.
A component is remountable: leaving the DOM disposes the editor (the alternative leaks one per
detach) but the component re-arms, so being re-added rebuilds it and replays the configuration, text
and view state. Dispose() is the one-way door.
CodeEditor
Everything above, plus the language providers. The package ships no language intelligence — every one of these is a delegate you supply.
| Area | Members |
|---|---|
| Completion | OnCompletion(ctx => Task<CompletionItem[]>), OnCompletionRaw, OnResolveCompletion, OnInlineCompletion |
| Hover | OnHover(ctx => Task<string>), OnHover(ctx => Task<MarkdownString>), OnHoverRaw |
| Signatures | OnSignatureHelp |
| Fixes | OnCodeActions |
| Navigation | OnDefinition, OnDeclaration, OnTypeDefinition, OnImplementation, OnReferences, OnDocumentHighlights |
| Symbols | OnDocumentSymbols, OnRename |
| Formatting | OnFormat(code => Task<string>), OnTypeFormat |
| Annotations | OnInlayHints, OnCodeLenses, OnFoldingRanges, OnSelectionRanges, OnDocumentLinks, OnColors, OnSemanticTokens, OnLinkedEditing |
| Diagnostics | ValidateAsYouType, Validate (plus the shared marker members) |
| Saving | OnSave(() => Task) — bound to Ctrl+S (Cmd+S on macOS) ahead of the browser — and SaveAsync() |
| Gestures | GoToDefinitionOnClickOnly() |
| Lifecycle | OnChanged, OnBeforeCreate |
| Suggest UI | ShowSuggestDetails(), CloseMessage() |
OnCompletion, OnHover and the navigation providers hand you a CodeContext (the full text, the
text up to the caret, the caret Offset, the Position, and the Word/WordRange under the cursor)
so you never touch dynamic. The …Raw variants take Monaco's (model, position) directly.
Monaco's provider registry is global per language, so every callback is gated on its own model — two
editors on csharp answer independently — and every registration is released when the component is
torn down.
var editor = MonacoEditor.Editor()
.SetLanguage("csharp")
.OnCompletion(async ctx => new[]
{
new CompletionItem { label = "Console", kind = CompletionItemKind.Class },
new CompletionItem { label = "WriteLine", kind = CompletionItemKind.Method, insertText = "WriteLine($0)" }
})
.OnHover(async ctx => $"**{ctx.Word}** — offset {ctx.Offset}")
.OnFormat(async code => await MyServer.FormatAsync(code))
.OnCodeActions(async ctx => await MyServer.GetFixesAsync(ctx.Text, ctx.Markers))
.OnDefinition(async ctx => await MyServer.FindDefinitionAsync(ctx.Offset))
.ValidateAsYouType(async code => await MyServer.GetErrorsAsync(code));
ValidateAsYouType clears the squiggles on each keystroke and only calls the validator after a second
of quiet, then discards the result if the text moved on while it was in flight — so a server-backed
validator is neither hammered nor able to squiggle stale code.
OnResolveCompletion takes either a synchronous delegate or one returning a Task; the async
overload is the one a server-backed host wants, since Monaco calls it for the highlighted item only.
That is what makes a hundred suggestions cost one documentation lookup instead of a hundred. A third
overload is handed a CodeContext as well — Monaco passes the item and a token and nothing else, so
the document and caret the request came from are read off the editor when the resolve runs.
ShowSuggestDetails() opens the pane that documentation lands in, which Monaco otherwise leaves
collapsed.
GoToDefinitionOnClickOnly() makes go-to-definition a click gesture only. Monaco treats it as a
hover gesture as well: while Ctrl (Cmd on macOS) is held, it resolves the definition under the pointer
on every mouse move to underline the word as a link and preview its source — for a server-backed
OnDefinition that is a request per pixel, and for one that answers by opening documentation of its
own, a navigation on a mere hover. With it on, Ctrl-click, F12 and the context menu still navigate,
and hovering keeps OnHover's tooltips. GoToPosition(line, column) is the other half of navigation
answered outside Monaco: caret, centred scroll and focus in one call.
CloseMessage() takes down Monaco's transient over-the-caret message — "No definition found for
'x'". Monaco shows it whenever a definition provider yields nothing, so a provider that did resolve
the symbol and opened its documentation elsewhere needs to close it. Monaco shows the message on the
turn after the provider settles, so the call belongs in a zero-delay timeout rather than inline.
Two Monaco requirements the wrapper handles rather than passing on: injected text (before/after on
a decoration) needs showIfCollapsed when its range is empty, which Decoration.InlineNote sets; and
semantic highlighting is off unless the theme opts in, which OnSemanticTokens arranges.
Documentation in hovers and completion details
What OnHover and a completion item's documentation carry is markdown, rendered by Monaco's own
renderer — and that renderer does more than paragraphs:
- A fenced code block is coloured by the editor's tokenizer for its language, so a signature in <code>```csharp</code> looks like the code it describes. This is how VS Code's language servers render their hovers.
---draws a separator;**bold**, lists and`inline code`work as usual.$(icon-name)is a codicon when theMarkdownStringsetssupportThemeIcons.- A link whose target is
MonacoEditor.CommandLink(id, argument)runs the command registered withMonacoEditor.RegisterCommand(id, handler)when clicked. Monaco routes it through its command service, so documentation gets a clickable action without anything touching the rendered popup. The argument travels as JSON and comes back to the handler as it was.
MonacoEditor.RegisterCommand<string>("app.showTypeDocs", fullType =>
{
MonacoEditor.HideHovers(); // the popup renders above everything else on the page
OpenDocumentationPanel(fullType);
});
editor.OnHover(ctx => Task.FromResult(
"```csharp\nstring Greeter.Greet(string name)\n```\n\n---\n\nReturns a greeting.\n\n" +
$"[Open the documentation]({MonacoEditor.CommandLink("app.showTypeDocs", "Demo.Greeter")})"));
Command links run only on a MarkdownString marked isTrusted; on an untrusted one Monaco strips them
to their text. The string overload of OnHover trusts what it is given, since it is the host's own
text; the MarkdownString overload lets a host decide, and is also where supportThemeIcons and
supportHtml are set.
Do not inject HTML into the rendered popup, and do not lean on supportHtml for styling. Monaco
keeps HTML only after sanitising it against an allowlist that, since 0.56, has no class attribute in
it (only style on a span, for its colours), so HTML cannot be styled from a stylesheet — and a bare
<T> in the text disappears as an unknown tag. An earlier host smuggled escaped HTML behind a marker
and wrote it into the popup with innerHTML once Monaco had rendered it, then re-measured the widget
through Monaco's private hover controller; markdown, a fenced code block and the theme colours below
replace all of that. The tooltip's colours come from the theme: see Theming.
Several documents in one editor
CodeModel is a document independent of any editor. Create one per file, SetModel to switch, and
save/restore the view state per document so each keeps its caret, scroll and folding.
var main = MonacoEditor.CreateModel(mainSource, "typescript", "file:///src/main.ts");
var utils = MonacoEditor.CreateModel(utilSource, "typescript", "file:///src/utils.ts");
editor.SetModel(main);
// … later
var mainState = editor.SaveViewState();
editor.SetModel(utils).RestoreViewState(utilsState);
The URI is not decoration: Monaco's bundled TypeScript service resolves imports by it, and a JSON
schema is matched against it. Models you create are yours to Dispose().
Use ApplyEdits rather than assigning Text when the change should be undoable and leave the caret
alone — assigning Text calls setValue, which resets both.
Persisting history across reloads
PersistHistory(...) records the document as it is edited and puts it back the next time the same
document is opened — after a reload, or in a new browser session.
editor.PersistHistory(new EditorHistoryOptions
{
Scope = $"user:{userId}", // the partition every entry is filed under
DocumentId = "src/Program.cs" // the document within it
});
Two things are kept, because they are what Monaco hands out serialisably: the text, and the view state — caret, selections, scroll offset and folding. Monaco's undo stack is not among them: it lives in the editor's undo service as objects holding closures over the model, with no accessor and nothing to serialise. A restored revision is therefore applied as an ordinary edit between two undo stops, which puts it on the live undo stack — so undo reaches back past a restore.
The default store is the browser's IndexedDB. It is the only web storage that fits: sessionStorage
is emptied when the tab closes; localStorage survives but is synchronous (every write blocks the
thread Monaco lays out on), caps around 5 MB, stores strings only, and has no index to prune by.
IndexedDB is asynchronous, sized against available disk, stores the view state as an object, and its
cursors make "newest first" and "older than a month" bounded rather than full scans.
Every entry is stamped with a UTC epoch-millisecond Timestamp, and scoped: Scope is the partition —
a user id, a workspace id, or a composite — and DocumentId addresses the file inside it, so one
origin holds several users' or projects' histories without them seeing each other.
An entry also says where it came from and who made it, which is what a history fed by more than one
source needs before a list of it means anything. Origin is Local for what the recorder saved in
this browser, Remote for what arrived from outside it — a server checkpoint, another device, a build —
and Unknown for a row that does not say. Author is the display name: EditorHistoryOptions.Author
supplies it for local revisions, and a server-backed store fills in whoever the server says made each of
its own. Nothing has to be labelled by hand in the common arrangement: the recorder stamps Local, and
MirroredHistoryStore stamps Remote on everything it reads back from the store behind it.
| Option | What it does |
|---|---|
Scope, DocumentId |
The partition and the document. The only two with no default. |
Store |
Where it goes. Defaults to IndexedDbHistoryStore.Default. |
Author |
Who is typing, stamped onto every revision written here. Set it as soon as the history is shared. |
SnapshotDebounceMs, PlaceDebounceMs |
How long typing / the caret has to settle first (1500 ms, 500 ms). |
MaxEntries, MaxAge |
Retention: 50 revisions, 30 days. 0 for no cap. |
RestoreOnMount, RestorePlace |
Whether to put the document, and the caret, back on create. |
ShouldRestore |
The veto, given what was found. For when a server is also an authority on the document. |
Clock |
Where Timestamp comes from. Replace it when a server is the authority on time. |
OnSaved, OnRestored, OnError |
Told about each revision written, each one put back, and anything the store raised. |
editor.History is the recorder itself: SaveNowAsync(label) takes a revision by hand and tags it
("before format", a commit id), ListAsync(limit) lists what is stored newest first, Restore(entry)
puts one back, FlushAsync() writes what the debounce is holding, and ClearAsync() forgets the
document.
Browsing what is stored
editor.ShowHistory() opens the revisions in a modal: the list on the left, a diff of the selected
revision against what the editor holds now on the right, and a Revert that puts one back through
the same undoable edit Restore uses.
Button("History").SetIcon(UIcons.ClockFuturePast).OnClick(() => editor.ShowHistory());
It returns the EditorHistoryModal, or null when the editor has no history — so a host can hide the
button rather than open an empty overlay. OnRestored(...) is told which revision was put back, and
Modal is the Tesserae modal itself if it should be sized or hooked differently.
The same surface without the overlay is new EditorHistoryView(editor.History) — an IComponent, so
it goes in a panel, a split view or a page of its own. It carries the search box that filters revisions
by their content (and by author, so alex checkpoint narrows to one person's), the side-by-side/inline
toggle, change navigation, and the "contents are identical" notice for a revision that matches the editor.
A row says three things in two lines, and the list runs newest first over every source at once — a store answers in its own order, so the ordering is re-established over the union rather than inherited:
| Origin | A glyph on the title's own line: a browser window for what was typed here, a cloud for what came from outside it, in that origin's colour. The sentence is in its tooltip. |
| When | Said the way a person would: just now, 40 min ago, 06:21 for earlier today, yesterday 09:12, 3 Sep, 11:32. The exact stamp is in the tooltip. |
| Author | Under the time, against the same edge, and whatever you want it to be — see below. Absent when no author was recorded. |
The row itself is a Button with its content replaced, so the hover, the pressed state, the pointer,
the focus ring and Enter/Space are the toolkit's; the selected row takes the pressed background and a
column of the brand colour at its edge. Nothing here is a card and nothing here is a stylesheet.
The author is yours to draw. What a revision carries is whoever the store recorded — usually an id —
and the name behind it is a lookup, so RenderAuthor hands back a component rather than a string:
editor.ShowHistory().RenderAuthor(entry => InlineLabel(async label =>
{
var person = await Directory.LookUpAsync(entry.Author); // sets nothing -> the slot disappears
if (person is object) label.SetText(person.Name).SetImage(person.AvatarUrl);
}));
An InlineLabel built from a task draws a skeleton while the task runs, shows whatever the task set on
it, and removes itself when the task sets nothing — which is what an id nobody can resolve should look
like. Any IComponent will do, and null leaves the row without an author. Called after the modal is
open it re-draws the rows, so the one-liner above is enough. The default, if you say nothing, is the
recorded name behind a swatch in a colour derived from it, so one person's revisions read as one
person's down the list.
It is composed from Tesserae rather than drawn: SearchableList is the list and its search box, a
Card over a ListItemText is a row, a Banner is the notice, a SplitView is the two panes and
their draggable divider, and the comparison is this package's own DiffViewer. So it ships no
stylesheet — the selected row's colours are Theme variables, and the surface follows the app's light
and dark themes as they change.
Hooking an external system in
Three ways, in increasing order of involvement.
Be told. The browser stays the store; the callback posts what it wants where it wants.
editor.PersistHistory(new EditorHistoryOptions
{
Scope = scope,
DocumentId = documentId,
OnSaved = entry => Post("/api/history", entry.ToPlainObject())
});
Be the store. DelegateHistoryStore builds one out of lambdas, so a server-backed store is an
object initialiser rather than a class. Every hook is optional and an absent one degrades rather than
fails — a missing reader answers with nothing, a missing writer discards.
var server = new DelegateHistoryStore
{
Save = entry => Post("/api/history", entry.ToPlainObject()),
GetLatest = (scope, document) => GetEntry($"/api/history/latest?scope={scope}&doc={document}"),
List = query => GetEntries("/api/history", query)
};
Be both. MirroredHistoryStore writes to the browser and the server, reads from the browser, and
falls through to the server when the browser has nothing — which is what a second device, a new browser
profile or a cleared origin needs to pick the document back up. The server's failures are reported
through OnMirrorError rather than thrown: a server that is down should cost an editor its backup, not
its history.
ListAsync is the exception to "the browser first": browsing a history means seeing all of it, and the
server's checkpoints and this browser's drafts are interleaved in time rather than one being a subset of
the other. So a list is both stores' answers merged, newest first, one row per instant — the local copy
of a mirrored revision wins over the copy read back from the server, and everything from the server is
stamped Remote. Restoring still goes through GetLatestAsync, which is unchanged: what a reload puts
back is the local draft when there is one.
Store = MirroredHistoryStore.LocalFirst(server)
EditorHistoryEntry.ToPlainObject() / FromPlainObject(...) are the wire contract — the field names
they produce (scope, documentId, docKey, timestamp, text, viewState, language,
versionId, label, origin, author, id) are what an external store implements against. origin
is the string "local" or "remote"; anything else, the absent value included, reads back as
EditorHistoryOrigin.Unknown rather than as the wrong one.
Note that persistent is not permanent anywhere in the browser: a user agent may evict a whole origin under storage pressure, and clearing site data always does. That is the case mirroring to a server exists for.
MultiEditor
The shell an application puts around its editors: a Tree of documents on the left, a Pivot with one
tab per open document on the right, and the wiring between them — unsaved-changes markers on the tabs
(TabSaveIndicator) and a prompt before a dirty tab closes, the guard against leaving the page
(UnsavedChangesGuard), Ctrl+S, the open set and the active tab mirrored into the URL, the tree's
folders and the split width remembered across visits, a filter over the tree, and a Ctrl+P quick-open
palette. It is composed from Tesserae rather than drawn from scratch; what it adds is the wiring, which
is what every hand-rolled editor shell ends up re-writing.
var shell = MonacoEditor.MultiEditor()
.PersistInUrl() // ?open=a,b&active=a
.PersistLayout("workspace:build") // folders, scroll, split width in localStorage
.Folder("endpoints", UIcons.Globe, new TreeCommand(UIcons.Plus).OnClick(() => NewEndpoint()))
.ConfigureEditor((doc, editor) => editor.GoToDefinitionOnClickOnly().OnCompletion(...).ValidateAsYouType(...))
.Search(term => Server.SearchAsync(term)) // adds server hits to the title match
.Documents(catalog.Select(item => new EditorDocument(item.Id, item.Name)
{
Folder = "endpoints/" + item.Group,
Icon = UIcons.FileCode,
Status = item.CompileError is object ? DocumentStatus.Error : DocumentStatus.None,
Load = () => Server.LoadAsync(item.Id),
Save = text => Server.SaveAsync(item.Id, text)
}));
An EditorDocument is a description, not an editor: an id, a title, a slash-separated Folder, a
Language (or the title's extension decides), Load and Save, a Status with a message, and the
entries of the row's "..." menu. The editor exists only while its tab is open, and a document opens as
a CodeEditor configured through ConfigureEditor — the place a host attaches its providers. A
document with Content shows that instead, so forms and viewers sit in tabs next to code; such a tab
reports its own dirty state through MarkDirty(id, dirty).
Hidden tabs stay mounted, so switching away and back keeps the caret, the scroll offset, the undo
history and the markers. Documents(...) can be called again whenever the catalog changes — open tabs
are re-bound by id — and SetStatus(id, status, message) flags a document after a compile without
rebuilding anything. Open(document) also takes a document the catalog does not list — a "new file"
with no identity yet — which is not written to the URL until it joins the catalog.
| Area | Members |
|---|---|
| Catalog | Documents, Add, Remove, Folder, SetStatus, Catalog, GetDocument |
| Tabs | Open(id), Open(document), Select, CloseAsync, CloseAllAsync, OpenDocumentIds, ActiveDocumentId, ActiveDocument, IsOpen, EditorOf |
| Saving | SaveAsync(id), SaveAllAsync, IsDirty, HasUnsavedChanges, MarkDirty, ConfirmClose |
| Configuration | ConfigureEditor, Landing, TreeWidth, FilterPlaceholder, Search, QuickOpen, PersistInUrl, PersistLayout |
| Events | OnActiveChanged, OnOpened, OnClosed, OnSaved, OnDirtyChanged |
The URL round-trip writes the active tab under its own key and reads it back before re-opening the
tabs, since re-opening selects each in turn and would otherwise overwrite it; and a document the URL
names before the catalog has arrived is opened as soon as Documents(...) delivers it, so the shell
can be mounted before its first server round trip.
DiffViewer
| Area | Members |
|---|---|
| Content | Original, Modified, SetOriginal, SetModified, SetContent, OriginalModel, ModifiedModel |
| Language | SetLanguage(string), SetLanguage(LanguageDefinition), SetLanguageByExtension, SetOriginalLanguage |
| Layout | SideBySide, Inline, HideUnchangedRegions, RenderOverviewRuler, Minimap, FontSize, DiffWordWrap |
| Comparison | IgnoreTrimWhitespace, RenderIndicators, ShowMoves, MaxComputationTime, Editable, OriginalEditable, RenderMarginRevertIcon |
| Results | GetLineChanges, ChangeCount, IsIdentical, OnDiffUpdated |
| Navigation | GoToNextDifference, GoToPreviousDifference |
| Sides | OriginalSide, ModifiedSide — an EditorSurface each, for decorating or subscribing to one pane |
| Escape hatch | Editor — the declared IStandaloneDiffEditor; SetRawOption(name, value), Options(o => …) |
The diff is computed on a worker, so read ChangeCount or GetLineChanges from OnDiffUpdated —
reading straight after setting the content gets the previous diff, or none.
var diff = MonacoEditor.Diff()
.SetLanguage("csharp")
.SetContent(before, after)
.HideUnchangedRegions() // collapse long identical runs
.ShowMoves() // draw moved blocks as moves
.OnDiffUpdated(() => label.Text = $"{diff.ChangeCount} changed blocks");
Custom languages
MonacoEditor.RegisterLanguage takes a LanguageDefinition — a Monarch tokenizer, an optional
language configuration, theme colours for the tokens it emits, and any punctuation that should trigger
completion. It is idempotent per language id, so a component can register its language
unconditionally.
var mylang = new LanguageDefinition
{
Id = "mylang",
Extensions = new[] { ".mylang" },
Tokenizer = new { tokenizer = new { root = new object[] { new object[] { "\\b(if|else)\\b", "keyword" } } } },
TokenColors = new[] { new TokenColor("keyword", "c586c0", "bold") },
CompletionTriggerCharacters = new[] { ":", "|" }
};
var editor = MonacoEditor.Editor().SetLanguage(mylang);
Monaco only auto-triggers completion on word characters, so a language whose syntax hinges on
punctuation needs CompletionTriggerCharacters declared or your completion handler is never asked.
A grammar that should not be in the initial payload goes on TokenizerFactory instead of
Tokenizer — a Func<Task<object>> Monaco calls the first time a document uses the language, and
never if none does. ConfigurationFactory is its companion for the comment markers and brackets. Both
map onto how Monaco defers its own ~90 grammars, so put the fetch inside the delegate:
var mylang = new LanguageDefinition
{
Id = "mylang",
TokenizerFactory = async () =>
{
await Transpose.Require.RequireAsync("assets/js/mylang.js");
return JsGlobals.MyLangGrammar;
},
TokenColors = new[] { new TokenColor("keyword", "c586c0", "bold") }
};
TokenColors stay on the definition rather than in the deferred grammar: they are folded into the
themes when those are defined, which happens as Monaco loads, well before any factory runs.
MonacoEditor.SetTokenizer(languageId, …) is the other direction — it replaces the grammar of a
language that already exists, one of Monaco's own included. Monaco treats tokenizers as exclusive per
language, so the last one registered wins; that is how a deliberately coarse built-in grammar gets
swapped for a finer one. It takes the same two shapes, eager or deferred, and
MonacoEditor.SetLanguageConfiguration does the same for the brackets and comment markers.
Theming
The components follow the active Tesserae theme. MonacoEditor.LIGHT_THEME / DARK_THEME are defined
when Monaco loads from MonacoEditor.TesseraeThemeColors(): the editor background from
Theme.Secondary.Background, and the surfaces Monaco draws its own popups with — the hover, the suggest
list and its details pane, every other widget — from Theme.Default.Background, .Border and
.Foreground, with links in Tesserae's link colour and code blocks on the editor background. After
toggling the Tesserae theme at runtime, call MonacoEditor.DefineThemes() then MonacoEditor.ApplyTheme().
That is also how a tooltip is restyled. Monaco reads every colour of its widgets from the theme
(editorHoverWidget.background, editorSuggestWidget.border, textLink.foreground, …) and publishes
them as --vscode-* variables, so a stylesheet rule on .monaco-hover is never needed — and, being an
internal class name rather than API, breaks across releases. Put the colour in ThemeColors instead.
| Member | Purpose |
|---|---|
MonacoEditor.TesseraeThemeColors() |
The colours derived from the Tesserae theme, keyed by Monaco's colour ids. Every theme the package defines starts from these; a host defining its own themes from scratch can too. |
MonacoEditor.ThemeColors |
Monaco's theme colour ids — selection, gutter, scrollbar, diff, bracket colours, and any of the derived ones above to override. Applied on top of the derived set. |
MonacoEditor.AddTokenColors(…) |
Syntax colours for a built-in language's tokens, and for the token types a semantic-tokens provider emits. LanguageDefinition.TokenColors only covers its own language. |
MonacoEditor.LightBase / DarkBase |
What the two themes inherit from — set to "hc-light" / "hc-black" for high contrast. |
MonacoEditor.DefineTheme(name, base, rules, colors) |
A theme of your own, for ApplyTheme(name) or a component's Theme(…). Starts from the derived colours and ThemeColors, then applies colors. |
Bundled language services
The JSON, TypeScript, CSS and HTML workers ship with the package. They validate syntax out of the box; these turn them into real language services. All are safe to call before Monaco has loaded.
MonacoEditor.ConfigureJson(schemas: new[]
{
new JsonSchema("https://myapp/config.schema.json", new[] { "*" }, new
{
type = "object",
required = new[] { "name" },
properties = new { name = new { type = "string" } }
})
});
MonacoEditor.ConfigureTypeScript(target: ScriptTarget.ES2020, strict: true, lib: new[] { "es2020", "dom" });
MonacoEditor.AddTypeScriptLib("declare namespace myApp { function log(m: string): void; }", "file:///myapp.d.ts");
ConfigureJson is what turns a JSON editor from a syntax check into schema validation, with completion
and hover for the properties the schema describes. AddTypeScriptLib is how a script the user writes
gets completion against the host's own API.
Read the results back with GetMarkers() / OnMarkersChanged — a worker's diagnostics arrive well
after the edit, so polling after typing reads the previous state.
Global configuration
| Member | Purpose |
|---|---|
MonacoEditor.AssetsPath |
Folder holding monaco.js, its chunks/ and the *.worker.js files (default assets/js/monaco, where the build copies them). Set before the first editor is built; the chunks and workers follow it automatically. |
MonacoEditor.LoadAsync() |
Loads Monaco (at most once per page). Components await this themselves; call it to warm Monaco up, or before calling monaco.* directly. |
MonacoEditor.IsLoaded |
Whether monaco.* is safe to call. |
MonacoEditor.GetLanguageIds() / TryGetLanguageIdForExtension |
Monaco's language registry. |
MonacoEditor.RegisterLanguage(definition) |
A language of your own, eager or deferred. Idempotent per id. |
MonacoEditor.SetTokenizer(id, …) / SetLanguageConfiguration(id, …) |
Replace the grammar or the configuration of a language that already exists, Monaco's own included. |
MonacoEditor.RegisterCommand(id, handler) / CommandLink(id, argument) |
A command a link in hover or completion documentation runs, and the link that runs it — see Documentation in hovers and completion details. |
MonacoEditor.HideHovers() |
Hides the hover tooltip on every editor, through Monaco's own hover controller — for a command that opens something the popup would sit on top of. EditorSurface.HideHover() does one editor. |
MonacoEditor.WhenLoaded(action) |
Runs action once monaco.* is safe to touch — immediately if it already is, queued otherwise. The safe way to make any global Monaco call from application code, since most configuration happens while components are being built. |
MonacoEditor.CreateModel / GetModel / GetModels / GetEditors |
Documents and editors Monaco currently holds. |
MonacoEditor.GetMarkers / OnMarkersChanged |
Every squiggle on the page, the host's own and the workers'. |
MonacoEditor.Colorize / ColorizeAsync / ColorizeElement |
Syntax-highlighted HTML with no editor instance — much cheaper than a CodeViewer for a snippet nobody will interact with. |
MonacoEditor.CreateWebWorker |
A Monaco-managed worker running your own module. |
MonacoEditor.ToPlainObject |
A structured-clone-safe copy, for a whole object graph crossing to a worker. For a plain array, Script.ToArray is the cheaper fix — a Transpose array carries a $type function, which postMessage refuses. |
MonacoEditor.SetLocale |
Translations for Monaco's own UI strings. Must run before the first editor is built. |
Suggest and hover popups render into a single shared, body-mounted host, so they are not clipped when an editor sits inside a modal, a panel or a split view.
Which Monaco, and how it is built
monaco-editor 0.56.0, pinned in package.json and bundled from its ESM build by
build/bundle-monaco.mjs (esbuild). Nothing Monaco-related is committed to this repo — the bundle is
regenerated from the pinned npm package on every build and is gitignored.
The bundle step is not a convenience, it is required. Monaco's ESM tree cannot be loaded by a browser
directly: it contains ~133 bare import './x.css' statements, which browsers reject outright
("Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of
text/css"), spread across 1331 modules. This is what Monaco's own docs mean by "ESM version
(compatible with e.g. webpack)". The alternative — Monaco's prebuilt AMD dist — is deprecated
upstream and slated for removal, so it is deliberately not used.
esbuild resolves that graph and emits an ES-module entry, the chunks it pulls in, and the five
language-service workers into assets/js/monaco/. As much as possible is resolved at build time —
minification, codicon.ttf as a data URI, Monaco's stylesheets folded into the JS as self-injecting
<style> elements, and the MonacoEnvironment worker wiring:
| File | Size | Loaded |
|---|---|---|
monaco.js + its shared chunks/ |
~4.1 MB | On first editor (an ES module publishing window.monaco) |
chunks/<language>-*.js |
1–20 KB each | On demand — the first document in that language |
editor.worker.js |
~300 KB | On demand — diffs, word-based suggestions |
json.worker.js |
~430 KB | On demand — a json model |
css.worker.js |
~1 MB | On demand — a css/scss/less model |
html.worker.js |
~750 KB | On demand — an html/handlebars/razor model |
ts.worker.js |
~7 MB | On demand — a typescript/javascript model |
Nothing is fetched until a component mounts, so a page with no editor on it pays nothing.
The entry is a module because that is what keeps the grammars lazy. Monaco registers each of its
~90 grammars, and each of its four language-service modes, behind a dynamic import(). Bundled to a
single IIFE those are resolved at build time and inlined, so an app that only shows C# still
downloads Perl, Pascal and PowerQuery. Bundled to ESM with code splitting they stay real dynamic
imports, and each language becomes a chunk fetched the first time a document actually uses it —
walking the whole 29-page sample gallery fetches five of the 92 chunks on disk.
The entry installs MonacoEnvironment and resolves the worker and chunk URLs from its own
import.meta.url, so pointing AssetsPath at another origin moves them all with it — and in that
case the workers load through a same-origin blob shim, since the Worker constructor rejects
cross-origin scripts. Set your own window.MonacoEnvironment before the first editor if you want a
different worker strategy.
Serve assets/js/monaco/ as static files with their real paths intact: the chunk names are baked
into the entry's import statements, so a host that renames or flattens them breaks the lazy loads.
License
MIT. Monaco is MIT-licensed too; its license text ships alongside the bundle at
assets/js/monaco/LICENSE.txt.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Tesserae (>= 2026.9.70414)
- Transpose.BCL (>= 26.9.4872)
- Transpose.Core (>= 26.9.4869)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 26.9.5658 | 58 | 9/21/2026 |
| 26.9.4927 | 108 | 9/9/2026 |
| 26.9.4910 | 96 | 9/9/2026 |
| 26.9.4851 | 109 | 9/8/2026 |
| 26.9.4813 | 105 | 9/7/2026 |
| 26.9.4793 | 101 | 9/4/2026 |
| 26.9.4792 | 108 | 9/4/2026 |
| 26.9.4789 | 110 | 9/4/2026 |
| 26.9.4782 | 105 | 9/3/2026 |
| 26.9.4779 | 101 | 9/3/2026 |
| 26.9.4773 | 102 | 9/2/2026 |
| 26.8.4624 | 103 | 8/28/2026 |
| 26.8.4523 | 111 | 8/24/2026 |
| 26.8.4516 | 116 | 8/24/2026 |
| 26.8.4279 | 122 | 8/20/2026 |
| 26.8.4276 | 103 | 8/20/2026 |
| 26.8.4242 | 103 | 8/20/2026 |
| 26.8.4216 | 106 | 8/19/2026 |