diff --git a/Examples/UICatalog/Scenarios/Deepdives.cs b/Examples/UICatalog/Scenarios/Deepdives.cs index f4f381f460..b49efb6138 100644 --- a/Examples/UICatalog/Scenarios/Deepdives.cs +++ b/Examples/UICatalog/Scenarios/Deepdives.cs @@ -58,10 +58,7 @@ public override void Main () Height = Dim.Fill (1) }; - _markdownView = new Markdown - { - Width = Dim.Fill (), Height = Dim.Fill (), SyntaxHighlighter = new TextMateSyntaxHighlighter (ThemeName.Abbys), UseThemeBackground = true - }; + _markdownView = new Markdown { Width = Dim.Fill (), Height = Dim.Fill (), SyntaxHighlighter = new TextMateSyntaxHighlighter () }; _markdownView.ViewportSettings |= ViewportSettingsFlags.HasHorizontalScrollBar; @@ -114,7 +111,13 @@ public override void Main () Shortcut contentWidthShortcut = new () { CommandView = _contentWidthUpDown, Text = "Content Width" }; - DropDownList themeDropDown = new () { ReadOnly = true, CanFocus = false, Value = ThemeName.Abbys, Autocomplete = null }; + DropDownList themeDropDown = new () + { + ReadOnly = true, + CanFocus = false, + Value = (Enum.TryParse (_markdownView.SyntaxHighlighter.ThemeName, out ThemeName theme) ? theme : ThemeName.DarkPlus), + Autocomplete = null + }; themeDropDown.ValueChanged += (_, e) => { @@ -123,18 +126,25 @@ public override void Main () return; } - TextMateSyntaxHighlighter highlighter = new (themeName); - _markdownView.SyntaxHighlighter = highlighter; - - // Force re-layout so code blocks pick up new theme - string text = _markdownView.Text; - _markdownView.Text = string.Empty; - _markdownView.Text = text; + _markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (themeName); }; Shortcut themeShortcut = new () { Text = "_Theme:", CommandView = themeDropDown, MouseHighlightStates = MouseState.None }; - CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = CheckState.UnChecked }; + // Auto-select a light or dark syntax theme based on the terminal's actual background color. + _app.Driver!.DefaultAttributeChanged += (_, e) => + { + if (_markdownView is null || e.NewValue is not { } attr) + { + return; + } + + ThemeName autoTheme = TextMateSyntaxHighlighter.GetThemeForBackground (attr.Background); + _markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (autoTheme); + themeDropDown.Value = autoTheme; + }; + + CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = _markdownView.UseThemeBackground ? CheckState.Checked : CheckState.UnChecked }; themeBgCheckBox.ValueChanged += (_, e) => { @@ -144,11 +154,6 @@ public override void Main () } _markdownView.UseThemeBackground = e.NewValue == CheckState.Checked; - - // Force re-layout - string text = _markdownView.Text; - _markdownView.Text = string.Empty; - _markdownView.Text = text; }; Shortcut themeBgShortcut = new () { CommandView = themeBgCheckBox }; diff --git a/Examples/UICatalog/Scenarios/MarkdownTester.cs b/Examples/UICatalog/Scenarios/MarkdownTester.cs index e9968ca31a..aae64f8b01 100644 --- a/Examples/UICatalog/Scenarios/MarkdownTester.cs +++ b/Examples/UICatalog/Scenarios/MarkdownTester.cs @@ -14,7 +14,14 @@ public override void Main () using IApplication app = Application.Create (); app.Init (); - Window window = new () { Title = "Markdown Tester", Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + Window window = new () + { + Title = "Markdown Tester", + Width = Dim.Fill (), + Height = Dim.Fill (), + BorderStyle = LineStyle.None, + SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Accent) + }; // --- Source editor (top half) --- FrameView editorFrame = new () @@ -58,9 +65,7 @@ public override void Main () Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), - Text = Markdown.DefaultMarkdownSample, - SyntaxHighlighter = new TextMateSyntaxHighlighter (), - UseThemeBackground = true + SyntaxHighlighter = new TextMateSyntaxHighlighter () }; previewFrame.Add (preview); @@ -74,33 +79,49 @@ public override void Main () Shortcut quitShortcut = new () { Title = "Quit", Key = Key.Esc, Action = app.RequestStop }; - DropDownList themeDropDown = new () { Value = ThemeName.DarkPlus, ReadOnly = true, CanFocus = false }; + DropDownList themeDropDown = new () + { + Value = (preview.SyntaxHighlighter as TextMateSyntaxHighlighter)?.ThemeName ?? ThemeName.DarkPlus, + ReadOnly = true, + CanFocus = false + }; themeDropDown.ValueChanged += (_, e) => { - if (e.Value is { } themeName) + if (e.Value is not { } themeName) { - TextMateSyntaxHighlighter highlighter = new (themeName); - preview.SyntaxHighlighter = highlighter; - preview.Text = editor.Text; + return; } + preview.SyntaxHighlighter = new TextMateSyntaxHighlighter (themeName); + preview.Text = editor.Text; }; Shortcut themeShortcut = new () { Title = "Theme", CommandView = themeDropDown }; - CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = CheckState.UnChecked }; + // Auto-select a light or dark syntax theme based on the terminal's actual background color. + app.Driver!.DefaultAttributeChanged += (_, e) => + { + if (e.NewValue is not { } attr) + { + return; + } + + ThemeName autoTheme = TextMateSyntaxHighlighter.GetThemeForBackground (attr.Background); + preview.SyntaxHighlighter = new TextMateSyntaxHighlighter (autoTheme); + themeDropDown.Value = autoTheme; + }; - themeBgCheckBox.ValueChanged += (_, e) => - { - preview.UseThemeBackground = e.NewValue == CheckState.Checked; - preview.Text = editor.Text; - }; + CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = preview.UseThemeBackground ? CheckState.Checked : CheckState.UnChecked }; + + themeBgCheckBox.ValueChanged += (_, e) => { preview.UseThemeBackground = e.NewValue == CheckState.Checked; }; Shortcut themeBgShortcut = new () { CommandView = themeBgCheckBox }; statusBar.Add (themeShortcut, themeBgShortcut, quitShortcut); window.Add (statusBar); + preview.Text = editor.Text; + app.Run (window); window.Dispose (); } diff --git a/Examples/mdv/Program.cs b/Examples/mdv/Program.cs index 3b7f46f20d..0fb5953860 100644 --- a/Examples/mdv/Program.cs +++ b/Examples/mdv/Program.cs @@ -209,8 +209,7 @@ static void RunFullScreen (List files, ThemeName syntaxTheme) { Width = Dim.Fill (), Height = Dim.Fill (1), // leave room for StatusBar - SyntaxHighlighter = new TextMateSyntaxHighlighter (syntaxTheme), - UseThemeBackground = true + SyntaxHighlighter = new TextMateSyntaxHighlighter (syntaxTheme) }; // Vertical scrollbar is already enabled by MarkdownView constructor @@ -295,26 +294,30 @@ static void RunFullScreen (List files, ThemeName syntaxTheme) return; } - TextMateSyntaxHighlighter highlighter = new (themeName); - markdownView.SyntaxHighlighter = highlighter; - - string text = markdownView.Text; - markdownView.Text = string.Empty; - markdownView.Text = text; + markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (themeName); }; statusItems.Add (new Shortcut { Title = "Theme", CommandView = themeDropDown }); + // Auto-select a light or dark syntax theme based on the terminal's actual background color. + app.Driver!.DefaultAttributeChanged += (_, e) => + { + if (e.NewValue is not { } attr) + { + return; + } + + ThemeName autoTheme = TextMateSyntaxHighlighter.GetThemeForBackground (attr.Background); + markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (autoTheme); + themeDropDown.Value = autoTheme; + }; + // Theme background toggle CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = CheckState.Checked }; themeBgCheckBox.ValueChanged += (_, e) => { markdownView.UseThemeBackground = e.NewValue == CheckState.Checked; - - string text = markdownView.Text; - markdownView.Text = string.Empty; - markdownView.Text = text; }; statusItems.Add (new Shortcut { CommandView = themeBgCheckBox }); diff --git a/GitVersion.yml b/GitVersion.yml index 66f35ba108..e6a47f9790 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -18,7 +18,7 @@ # - develop: Develop branch for V2 # # Package Naming: -# - from develop: 2.1.0-develop.1 (minor version increments) +# - from develop: 2.0.0-develop.1 (patch version increments) # - from main (pre-release): 2.0.0-prealpha.1 or 2.0.0-beta.1 # - from main (release): 2.0.0 (patch version increments) # @@ -46,8 +46,8 @@ branches: regex: develop # Adds 'develop' as pre-release label (e.g., 2.1.0-develop.1) label: develop - # Increments minor version (x.y+1.z) on commits - increment: Minor + # Increments patch version (x.y.z+1) on commits + increment: Patch # No source branches specified as this is the root of development source-branches: [] # Indicates this branch feeds into release branches diff --git a/Terminal.Gui/Drawing/Markdown/ISyntaxHighlighter.cs b/Terminal.Gui/Drawing/Markdown/ISyntaxHighlighter.cs index c3b5540e3c..bbe97d98c5 100644 --- a/Terminal.Gui/Drawing/Markdown/ISyntaxHighlighter.cs +++ b/Terminal.Gui/Drawing/Markdown/ISyntaxHighlighter.cs @@ -22,6 +22,11 @@ public interface ISyntaxHighlighter /// void ResetState (); + /// + /// Gets the name of the currently active syntax highlighting theme. + /// + string ThemeName { get; } + /// /// Gets the default background color from the active syntax highlighting theme. /// Used by code block views to fill their viewport background consistently with diff --git a/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs b/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs index 098cd81993..cd69b6f828 100644 --- a/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs +++ b/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs @@ -30,7 +30,11 @@ public static Attribute GetAttributeForSegment (View view, StyledSegment segment { if (segment.Attribute is { } explicitAttr) { - return explicitAttr; + // When a caller-provided background override is present, apply it even to + // segments that carry an explicit attribute from the highlighter. This keeps + // the token foreground colours but ensures the background matches the fill + // colour of the containing code block / viewport. + return themeBackground is { } overrideBg ? explicitAttr with { Background = overrideBg } : explicitAttr; } // Use the provided theme background, or fall back to the view's normal background. diff --git a/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs b/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs index db24de0bc6..ae4fe69893 100644 --- a/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs +++ b/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs @@ -62,7 +62,7 @@ public class TextMateSyntaxHighlighter : ISyntaxHighlighter /// public TextMateSyntaxHighlighter (ThemeName theme = ThemeName.DarkPlus) { - CurrentThemeName = theme; + ThemeName = theme; _registryOptions = new RegistryOptions (theme); _registry = new Registry (_registryOptions); CacheThemeDefaults (); @@ -138,8 +138,11 @@ public IReadOnlyList Highlight (string code, string? language) /// A theme appropriate for the background luminance. public static ThemeName GetThemeForBackground (Color background) => background.IsDarkColor () ? ThemeName.DarkPlus : ThemeName.LightPlus; - /// Gets the that is currently active. - public ThemeName CurrentThemeName { get; private set; } + /// Gets the that is currently active. + public ThemeName ThemeName { get; private set; } + + /// + string ISyntaxHighlighter.ThemeName => ThemeName.ToString (); /// public Color? DefaultBackground => _defaultBackground; @@ -188,7 +191,7 @@ public IReadOnlyList Highlight (string code, string? language) /// The new VS Code theme to use. public void SetTheme (ThemeName theme) { - CurrentThemeName = theme; + ThemeName = theme; _registryOptions = new RegistryOptions (theme); _registry = new Registry (_registryOptions); _grammarCache.Clear (); diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 1e03d1cd88..6c276de933 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -279,10 +279,18 @@ private void OnSizeMonitorOnSizeChanged (object? _, SizeChangedEventArgs e) => /// public Attribute? DefaultAttribute { get; private set; } + /// + public event EventHandler>? DefaultAttributeChanged; + /// /// Sets the terminal's default attribute (queried via OSC 10/11). /// - internal void SetDefaultAttribute (Attribute attr) => DefaultAttribute = attr; + internal void SetDefaultAttribute (Attribute attr) + { + Attribute? old = DefaultAttribute; + DefaultAttribute = attr; + DefaultAttributeChanged?.Invoke (this, new ValueChangedEventArgs (old, attr)); + } /// public TerminalColorCapabilities? ColorCapabilities { get; private set; } diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index a6faa8946b..c549aae756 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -150,6 +150,11 @@ public interface IDriver : IDisposable /// Attribute? DefaultAttribute { get; } + /// + /// Raised when changes (e.g. after terminal color detection completes). + /// + event EventHandler>? DefaultAttributeChanged; + /// /// Gets the terminal's color capabilities as detected from environment variables. /// if detection has not been performed. diff --git a/Terminal.Gui/Views/Markdown/Markdown.cs b/Terminal.Gui/Views/Markdown/Markdown.cs index f408b12973..af8adfd52c 100644 --- a/Terminal.Gui/Views/Markdown/Markdown.cs +++ b/Terminal.Gui/Views/Markdown/Markdown.cs @@ -76,15 +76,41 @@ public MarkdownPipeline? MarkdownPipeline /// Gets or sets an optional syntax highlighter for fenced code blocks. /// An implementation, or for plain-text code blocks. - public ISyntaxHighlighter? SyntaxHighlighter { get; set; } + public ISyntaxHighlighter? SyntaxHighlighter + { + get; + set + { + if (ReferenceEquals (field, value)) + { + return; + } + + field = value; + InvalidateParsedAndLayout (); + } + } /// /// Gets or sets whether the view fills its background with the syntax highlighting theme's /// editor background color. When and a /// is set, the theme's is used for the - /// entire viewport, headings, body text, and table cells. Defaults to . + /// entire viewport, headings, body text, and table cells. Defaults to . /// - public bool UseThemeBackground { get; set; } + public bool UseThemeBackground + { + get; + set + { + if (field == value) + { + return; + } + + field = value; + InvalidateParsedAndLayout (); + } + } = true; /// /// Gets or sets whether heading lines include the # prefix (e.g. # , ## ). diff --git a/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs b/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs index d7e05df45c..33b78bc947 100644 --- a/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs +++ b/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs @@ -284,7 +284,7 @@ protected override bool OnDrawingContent (DrawContext? context) foreach (StyledSegment segment in segments) { - Attribute attr = MarkdownAttributeHelper.GetAttributeForSegment (this, segment, SyntaxHighlighter); + Attribute attr = MarkdownAttributeHelper.GetAttributeForSegment (this, segment, SyntaxHighlighter, codeBg); SetAttribute (attr); foreach (string grapheme in GraphemeHelper.GetGraphemes (segment.Text)) diff --git a/Terminal.Gui/Views/Markdown/MarkdownView.Layout.cs b/Terminal.Gui/Views/Markdown/MarkdownView.Layout.cs index 72c7b2457a..a0b7a7fe49 100644 --- a/Terminal.Gui/Views/Markdown/MarkdownView.Layout.cs +++ b/Terminal.Gui/Views/Markdown/MarkdownView.Layout.cs @@ -142,14 +142,34 @@ private void SyncCodeBlockViews () MarkdownCodeBlock codeBlock = new () { + SyntaxHighlighter = SyntaxHighlighter, StyledLines = codeLines, X = 0, Y = start, Width = Dim.Fill (), - ThemeBackground = UseThemeBackground ? SyntaxHighlighter?.DefaultBackground : null, ShowCopyButton = ShowCopyButtons }; + // When a syntax highlighter provides a default background, compute a + // slightly shifted variant and set it as the code block's ThemeBackground. + // This ensures code blocks are visually distinct from body text AND + // compatible with the highlighter's token foreground colors. + // + // We pass !isDark to GetDimmerColor so the bg shifts *away* from the + // body background: dark themes get a slightly lighter code block bg, + // light themes get a slightly darker one. Passing isDark (the intuitive + // direction) caused light-theme code blocks to wash out to medium gray + // because white (L≥90) hit the fallback in GetDimmerColor. + // + // We compute the color directly rather than using Scheme/VisualRole.Code + // because scheme resolution depends on view tree init state, but this + // code runs during layout before the new SubView is fully initialised. + if (SyntaxHighlighter?.DefaultBackground is { } highlighterBg) + { + bool isDark = highlighterBg.IsDarkColor (); + codeBlock.ThemeBackground = highlighterBg.GetDimmerColor (0.2, !isDark); + } + _codeBlockViews.Add (codeBlock); Add (codeBlock); } diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/AstLoweringTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/AstLoweringTests.cs index 325c52db30..dd5efd7ba8 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/AstLoweringTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/AstLoweringTests.cs @@ -689,6 +689,8 @@ public IReadOnlyList Highlight (string code, string? language) public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground => null; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownTableTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownTableTests.cs index cc16bae354..2bd04ceebf 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownTableTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownTableTests.cs @@ -711,6 +711,8 @@ private sealed class ThemeBgHighlighter (Color themeBg) : ISyntaxHighlighter public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground { get; } = themeBg; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownViewTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownViewTests.cs index 372687d07a..0bc438b415 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownViewTests.cs @@ -18,7 +18,7 @@ public void Constructor_Defaults () Assert.True (view.CanFocus); Assert.Equal (string.Empty, view.Text); Assert.Equal (0, view.LineCount); - Assert.False (view.UseThemeBackground); + Assert.True (view.UseThemeBackground); } [Fact] @@ -1251,11 +1251,31 @@ private sealed class ThemeBackgroundHighlighter (Color themeBg) : ISyntaxHighlig public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground { get; } = themeBg; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; } + /// + /// A mock highlighter that returns segments with explicit values, + /// simulating real TextMate-style tokenization where each token carries its own colors. + /// + private sealed class ExplicitAttributeHighlighter (Color tokenFg, Color tokenBg) : ISyntaxHighlighter + { + public IReadOnlyList Highlight (string code, string? language) + => [new (code, MarkdownStyleRole.CodeBlock, attribute: new Attribute (tokenFg, tokenBg))]; + + public void ResetState () { } + + public string ThemeName => string.Empty; + + public Color? DefaultBackground { get; } = tokenBg; + + public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; + } + #endregion #region Viewport scroll position @@ -1350,4 +1370,399 @@ Middle text. } #endregion + + #region CodeBlock background attribute tests + + // Copilot + + [Fact] + public void UseThemeBackground_True_CodeBlock_Is_Distinct_From_Body () + { + // Copilot + // When UseThemeBackground is true and a SyntaxHighlighter is set, the code block + // must have a background that is DISTINCT from the body (which uses DefaultBackground) + // but still derived from the theme (so token colors remain readable). + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (20, 6); + + Color themeBg = new (30, 30, 30); + ThemeBackgroundHighlighter highlighter = new (themeBg); + + using Runnable window = new (); + window.Width = Dim.Fill (); + window.Height = Dim.Fill (); + window.BorderStyle = LineStyle.None; + window.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = true, + Text = "Hello\n\n```\ncode\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 0 = "Hello" (body text with theme bg) + Color mainBg = contents! [0, 0].Attribute!.Value.Background; + + // Row 2 = code block line "code" (should be distinct from body bg) + Color codeBg = contents [2, 0].Attribute!.Value.Background; + + Assert.NotEqual (mainBg, codeBg); + + app.Dispose (); + } + + [Fact] + public void UseThemeBackground_True_CodeBlock_Bg_Derives_From_Theme_Not_Scheme () + { + // Copilot + // When UseThemeBackground is true with a light theme, the code block should NOT + // use the dark VisualRole.Code from the view's (possibly dark) scheme. Instead + // it should use a dimmed variant of the highlighter's DefaultBackground. + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 10); + + Color lightThemeBg = new (250, 250, 250); // Light theme bg + ThemeBackgroundHighlighter highlighter = new (lightThemeBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.Black, new Color (0, 0, 128)))); // Dark scheme bg + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = true, + Text = "Hello\n\n```csharp\nvar x = 1;\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.Black, new Color (0, 0, 128)))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 2 = code block line — bg should be light (derived from light theme), not dark + Color codeBg = contents! [2, 0].Attribute!.Value.Background; + Assert.False (codeBg.IsDarkColor (), $"Code block bg {codeBg} should be light (derived from light theme), not dark"); + + // Code block bg should be the dimmed variant of the theme bg + bool isDark = lightThemeBg.IsDarkColor (); + Color expectedDimmed = lightThemeBg.GetDimmerColor (0.2, !isDark); + Assert.Equal (expectedDimmed, codeBg); + + app.Dispose (); + } + + [Fact] + public void UseThemeBackground_False_CodeBlock_Text_Matches_Fill_Background () + { + // Copilot + // When UseThemeBackground is false, the code block text segments should use + // the same background as the code block fill (Code role background). + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (20, 6); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + UseThemeBackground = false, + Text = "Hello\n\n```\ncode\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 2 = code block line "code" + // The text cell (col 0, 'c') background should match the fill cell (col 10, empty) background + Color textBg = contents! [2, 0].Attribute!.Value.Background; + Color fillBg = contents [2, 10].Attribute!.Value.Background; + + Assert.Equal (textBg, fillBg); + + // The code block background should also differ from the main content background + Color mainBg = contents [0, 0].Attribute!.Value.Background; + Assert.NotEqual (mainBg, textBg); + + app.Dispose (); + } + + [Fact] + public void CodeBlock_With_Highlighter_Bg_Derives_From_Theme_Regardless_Of_UseThemeBackground () + { + // Copilot + // When a SyntaxHighlighter is set (with DefaultBackground), the code block's scheme + // is overridden so VisualRole.Code derives from the highlighter bg, not the view's + // scheme bg. This ensures token colors remain readable on a compatible background. + // This applies regardless of UseThemeBackground. + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 10); + + Color lightThemeBg = new (250, 250, 250); + ThemeBackgroundHighlighter highlighter = new (lightThemeBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.Black, new Color (0, 0, 128)))); // Dark scheme + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = false, // Body uses scheme bg, but code block should still use theme-derived bg + Text = "Hello\n\n```csharp\nvar x = 1;\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.Black, new Color (0, 0, 128)))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 2 = code block — bg should be light (derived from light theme), not the dark scheme bg + Color codeBg = contents! [2, 0].Attribute!.Value.Background; + Assert.False (codeBg.IsDarkColor (), $"Code block bg {codeBg} should be light (theme-derived), not dark (scheme-derived)"); + + // Code block fill and text should have matching bg + Color fillBg = contents [2, 15].Attribute!.Value.Background; + Assert.Equal (codeBg, fillBg); + + app.Dispose (); + } + + [Fact] + public void CodeBlock_SyntaxHighlighter_Is_Passed_To_SubView () + { + // Copilot + // The MarkdownCodeBlock SubViews created by SyncCodeBlockViews must receive + // the parent Markdown view's SyntaxHighlighter so that GetAttributeForSegment + // can query the highlighter for scope-specific attributes. + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 10); + + Color themeBg = new (250, 250, 250); + ThemeBackgroundHighlighter highlighter = new (themeBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = true, + Text = "```\ncode\n```" + }; + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + // Find the MarkdownCodeBlock SubView + MarkdownCodeBlock? codeBlockView = null; + + foreach (View sub in mv.SubViews) + { + if (sub is not MarkdownCodeBlock cb) + { + continue; + } + codeBlockView = cb; + + break; + } + + Assert.NotNull (codeBlockView); + + // The code block must have the parent's SyntaxHighlighter set + Assert.Same (highlighter, codeBlockView.SyntaxHighlighter); + + app.Dispose (); + } + + [Fact] + public void CodeBlock_With_ExplicitAttribute_Highlighter_Fill_Matches_Text_Bg () + { + // Copilot + // A real TextMate highlighter returns segments with explicit Attribute (tokenFg, tokenBg). + // The code block fill bg (from OnClearingViewport) is a dimmed variant of DefaultBackground. + // GetAttributeForSegment must override the explicit attribute's bg so text bg matches fill bg. + // Without the fix, text bg = raw theme bg, fill bg = dimmed theme bg → mismatch. + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 8); + + Color tokenFg = new (200, 200, 200); + Color tokenBg = new (30, 30, 30); + ExplicitAttributeHighlighter highlighter = new (tokenFg, tokenBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = true, + Text = "Hello\n\n```csharp\nvar x = 1;\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 2 = code block line "var x = 1;" + // Text cell bg (col 0) must match fill cell bg (col 20, empty space) + Color textBg = contents! [2, 0].Attribute!.Value.Background; + Color fillBg = contents [2, 20].Attribute!.Value.Background; + + Assert.Equal (fillBg, textBg); + + // The bg should be the dimmed variant of the theme bg, NOT the raw theme bg + bool isDark = tokenBg.IsDarkColor (); + Color expectedDimmed = tokenBg.GetDimmerColor (0.2, !isDark); + Assert.Equal (expectedDimmed, textBg); + + // Token foreground should be preserved + Color actualFg = contents [2, 0].Attribute!.Value.Foreground; + Assert.Equal (tokenFg, actualFg); + + app.Dispose (); + } + + [Fact] + public void Setting_SyntaxHighlighter_After_Text_Updates_CodeBlock_Bg () + { + // Copilot + // Setting SyntaxHighlighter on a MarkdownView that already has Text must + // invalidate layout so code blocks pick up the new highlighter's theme bg. + // This should NOT require re-setting Text (that was a hack). + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 8); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + Text = "Hello\n\n```csharp\nvar x = 1;\n```" + }; + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Before: code block bg uses VisualRole.Code from scheme + Color bgBefore = contents! [2, 0].Attribute!.Value.Background; + + // Now set a highlighter with a light theme bg + Color lightThemeBg = new (250, 250, 250); + ThemeBackgroundHighlighter highlighter = new (lightThemeBg); + mv.SyntaxHighlighter = highlighter; + + // Re-draw WITHOUT re-setting Text + app.LayoutAndDraw (); + + contents = app.Driver.Contents; + + // After: code block bg should be derived from the light theme + Color bgAfter = contents! [2, 0].Attribute!.Value.Background; + + Assert.NotEqual (bgBefore, bgAfter); + Assert.False (bgAfter.IsDarkColor (), $"Code block bg {bgAfter} should be light (theme-derived) after setting highlighter"); + + app.Dispose (); + } + + [Fact] + public void Setting_UseThemeBackground_After_Text_Updates_Without_ReSettingText () + { + // Copilot + // Changing UseThemeBackground must invalidate layout so body and code blocks update. + // This should NOT require re-setting Text (that was a hack). + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 8); + + Color themeBg = new (30, 30, 30); + ThemeBackgroundHighlighter highlighter = new (themeBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = false, + Text = "Hello\n\n```\ncode\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Body bg when UseThemeBackground = false → scheme bg (Blue) + Color bodyBgBefore = contents! [0, 0].Attribute!.Value.Background; + Assert.Equal (Color.Blue, bodyBgBefore); + + // Now toggle UseThemeBackground to true WITHOUT re-setting Text + mv.UseThemeBackground = true; + app.LayoutAndDraw (); + + contents = app.Driver.Contents; + + // Body bg should now use the theme bg (dark, 30,30,30) + Color bodyBgAfter = contents! [0, 0].Attribute!.Value.Background; + Assert.NotEqual (bodyBgBefore, bodyBgAfter); + Assert.Equal (themeBg, bodyBgAfter); + + app.Dispose (); + } + + #endregion } diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/SyntaxHighlighterPipelineTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/SyntaxHighlighterPipelineTests.cs index 72580c1b73..409fdf928b 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/SyntaxHighlighterPipelineTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/SyntaxHighlighterPipelineTests.cs @@ -189,6 +189,8 @@ public void ResetState () ResetStateCallCount++; } + public string ThemeName => string.Empty; + public Color? DefaultBackground => null; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; @@ -200,6 +202,8 @@ private sealed class ExplicitAttributeHighlighter (Attribute attr) : ISyntaxHigh public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground => null; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; @@ -275,6 +279,8 @@ private sealed class ScopeAwareHighlighter (MarkdownStyleRole targetRole, Attrib public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground => null; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => role == targetRole ? attr : null; @@ -287,6 +293,8 @@ private sealed class ThemeBackgroundHighlighter (MarkdownStyleRole targetRole, A public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground { get; } = themeBg; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => role == targetRole ? attr : null; diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/TextMateSyntaxHighlighterTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/TextMateSyntaxHighlighterTests.cs index fd46bbe000..16ec2f15cf 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/TextMateSyntaxHighlighterTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/TextMateSyntaxHighlighterTests.cs @@ -364,11 +364,11 @@ public void SetTheme_Updates_DefaultBackground () // --- ThemeName property --- Copilot [Fact] - public void Constructor_Sets_CurrentThemeName () + public void Constructor_Sets_ThemeName () { // Copilot TextMateSyntaxHighlighter highlighter = new (ThemeName.Monokai); - Assert.Equal (ThemeName.Monokai, highlighter.CurrentThemeName); + Assert.Equal (ThemeName.Monokai, highlighter.ThemeName); } [Fact] @@ -376,17 +376,17 @@ public void Default_Constructor_Has_DarkPlus_ThemeName () { // Copilot TextMateSyntaxHighlighter highlighter = new (); - Assert.Equal (ThemeName.DarkPlus, highlighter.CurrentThemeName); + Assert.Equal (ThemeName.DarkPlus, highlighter.ThemeName); } [Fact] - public void SetTheme_Updates_CurrentThemeName () + public void SetTheme_Updates_ThemeName () { // Copilot TextMateSyntaxHighlighter highlighter = new (); - Assert.Equal (ThemeName.DarkPlus, highlighter.CurrentThemeName); + Assert.Equal (ThemeName.DarkPlus, highlighter.ThemeName); highlighter.SetTheme (ThemeName.SolarizedLight); - Assert.Equal (ThemeName.SolarizedLight, highlighter.CurrentThemeName); + Assert.Equal (ThemeName.SolarizedLight, highlighter.ThemeName); } }