diff --git a/ILSpy/Commands/AboutCommand.cs b/ILSpy/Commands/AboutCommand.cs
index ca42bb896..9cbf303a1 100644
--- a/ILSpy/Commands/AboutCommand.cs
+++ b/ILSpy/Commands/AboutCommand.cs
@@ -116,6 +116,7 @@ namespace ILSpy.Commands
SyntaxExtension = syntaxExtension,
Text = output.GetText(),
HighlightingModel = output.HighlightingModel,
+ HighlightingSpans = output.HighlightingSpans,
References = output.References,
DefinitionLookup = output.DefinitionLookup,
UIElements = output.UIElements,
diff --git a/ILSpy/TextView/AvaloniaEditTextOutput.cs b/ILSpy/TextView/AvaloniaEditTextOutput.cs
index daf0445b7..9287e2138 100644
--- a/ILSpy/TextView/AvaloniaEditTextOutput.cs
+++ b/ILSpy/TextView/AvaloniaEditTextOutput.cs
@@ -54,6 +54,15 @@ namespace ILSpy.TextView
public RichTextModel HighlightingModel { get; } = new RichTextModel();
+ // The same spans that build HighlightingModel, but holding the SHARED named
+ // HighlightingColor instances (RichTextModel.SetHighlighting clones them, so the model
+ // itself freezes the colours at decompile time). Kept so the view can rebuild the model
+ // on a theme switch -- the shared instances are re-coloured in place by ThemeManager.
+ readonly List<(int Start, int Length, HighlightingColor Color)> highlightingSpans = new();
+
+ /// The highlighting spans referencing the live named colours; see the field note.
+ public IReadOnlyList<(int Start, int Length, HighlightingColor Color)> HighlightingSpans => highlightingSpans;
+
/// Foldings collected during writing — only ones spanning more than one line.
public IReadOnlyList Foldings => foldings;
@@ -226,7 +235,10 @@ namespace ILSpy.TextView
var (start, color) = openSpans.Pop();
var length = builder.Length - start;
if (length > 0)
+ {
HighlightingModel.SetHighlighting(start, length, color);
+ highlightingSpans.Add((start, length, color));
+ }
}
}
}
diff --git a/ILSpy/TextView/DecompilerTabPageModel.cs b/ILSpy/TextView/DecompilerTabPageModel.cs
index cf181d49c..2e52d1eec 100644
--- a/ILSpy/TextView/DecompilerTabPageModel.cs
+++ b/ILSpy/TextView/DecompilerTabPageModel.cs
@@ -80,6 +80,13 @@ namespace ILSpy.TextView
[ObservableProperty]
private RichTextModel? highlightingModel;
+ ///
+ /// The highlighting spans behind , referencing the live
+ /// named colours (the model itself clones them). The view rebuilds the model from these on
+ /// a theme switch so the re-coloured palette reaches the already-decompiled output.
+ ///
+ public System.Collections.Generic.IReadOnlyList<(int Start, int Length, AvaloniaEdit.Highlighting.HighlightingColor Color)>? HighlightingSpans { get; set; }
+
///
/// Multi-line fold ranges collected by the decompiler (member bodies, attribute blocks,
/// hidden compiler-generated regions, …). Fed to a FoldingManager on the editor.
@@ -339,6 +346,7 @@ namespace ILSpy.TextView
// "Folding must be within document boundary". This path is hit by
// DockWorkspace.OnLanguagePropertyChanged's CurrentNode toggle-through-null.
HighlightingModel = null;
+ HighlightingSpans = null;
Foldings = null;
References = null;
DefinitionLookup = null;
@@ -420,6 +428,7 @@ namespace ILSpy.TextView
using (ILSpy.AppEnv.AppLog.Phase($"DecompileAsync #{callNumber}: collect output (GetText + collateral)"))
rendered = output.GetText();
var model = output.HighlightingModel;
+ var collectedSpans = output.HighlightingSpans;
var collectedFoldings = output.Foldings;
var collectedReferences = output.References;
var collectedLookup = output.DefinitionLookup;
@@ -433,6 +442,7 @@ namespace ILSpy.TextView
Title = cachedBaseTitle;
SyntaxExtension = effectiveSyntaxExtension;
HighlightingModel = model;
+ HighlightingSpans = collectedSpans;
Foldings = collectedFoldings;
References = collectedReferences;
DefinitionLookup = collectedLookup;
@@ -550,6 +560,7 @@ namespace ILSpy.TextView
activeCts = null;
SyntaxExtension = output.SyntaxExtensionOverride ?? Language?.FileExtension ?? string.Empty;
HighlightingModel = output.HighlightingModel;
+ HighlightingSpans = output.HighlightingSpans;
Foldings = output.Foldings;
References = output.References;
DefinitionLookup = output.DefinitionLookup;
diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs
index ff44fcdb7..8839e65cb 100644
--- a/ILSpy/TextView/DecompilerTextView.axaml.cs
+++ b/ILSpy/TextView/DecompilerTextView.axaml.cs
@@ -202,6 +202,30 @@ namespace ILSpy.TextView
RoutingStrategies.Tunnel,
handledEventsToo: false);
Editor.KeyDown += OnEditorKeyDownForZoom;
+
+ // A theme switch re-colours the shared named HighlightingColors in place, but the
+ // semantic RichTextModel cloned them at decompile time (RichTextModel.SetHighlighting
+ // clones). Rebuild the model from the captured spans -- which still reference the live,
+ // re-coloured instances -- so the already-decompiled output repaints with the new palette.
+ ILSpy.Themes.ThemeManager.Current.ThemeChanged += OnThemeChangedRebuildHighlighting;
+ }
+
+ void OnThemeChangedRebuildHighlighting(object? sender, System.EventArgs e)
+ {
+ if (boundModel?.HighlightingSpans is not { Count: > 0 } spans)
+ return;
+ var transformers = Editor.TextArea.TextView.LineTransformers;
+ if (activeColorizer != null)
+ {
+ transformers.Remove(activeColorizer);
+ activeColorizer = null;
+ }
+ var model = new RichTextModel();
+ foreach (var (start, length, color) in spans)
+ model.SetHighlighting(start, length, color);
+ activeColorizer = new RichTextColorizer(model);
+ transformers.Add(activeColorizer);
+ Editor.TextArea.TextView.Redraw();
}
void OnEditorPointerWheelChanged(object? sender, PointerWheelEventArgs e)
diff --git a/ILSpy/TextView/HighlightingService.cs b/ILSpy/TextView/HighlightingService.cs
index cfcf4a3a4..d0a843ad0 100644
--- a/ILSpy/TextView/HighlightingService.cs
+++ b/ILSpy/TextView/HighlightingService.cs
@@ -23,6 +23,8 @@ using System.Xml;
using AvaloniaEdit.Highlighting;
using AvaloniaEdit.Highlighting.Xshd;
+using ILSpy.Themes;
+
namespace ILSpy.TextView
{
///
@@ -73,7 +75,12 @@ namespace ILSpy.TextView
.GetManifestResourceStream(ResourcePrefix + resourceName)
?? throw new InvalidOperationException($"Highlighting resource not found: {resourceName}");
using var reader = XmlReader.Create(stream);
- return HighlightingLoader.Load(reader, HighlightingManager.Instance);
+ var definition = HighlightingLoader.Load(reader, HighlightingManager.Instance);
+ // Hand the freshly-loaded definition to the theme manager: it applies the active
+ // theme's colours now (so the first decompile is themed) and re-themes it on every
+ // later theme switch.
+ ThemeManager.Current.RegisterThemableDefinition(definition);
+ return definition;
}
}
}
diff --git a/ILSpy/Themes/SyntaxColor.cs b/ILSpy/Themes/SyntaxColor.cs
new file mode 100644
index 000000000..618eb1ed6
--- /dev/null
+++ b/ILSpy/Themes/SyntaxColor.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using Avalonia.Media;
+
+using AvaloniaEdit.Highlighting;
+
+namespace ILSpy.Themes
+{
+ ///
+ /// A theme's colour override for a single named (ported from
+ /// the WPF ILSpy theming model). A theme supplies one per syntax token;
+ /// writes it onto the shared HighlightingColor instance, so both the .xshd colorizer and the
+ /// semantic RichTextModel -- which reference the same instance -- pick up the change at once.
+ ///
+ public sealed class SyntaxColor
+ {
+ public Color? Foreground { get; init; }
+ public Color? Background { get; init; }
+ public FontWeight? FontWeight { get; init; }
+ public FontStyle? FontStyle { get; init; }
+
+ public void ApplyTo(HighlightingColor color)
+ {
+ color.Foreground = Foreground is { } foreground ? new SimpleHighlightingBrush(foreground) : null;
+ color.Background = Background is { } background ? new SimpleHighlightingBrush(background) : null;
+ color.FontWeight = FontWeight;
+ color.FontStyle = FontStyle;
+ }
+ }
+}
diff --git a/ILSpy/Themes/SyntaxColorPalettes.cs b/ILSpy/Themes/SyntaxColorPalettes.cs
new file mode 100644
index 000000000..ed09a3c06
--- /dev/null
+++ b/ILSpy/Themes/SyntaxColorPalettes.cs
@@ -0,0 +1,91 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System.Collections.Generic;
+
+using Avalonia.Media;
+
+namespace ILSpy.Themes
+{
+ ///
+ /// Per-theme syntax-colour palettes, keyed by the named <Color> from the .xshd.
+ /// Only the dark C# palette is hand-authored today (the light scheme stays the .xshd default);
+ /// any token without an entry falls back to the algorithmic dark conversion in
+ /// . These values are a first cut and meant to
+ /// be tuned -- they live here so the palette is one easy-to-edit place.
+ ///
+ public static class SyntaxColorPalettes
+ {
+ static Color C(string hex) => Color.Parse(hex);
+
+ static SyntaxColor Fg(string hex) => new() { Foreground = C(hex) };
+ static SyntaxColor Bold(string hex) => new() { Foreground = C(hex), FontWeight = FontWeight.Bold };
+ static SyntaxColor Italic(string hex) => new() { Foreground = C(hex), FontStyle = FontStyle.Italic };
+
+ // Token families on a #1E1E1E editor background:
+ // keyword blue #569CD6 control-flow #C586C0 type teal #4EC9B0
+ // method yellow #DCDCAA local/param #9CDCFE string #CE9178
+ // number green #B5CEA8 comment #6A9955
+ public static IReadOnlyDictionary CSharpDark { get; } = new Dictionary {
+ ["Comment"] = Fg("#6A9955"),
+ ["String"] = Fg("#CE9178"),
+ ["StringInterpolation"] = Fg("#D7BA7D"),
+ ["Char"] = Fg("#CE9178"),
+ ["Preprocessor"] = Fg("#9B9B9B"),
+ ["Punctuation"] = Fg("#D4D4D4"),
+ ["NumberLiteral"] = Fg("#B5CEA8"),
+
+ ["Keywords"] = Fg("#569CD6"),
+ ["ValueTypeKeywords"] = Fg("#569CD6"),
+ ["ReferenceTypeKeywords"] = Fg("#569CD6"),
+ ["GotoKeywords"] = Fg("#C586C0"),
+ ["QueryKeywords"] = Fg("#C586C0"),
+ ["ExceptionKeywords"] = Fg("#C586C0"),
+ ["CheckedKeyword"] = Fg("#569CD6"),
+ ["UnsafeKeywords"] = Fg("#569CD6"),
+ ["OperatorKeywords"] = Fg("#569CD6"),
+ ["ParameterModifiers"] = Fg("#569CD6"),
+ ["Modifiers"] = Fg("#569CD6"),
+ ["Visibility"] = Fg("#569CD6"),
+ ["NamespaceKeywords"] = Fg("#569CD6"),
+ ["GetSetAddRemove"] = Fg("#569CD6"),
+ ["TrueFalse"] = Fg("#569CD6"),
+ ["TypeKeywords"] = Fg("#569CD6"),
+ ["AttributeKeywords"] = Fg("#C586C0"),
+ ["ThisOrBaseReference"] = Fg("#569CD6"),
+ ["NullOrValueKeywords"] = Fg("#569CD6"),
+
+ ["ReferenceTypes"] = Fg("#4EC9B0"),
+ ["InterfaceTypes"] = Fg("#4EC9B0"),
+ ["TypeParameters"] = Fg("#4EC9B0"),
+ ["DelegateTypes"] = Fg("#4EC9B0"),
+ ["ValueTypes"] = Fg("#4EC9B0"),
+ ["EnumTypes"] = Fg("#4EC9B0"),
+
+ ["MethodDeclaration"] = Bold("#DCDCAA"),
+ ["MethodCall"] = Bold("#DCDCAA"),
+ ["FieldDeclaration"] = Italic("#9CDCFE"),
+ ["FieldAccess"] = Italic("#9CDCFE"),
+ ["Variable"] = Fg("#9CDCFE"),
+ ["Parameter"] = Fg("#9CDCFE"),
+
+ ["InactiveCode"] = Fg("#808080"),
+ ["SemanticError"] = Fg("#F44747"),
+ };
+ }
+}
diff --git a/ILSpy/Themes/ThemeManager.cs b/ILSpy/Themes/ThemeManager.cs
index 96f4bd75e..49fab1b78 100644
--- a/ILSpy/Themes/ThemeManager.cs
+++ b/ILSpy/Themes/ThemeManager.cs
@@ -40,6 +40,12 @@ namespace ILSpy.Themes
// declares its own dark palette in two variants).
const string IsThemeAwareKey = "ILSpy.IsThemeAware";
+ // Highlighting definitions whose named colours we re-theme on every theme switch, plus a
+ // snapshot of each definition's ORIGINAL (light, .xshd-default) colours so switching back
+ // to Light restores them exactly. Keyed by colour name within each definition.
+ readonly List themableDefinitions = new();
+ readonly Dictionary> originalColors = new();
+
public static ThemeManager Current { get; } = new();
public string DefaultTheme => "Light";
@@ -87,9 +93,78 @@ namespace ILSpy.Themes
{
app.RequestedThemeVariant = Theme == "Dark" ? ThemeVariant.Dark : ThemeVariant.Light;
}
+ // Re-theme the syntax colours BEFORE notifying the editors, so their Redraw on
+ // ThemeChanged repaints against the new palette.
+ foreach (var definition in themableDefinitions)
+ ApplyHighlightingColors(definition);
ThemeChanged?.Invoke(this, EventArgs.Empty);
}
+ ///
+ /// Registers a highlighting definition for theme-aware colouring and applies the current
+ /// theme to it immediately. Called by HighlightingService when a definition is first
+ /// loaded; from then on the definition is re-themed on every theme switch.
+ ///
+ public void RegisterThemableDefinition(IHighlightingDefinition definition)
+ {
+ ArgumentNullException.ThrowIfNull(definition);
+ if (!themableDefinitions.Contains(definition))
+ themableDefinitions.Add(definition);
+ ApplyHighlightingColors(definition);
+ }
+
+ ///
+ /// Writes the active theme's colours onto a definition's named
+ /// instances IN PLACE -- the same instances the semantic RichTextModel references, so the
+ /// decompiled output and the .xshd colorizer both pick up the change. Light restores the
+ /// original .xshd colours; Dark applies the hand-authored palette where one exists and the
+ /// algorithmic conversion elsewhere. Marks the definition theme-aware so the per-paint
+ /// colorizer doesn't additionally remap it.
+ ///
+ public void ApplyHighlightingColors(IHighlightingDefinition definition)
+ {
+ ArgumentNullException.ThrowIfNull(definition);
+ if (!originalColors.TryGetValue(definition, out var snapshot))
+ {
+ snapshot = new Dictionary();
+ foreach (var color in definition.NamedHighlightingColors)
+ snapshot[color.Name] = color.Clone();
+ originalColors[definition] = snapshot;
+ }
+
+ var darkPalette = definition.Name == "C#" ? SyntaxColorPalettes.CSharpDark : null;
+ foreach (var color in definition.NamedHighlightingColors)
+ {
+ if (!snapshot.TryGetValue(color.Name, out var original))
+ continue;
+ if (IsDarkTheme)
+ {
+ if (darkPalette is not null && darkPalette.TryGetValue(color.Name, out var syntaxColor))
+ syntaxColor.ApplyTo(color);
+ else
+ CopyColor(GetColorForDarkTheme(original), color);
+ }
+ else
+ {
+ CopyColor(original, color);
+ }
+ }
+
+ definition.Properties[IsThemeAwareKey] = bool.TrueString;
+ }
+
+ // Copies colour/style fields from one HighlightingColor onto another. Used instead of
+ // swapping instances because the RichTextModel holds references to the targets.
+ static void CopyColor(HighlightingColor source, HighlightingColor target)
+ {
+ target.Foreground = source.Foreground;
+ target.Background = source.Background;
+ target.FontWeight = source.FontWeight;
+ target.FontStyle = source.FontStyle;
+ target.Underline = source.Underline;
+ target.Strikethrough = source.Strikethrough;
+ }
+
///
/// Reads the XSHD's ILSpy.IsThemeAware property to decide whether the
/// definition opts out of the dark-theme colour remap. Case-sensitive: only the