Files
AtomUI/tests/AtomUI.Toolkits.GalleryBase.Tests/SourceCode/GalleryCodeViewerRuntimeTests.cs
T

837 lines
28 KiB
C#

using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using AtomUI.Controls;
using AtomUI.Theme;
using AtomUI.Toolkits.GalleryBase.Controls;
using AtomUI.Toolkits.GalleryBase.SourceCode;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Headless;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.VisualTree;
using AvaloniaEdit;
using AvaloniaEdit.Editing;
using AvaloniaEdit.TextMate;
using Shouldly;
using TextMateSharp.Registry;
using TextMateSharp.Grammars;
using Xunit;
using AtomUIContextMenu = AtomUI.Desktop.Controls.ContextMenu;
using AtomUIMenuItem = AtomUI.Desktop.Controls.MenuItem;
using DesktopTabControl = AtomUI.Desktop.Controls.TabControl;
using DesktopTabItem = AtomUI.Desktop.Controls.TabItem;
namespace AtomUI.Toolkits.GalleryBase.Tests.SourceCode;
public class GalleryCodeViewerRuntimeTests
{
static GalleryCodeViewerRuntimeTests()
{
AvaloniaTestApp.EnsureInitialized();
}
[Fact]
public void GalleryCodeViewer_Applies_TextEditor_Template_Without_Fluent_Theme_Resources()
{
var viewer = new GalleryCodeViewer
{
CodeText = "public class Demo { }",
Language = "csharp"
};
Should.NotThrow(() => ShowInWindow(viewer, _ => { }));
}
[Fact]
public void GalleryCodeViewer_Uses_Light_And_Dark_Syntax_Theme_Defaults()
{
var viewer = new GalleryCodeViewer();
viewer.LightSyntaxTheme.ShouldBe(ThemeName.LightPlus);
viewer.DarkSyntaxTheme.ShouldBe(ThemeName.DarkPlus);
viewer.Dispose();
}
[Fact]
public void GalleryCodeViewer_Updates_TextMate_Theme_When_ThemeVariant_Changes()
{
var viewer = new GalleryCodeViewer
{
CodeText = "<WrapPanel><Button Content=\"Demo\" /></WrapPanel>",
Language = "axaml"
};
var themeScope = new ThemeVariantScope
{
RequestedThemeVariant = ThemeVariant.Light,
Child = viewer
};
ShowInWindow(viewer, _ =>
{
GetTextMateThemeColor(viewer, "editor.background").ShouldBe("#FFFFFF", StringCompareShould.IgnoreCase);
themeScope.RequestedThemeVariant = ThemeVariant.Dark;
Dispatcher.UIThread.RunJobs();
GetTextMateThemeColor(viewer, "editor.background")
.ToUpperInvariant()
.ShouldNotBe("#FFFFFF");
}, themeScope);
}
[Fact]
public void GalleryCodeViewer_Horizontal_ScrollBar_Starts_After_Line_Number_Gutter()
{
var viewer = new GalleryCodeViewer
{
CodeText = string.Join(
"\n",
Enumerable.Range(1, 40).Select(index => $"{index}: {new string('x', 220)}")),
Language = "csharp"
};
ShowInWindow(viewer, editor =>
{
var lineNumberMargin = editor.GetVisualDescendants()
.OfType<LineNumberMargin>()
.Single();
var horizontalScrollBar = editor.GetVisualDescendants()
.OfType<ScrollBar>()
.Single(scrollBar => scrollBar.Orientation == Orientation.Horizontal);
horizontalScrollBar.IsVisible.ShouldBeTrue();
lineNumberMargin.Bounds.Width.ShouldBeGreaterThan(0);
var lineNumberRightPoint = lineNumberMargin.TranslatePoint(
new Point(lineNumberMargin.Bounds.Width, 0),
editor);
var scrollBarLeftPoint = horizontalScrollBar.TranslatePoint(new Point(), editor);
lineNumberRightPoint.ShouldNotBeNull();
scrollBarLeftPoint.ShouldNotBeNull();
scrollBarLeftPoint.Value.X.ShouldBeGreaterThanOrEqualTo(lineNumberRightPoint.Value.X - 0.5);
});
}
[Fact]
public void GalleryCodeViewer_ScrollBar_Inset_Converges_While_Horizontally_Scrolling()
{
var viewer = new GalleryCodeViewer
{
CodeText = string.Join(
"\n",
Enumerable.Range(1, 40).Select(index => $"{index}: {new string('x', 220)}")),
Language = "csharp"
};
ShowInWindow(viewer, editor =>
{
var horizontalScrollBar = editor.GetVisualDescendants()
.OfType<ScrollBar>()
.Single(scrollBar => scrollBar.Orientation == Orientation.Horizontal);
var scrollViewer = GetEditorScrollViewer(editor);
// Drive the editor through a range of horizontal scroll offsets, forcing a layout
// pass at each step. This reproduces the auto-scroll-while-selecting feedback loop:
// the inset must settle to a stable value and not keep mutating the margin (which
// previously re-triggered layout forever and hung the UI thread).
double MeasureInsetAfterScroll(double offset)
{
scrollViewer.Offset = new Vector(offset, scrollViewer.Offset.Y);
editor.InvalidateMeasure();
editor.InvalidateArrange();
Dispatcher.UIThread.RunJobs();
return horizontalScrollBar.Margin.Left;
}
foreach (var offset in new[] { 0d, 50d, 120d, 200d, 320d })
{
MeasureInsetAfterScroll(offset);
}
// After settling at a fixed offset, repeated layout passes must not change the inset.
var settled = MeasureInsetAfterScroll(320d);
for (var i = 0; i < 5; i++)
{
MeasureInsetAfterScroll(320d).ShouldBe(settled, 0.5);
}
// The inset stays pinned to the (non-scrolling) gutter width, independent of offset.
settled.ShouldBeGreaterThan(0);
});
}
[Fact]
public void GalleryCodeViewer_Selection_Drag_AutoScroll_Does_Not_Hang()
{
var viewer = new GalleryCodeViewer
{
CodeText = string.Join(
"\n",
Enumerable.Range(1, 40).Select(index => $"{index}: {new string('x', 260)}")),
Language = "csharp"
};
var window = new Window
{
Width = 640,
Height = 480,
Content = viewer
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
viewer.ApplyTemplate();
var editor = viewer.GetVisualDescendants()
.OfType<TextEditor>()
.Single();
editor.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
var textView = editor.TextArea.TextView;
var startPoint = textView.TranslatePoint(new Point(80, 22), window).ShouldNotBeNull();
var dragPoint = textView.TranslatePoint(new Point(textView.Bounds.Width + 160, 82), window)
.ShouldNotBeNull();
window.MouseMove(startPoint);
window.MouseDown(startPoint, MouseButton.Left);
window.MouseMove(dragPoint);
for (var i = 0; i < 8; i++)
{
Dispatcher.UIThread.RunJobs();
}
window.MouseUp(dragPoint, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
editor.HorizontalOffset.ShouldBeGreaterThan(0);
editor.TextArea.Selection.IsEmpty.ShouldBeFalse();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
}
finally
{
window.Close();
Dispatcher.UIThread.RunJobs();
viewer.Dispose();
}
}
[Fact]
public void GalleryCodeViewer_Keeps_TextMate_When_Click_Does_Not_Start_Selection_Drag()
{
var viewer = new GalleryCodeViewer
{
CodeText = string.Join(
"\n",
Enumerable.Repeat(
" <atom:TextBlock Foreground=\"{atom:SharedTokenResource ColorTextTertiary}\" Text=\"{Binding Description}\" />",
40)),
Language = "axaml"
};
var window = new Window
{
Width = 640,
Height = 480,
Content = viewer
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
viewer.ApplyTemplate();
var editor = viewer.GetVisualDescendants()
.OfType<TextEditor>()
.Single();
editor.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
var startPoint = editor.TextArea.TextView.TranslatePoint(new Point(80, 22), window)
.ShouldNotBeNull();
window.MouseMove(startPoint);
window.MouseDown(startPoint, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
window.MouseUp(startPoint, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
}
finally
{
window.Close();
Dispatcher.UIThread.RunJobs();
viewer.Dispose();
}
}
[Fact]
public void GalleryCodeViewer_Keeps_TextMate_During_Pointer_Selection_Drag()
{
var viewer = new GalleryCodeViewer
{
CodeText = string.Join(
"\n",
Enumerable.Repeat(
" <atom:TextBlock Foreground=\"{atom:SharedTokenResource ColorTextTertiary}\" Text=\"{Binding Description}\" />",
40)),
Language = "axaml"
};
var window = new Window
{
Width = 640,
Height = 480,
Content = viewer
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
viewer.ApplyTemplate();
var editor = viewer.GetVisualDescendants()
.OfType<TextEditor>()
.Single();
editor.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
var textView = editor.TextArea.TextView;
var startPoint = textView.TranslatePoint(new Point(80, 22), window)
.ShouldNotBeNull();
var dragPoint = textView.TranslatePoint(new Point(180, 52), window)
.ShouldNotBeNull();
window.MouseMove(startPoint);
window.MouseDown(startPoint, MouseButton.Left);
window.MouseMove(dragPoint);
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
window.MouseUp(dragPoint, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
editor.TextArea.TextView.LineTransformers
.OfType<TextMateColoringTransformer>()
.ShouldNotBeEmpty();
}
finally
{
window.Close();
Dispatcher.UIThread.RunJobs();
viewer.Dispose();
}
}
[Fact]
public void GalleryCodeViewer_Uses_Dark_Syntax_Theme_When_AtomUI_Dark_Mode_Is_Enabled()
{
var application = Application.Current!;
var previousDarkMode = application.IsDarkThemeMode();
try
{
application.SetDarkThemeMode(true);
Dispatcher.UIThread.RunJobs();
var viewer = new GalleryCodeViewer
{
CodeText = "<StackPanel Margin=\"20\" Spacing=\"12\" />",
Language = "axaml"
};
ShowInWindow(viewer, _ =>
{
GetCurrentSyntaxTheme(viewer).ShouldBe(ThemeName.DarkPlus);
GetAxamlTagNameColor(viewer).ShouldBe("#569CD6", StringCompareShould.IgnoreCase);
});
}
finally
{
application.SetDarkThemeMode(previousDarkMode);
Dispatcher.UIThread.RunJobs();
}
}
[Fact]
public void GalleryCodeViewer_Treats_AtomUI_Custom_Dark_ThemeVariant_As_Dark_Syntax_Theme()
{
var application = Application.Current!;
var previousDarkMode = application.IsDarkThemeMode();
var actualThemeVariantChangedCount = 0;
var isDarkModeWhenThemeManagerVariantRaised = false;
EventHandler handler = (_, _) => actualThemeVariantChangedCount++;
var themeManager = application.GetThemeManager().ShouldNotBeNull();
using var themeVariantSubscription = themeManager.BindingSource
.GetObservable(IThemeManager.ThemeVariantProperty)
.Subscribe(_ =>
{
isDarkModeWhenThemeManagerVariantRaised =
themeManager.ActivatedTheme?.IsDarkMode == true;
});
try
{
application.SetDarkThemeMode(false);
Dispatcher.UIThread.RunJobs();
application.ActualThemeVariantChanged += handler;
application.SetDarkThemeMode(true);
Dispatcher.UIThread.RunJobs();
actualThemeVariantChangedCount.ShouldBeGreaterThan(0);
isDarkModeWhenThemeManagerVariantRaised.ShouldBeTrue();
application.ActualThemeVariant.ShouldNotBe(ThemeVariant.Dark);
themeManager.ActivatedTheme
.ShouldNotBeNull()
.Algorithms
.ShouldContain(ThemeAlgorithm.Dark);
var viewer = new GalleryCodeViewer
{
CodeText = "<StackPanel Margin=\"20\" Spacing=\"12\" />",
Language = "axaml"
};
ShowInWindow(viewer, _ =>
{
viewer.ActualThemeVariant.ShouldNotBe(ThemeVariant.Dark);
themeManager.ActivatedTheme
.ShouldNotBeNull()
.IsDarkMode
.ShouldBeTrue();
GetCurrentSyntaxTheme(viewer).ShouldBe(ThemeName.DarkPlus);
});
}
finally
{
application.ActualThemeVariantChanged -= handler;
application.SetDarkThemeMode(previousDarkMode);
Dispatcher.UIThread.RunJobs();
}
}
[Fact]
public void GalleryCodeViewer_Updates_TextMate_Theme_When_AtomUI_Dark_Mode_Changes()
{
var application = Application.Current!;
var previousDarkMode = application.IsDarkThemeMode();
try
{
application.SetDarkThemeMode(false);
Dispatcher.UIThread.RunJobs();
var viewer = new GalleryCodeViewer
{
CodeText = "<StackPanel Margin=\"20\" Spacing=\"12\" />",
Language = "axaml"
};
ShowInWindow(viewer, _ =>
{
GetCurrentSyntaxTheme(viewer).ShouldBe(ThemeName.LightPlus);
application.SetDarkThemeMode(true);
Dispatcher.UIThread.RunJobs();
GetCurrentSyntaxTheme(viewer).ShouldBe(ThemeName.DarkPlus);
GetAxamlTagNameColor(viewer).ShouldBe("#569CD6", StringCompareShould.IgnoreCase);
application.SetDarkThemeMode(false);
Dispatcher.UIThread.RunJobs();
GetCurrentSyntaxTheme(viewer).ShouldBe(ThemeName.LightPlus);
});
}
finally
{
application.SetDarkThemeMode(previousDarkMode);
Dispatcher.UIThread.RunJobs();
}
}
[Fact]
public void GalleryCodeViewer_Line_Number_Text_Keeps_Distance_From_Separator()
{
var viewer = new GalleryCodeViewer
{
CodeText = string.Join("\n", Enumerable.Range(1, 8).Select(index => $"Line {index}")),
Language = "text"
};
ShowInWindow(viewer, editor =>
{
var lineNumberMargin = editor.GetVisualDescendants()
.OfType<LineNumberMargin>()
.Single();
var separator = editor.TextArea.LeftMargins.Single(DottedLineMargin.IsDottedLineMargin);
var lineNumberRightPoint = lineNumberMargin.TranslatePoint(
new Point(lineNumberMargin.Bounds.Width, 0),
editor);
var separatorLeftPoint = separator.TranslatePoint(new Point(), editor);
lineNumberRightPoint.ShouldNotBeNull();
separatorLeftPoint.ShouldNotBeNull();
(separatorLeftPoint.Value.X - lineNumberRightPoint.Value.X).ShouldBeGreaterThanOrEqualTo(8);
});
}
[Fact]
public void GalleryCodeViewer_Uses_AtomUI_ContextMenu_For_Copying_Selected_Code()
{
var viewer = new GalleryCodeViewer
{
CodeText = "public class Demo { }",
Language = "csharp"
};
ShowInWindow(viewer, editor =>
{
var contextMenu = editor.ContextMenu.ShouldBeOfType<AtomUIContextMenu>();
var copyItem = contextMenu.Items
.OfType<AtomUIMenuItem>()
.Single();
copyItem.Header.ShouldBe("Copy");
copyItem.IsEnabled.ShouldBeFalse();
editor.Select(0, 6);
Dispatcher.UIThread.RunJobs();
copyItem.IsEnabled.ShouldBeTrue();
});
}
[Fact]
public async Task GalleryCodeViewer_ContextMenu_Copy_Writes_Selected_Code_To_Clipboard()
{
var viewer = new GalleryCodeViewer
{
CodeText = "public class Demo { }",
Language = "csharp"
};
await ShowInWindowAsync(viewer, async editor =>
{
editor.Select(0, 6);
Dispatcher.UIThread.RunJobs();
var copyItem = editor.ContextMenu
.ShouldBeOfType<AtomUIContextMenu>()
.Items
.OfType<AtomUIMenuItem>()
.Single();
copyItem.RaiseEvent(new RoutedEventArgs(AtomUIMenuItem.ClickEvent, copyItem));
await WaitForClipboardTextAsync(editor, "public");
});
}
[Fact]
public void DrawerContent_Renders_Tab_Header_Per_Snippet()
{
var group = new ShowCaseCodeSnippetGroup(
"Basic",
new[]
{
CreateSnippet("AXAML", "axaml", "<Button Content=\"Primary\" />"),
CreateSnippet("Code-behind", "csharp", "private void HandleClick() { }"),
CreateSnippet("ViewModel", "csharp", "public string SelectedValue { get; set; }")
});
ShowDrawerContentInWindow(group, content =>
{
var tabControl = content.GetVisualDescendants()
.OfType<DesktopTabControl>()
.Single();
tabControl.Items
.OfType<DesktopTabItem>()
.Select(static tabItem => tabItem.Header)
.ShouldBe(new object?[] { "AXAML", "Code-behind", "ViewModel" });
});
}
[Fact]
public void DrawerContent_Creates_Viewers_Lazily_Per_Selected_Tab()
{
var group = new ShowCaseCodeSnippetGroup(
"Basic",
new[]
{
CreateSnippet("AXAML", "axaml", "<Button Content=\"Primary\" />"),
CreateSnippet("Code-behind", "csharp", "private void HandleClick() { }"),
CreateSnippet("ViewModel", "csharp", "public string SelectedValue { get; set; }")
});
ShowDrawerContentInWindow(group, content =>
{
var tabControl = content.GetVisualDescendants()
.OfType<DesktopTabControl>()
.Single();
var tabItems = tabControl.Items.OfType<DesktopTabItem>().ToArray();
// Only the initially selected tab materializes a viewer.
tabItems[0].Content.ShouldBeOfType<GalleryCodeViewer>();
tabItems[1].Content.ShouldBeNull();
tabItems[2].Content.ShouldBeNull();
tabControl.SelectedIndex = 2;
Dispatcher.UIThread.RunJobs();
// Selecting another tab materializes its viewer; the untouched tab stays empty.
tabItems[2].Content.ShouldBeOfType<GalleryCodeViewer>();
tabItems[1].Content.ShouldBeNull();
});
}
[Fact]
public void DrawerContent_Shows_Placeholder_When_No_Snippets_Available()
{
var key = new ShowCaseCodeSnippetKey(
"AtomUIGallery.ShowCases.General.Button.Views.ButtonShowCase",
"ExamplesContent",
0);
ShowDrawerContentInWindow(null, key, content =>
{
content.GetVisualDescendants()
.OfType<DesktopTabControl>()
.ShouldBeEmpty();
content.GetVisualDescendants()
.OfType<TextBlock>()
.Select(static textBlock => textBlock.Text)
.ShouldContain(text => text != null && text.Contains("No source snippet found"));
});
}
private static ShowCaseCodeSnippet CreateSnippet(string tabTitle, string language, string text)
{
return new ShowCaseCodeSnippet(tabTitle, language, text, SourceFilePath: null, StartLine: 1, EndLine: 1);
}
private static void ShowDrawerContentInWindow(ShowCaseCodeSnippetGroup group,
Action<GalleryShowCaseCodeDrawerContent> verify)
{
var key = new ShowCaseCodeSnippetKey("Demo", "ExamplesContent", 0);
ShowDrawerContentInWindow(group, key, verify);
}
private static void ShowDrawerContentInWindow(ShowCaseCodeSnippetGroup? group,
ShowCaseCodeSnippetKey key,
Action<GalleryShowCaseCodeDrawerContent> verify)
{
var content = new GalleryShowCaseCodeDrawerContent(group, key);
var window = new Window
{
Width = 640,
Height = 480,
Content = content
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
content.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
verify(content);
}
finally
{
window.Close();
Dispatcher.UIThread.RunJobs();
content.Dispose();
}
}
private static void ShowInWindow(GalleryCodeViewer viewer, Action<TextEditor> verify, Control? content = null)
{
var window = new Window
{
Width = 640,
Height = 480,
Content = content ?? viewer
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
viewer.ApplyTemplate();
var editor = viewer.GetVisualDescendants()
.OfType<TextEditor>()
.Single();
editor.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
verify(editor);
}
finally
{
window.Close();
Dispatcher.UIThread.RunJobs();
viewer.Dispose();
}
}
private static async Task ShowInWindowAsync(GalleryCodeViewer viewer,
Func<TextEditor, Task> verify,
Control? content = null)
{
var window = new Window
{
Width = 640,
Height = 480,
Content = content ?? viewer
};
try
{
window.Show();
Dispatcher.UIThread.RunJobs();
viewer.ApplyTemplate();
var editor = viewer.GetVisualDescendants()
.OfType<TextEditor>()
.Single();
editor.ApplyTemplate();
Dispatcher.UIThread.RunJobs();
await verify(editor);
}
finally
{
window.Close();
Dispatcher.UIThread.RunJobs();
viewer.Dispose();
}
}
private static async Task WaitForClipboardTextAsync(Control control, string expectedText)
{
var clipboard = TopLevel.GetTopLevel(control)?.Clipboard;
clipboard.ShouldNotBeNull();
string? actualText = null;
for (var i = 0; i < 20; i++)
{
Dispatcher.UIThread.RunJobs();
using var dataTransfer = await clipboard!.TryGetInProcessDataAsync();
actualText = dataTransfer is null
? null
: await dataTransfer.TryGetValueAsync(DataFormat.Text);
if (actualText == expectedText)
{
return;
}
await Task.Delay(10);
}
actualText.ShouldBe(expectedText);
}
private static string GetTextMateThemeColor(GalleryCodeViewer viewer, string colorKey)
{
var installationField = typeof(GalleryCodeViewer).GetField(
"_textMateInstallation",
BindingFlags.Instance | BindingFlags.NonPublic);
installationField.ShouldNotBeNull();
var installation = installationField.GetValue(viewer);
installation.ShouldNotBeNull();
var arguments = new object?[] { colorKey, null };
var result = installation.GetType()
.GetMethod("TryGetThemeColor")!
.Invoke(installation, arguments);
result.ShouldBe(true);
return arguments[1].ShouldBeOfType<string>();
}
private static ThemeName GetCurrentSyntaxTheme(GalleryCodeViewer viewer)
{
var syntaxThemeField = typeof(GalleryCodeViewer).GetField(
"_currentSyntaxTheme",
BindingFlags.Instance | BindingFlags.NonPublic);
syntaxThemeField.ShouldNotBeNull();
return syntaxThemeField.GetValue(viewer).ShouldBeOfType<ThemeName>();
}
private static Avalonia.Controls.ScrollViewer GetEditorScrollViewer(TextEditor editor)
{
var scrollViewerProperty = typeof(TextEditor).GetProperty(
"ScrollViewer",
BindingFlags.Instance | BindingFlags.NonPublic);
scrollViewerProperty.ShouldNotBeNull();
return scrollViewerProperty.GetValue(editor).ShouldBeOfType<Avalonia.Controls.ScrollViewer>();
}
private static string? GetAxamlTagNameColor(GalleryCodeViewer viewer)
{
return GetAxamlTokenColor(viewer, "StackPanel");
}
private static string? GetAxamlTokenColor(GalleryCodeViewer viewer, string tokenText)
{
const string lineText = "<StackPanel Margin=\"20\" Spacing=\"12\">";
var registryOptions = new RegistryOptions(GetCurrentSyntaxTheme(viewer));
var registry = new Registry(registryOptions);
var grammar = registry.LoadGrammar(registryOptions.GetScopeByExtension(".xml"));
var theme = registry.GetTheme();
var tokens = grammar.TokenizeLine(lineText).Tokens;
var token = tokens.Single(token => lineText.Substring(token.StartIndex, token.Length) == tokenText);
var foregroundColorId = 0;
foreach (var rule in theme.Match(token.Scopes))
{
if (foregroundColorId == 0 && rule.foreground > 0)
{
foregroundColorId = rule.foreground;
}
}
return foregroundColorId > 0 ? theme.GetColor(foregroundColorId) : null;
}
}