Skip to content

Commit 39c5da3

Browse files
authored
Merge pull request #238 from gui-cs/release/v2.5.0
Release v2.5.0
2 parents 4278734 + c033cb3 commit 39c5da3

193 files changed

Lines changed: 867 additions & 255 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ ordering, `*.ans` must stay `binary` in `.gitattributes`, keep viewports small,
7171
Two NuGet packages with a strict dependency direction:
7272

7373
- **`src/Terminal.Gui.Editor`** — UI-framework-independent document model. Namespace `Terminal.Gui.Editor` and subnamespaces. **Must not reference Terminal.Gui.** Holds the rope-backed `TextDocument`, `DocumentLine`, `TextAnchor`, `UndoStack`, `ITextSource`, `TextSegment`, the `Rope`, and supporting utility types. Lifted from AvaloniaEdit (see fork policy below) — `Document/` and `Utils/` are landed; `Folding/`, `Search/`, `Indentation/`, `Highlighting/` are follow-up phases per `specs/00-plan.md`.
74-
- **`src/Terminal.Gui.Editor`** — the `Editor : View` and cell-grid rendering pipeline. Namespace `Terminal.Gui.Views` (matches Terminal.Gui convention, deliberately not `Terminal.Gui.Editor`). References `Terminal.Gui` (version pinned via `$(TerminalGuiVersion)` in `Directory.Build.props`) and `Terminal.Gui.Editor`. Split into partials: `Editor.cs` (core: `Document`, `CaretOffset`, edit-tracking arithmetic, content-size + scroll), `Editor.Drawing.cs` (`OnDrawingContent` + cursor positioning), `Editor.Keyboard.cs` (`OnKeyDown` switch — navigation / editing / undo+redo). No selection / folding / highlighting / multi-caret yet.
74+
- **`src/Terminal.Gui.Editor`** — the `Editor : View` and cell-grid rendering pipeline. Namespace `Terminal.Gui.Editor` (all public types in the assembly live under `Terminal.Gui.Editor.*`). References `Terminal.Gui` (version pinned via `$(TerminalGuiVersion)` in `Directory.Build.props`). Split into partials: `Editor.cs` (core: `Document`, `CaretOffset`, edit-tracking arithmetic, content-size + scroll), `Editor.Drawing.cs` (`OnDrawingContent` + cursor positioning), `Editor.Keyboard.cs` (`OnKeyDown` switch — navigation / editing / undo+redo). Subnamespaces: `Terminal.Gui.Editor.Document`, `Terminal.Gui.Editor.Rendering`, `Terminal.Gui.Editor.Highlighting`, `Terminal.Gui.Editor.Indentation`, `Terminal.Gui.Editor.Completion`.
7575
- **`examples/ted`** — standalone TG demo app exercising `Editor`. Not packed; not a NuGet artifact. Has a File menu, the `Editor` View, and a status bar; grows with the View. Run via `dotnet run --project examples/ted`.
7676

7777
The boundary matters: anything that takes a dependency on `Terminal.Gui` types belongs in `Terminal.Gui.Editor`, never in `Terminal.Gui.Editor`.
@@ -205,7 +205,7 @@ private void ExtendCaretBy (int delta)
205205
- **One public or internal type per file.** No nested types except inside the file that owns the outer type, and only when the nested type is a private implementation detail (`DocumentLine.LineNode`-style). If a nested type grows interesting, promote it to its own file.
206206
- **No file longer than 1000 lines.** When a file approaches that, split — by partial class (`Editor.Drawing.cs`, `Editor.Mouse.cs`), by helper extraction, or by genuinely splitting the type. The cleanup hook does not enforce this; the reviewer does.
207207
- **C# 14 `extension` blocks**: prefer extension blocks over a static class full of `this`-prefixed extension methods when the extensions form a coherent group on a single receiver type.
208-
- **Namespace per folder.** `src/Terminal.Gui.Editor/Document/``Terminal.Gui.Document`; `src/Terminal.Gui.Editor/Rendering/``Terminal.Gui.Views.Rendering`. Don't put unrelated types in the same namespace just because they share a folder.
208+
- **Namespace per folder.** `src/Terminal.Gui.Editor/Document/``Terminal.Gui.Editor.Document`; `src/Terminal.Gui.Editor/Rendering/``Terminal.Gui.Editor.Rendering`. All namespaces in the Editor assembly are rooted under `Terminal.Gui.Editor`. Don't put unrelated types in the same namespace just because they share a folder.
209209
- **No static members on `View`-derived types.** A class that derives from `Terminal.Gui.View` (e.g. `Editor`) must not declare `static` members — not fields, not properties, not events, not even "harmless" caches or lookup tables. Terminal.Gui's `Application` lifetime is per-instance (see "Testing tiers"); static state on a View is process-global, survives across `IApplication` instances, and silently couples otherwise-independent windows and parallel tests (the canonical cause of parallel-test hangs). Shared/lookup data lives in a dedicated non-View type (e.g. `XshdRoleMap`), exposed read-only (`private` + `FrozenDictionary`/`IReadOnlyXxx`), and is injected or queried — never hung off the View. `const` is the only exception (it is not state). This is a hard rule; a reviewer blocks on it.
210210

211211
### Testing convention

benchmarks/Terminal.Gui.Editor.Benchmarks/DocumentAccessBenchmarks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using BenchmarkDotNet.Attributes;
2-
using Terminal.Gui.Document;
2+
using Terminal.Gui.Editor.Document;
33

44
namespace Terminal.Gui.Editor.Benchmarks;
55

benchmarks/Terminal.Gui.Editor.Benchmarks/EditorHarness.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
using Terminal.Gui.App;
2-
using Terminal.Gui.Document;
32
using Terminal.Gui.Drawing;
43
using Terminal.Gui.Drivers;
4+
using Terminal.Gui.Editor.Document;
55
using Terminal.Gui.Testing;
66
using Terminal.Gui.ViewBase;
77
using Terminal.Gui.Views;

benchmarks/Terminal.Gui.Editor.Benchmarks/FindBenchmarks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using BenchmarkDotNet.Attributes;
2-
using Terminal.Gui.Document;
3-
using Terminal.Gui.Document.Search;
2+
using Terminal.Gui.Editor.Document;
3+
using Terminal.Gui.Editor.Document.Search;
44

55
namespace Terminal.Gui.Editor.Benchmarks;
66

benchmarks/Terminal.Gui.Editor.Benchmarks/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using System.Diagnostics;
22
using System.Text.RegularExpressions;
33
using BenchmarkDotNet.Running;
4-
using Terminal.Gui.Document;
5-
using Terminal.Gui.Document.Search;
4+
using Terminal.Gui.Editor.Document;
5+
using Terminal.Gui.Editor.Document.Search;
66

77
if (args.Length > 0 && args[0] == "--quick-find")
88
{

benchmarks/Terminal.Gui.Editor.Benchmarks/ScrollingBenchmarks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using BenchmarkDotNet.Attributes;
2-
using Terminal.Gui.Document;
2+
using Terminal.Gui.Editor.Document;
33
using Terminal.Gui.Editor.Rendering;
44
using Attribute = Terminal.Gui.Drawing.Attribute;
55

benchmarks/Terminal.Gui.Editor.Benchmarks/VisualLineBuildBenchmarks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using BenchmarkDotNet.Attributes;
2-
using Terminal.Gui.Document;
2+
using Terminal.Gui.Editor.Document;
33
using Terminal.Gui.Editor.Rendering;
44
using Attribute = Terminal.Gui.Drawing.Attribute;
55

examples/prompt/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// outputs to stdout on Enter, exits silently on Esc.
33

44
using Terminal.Gui.App;
5-
using Terminal.Gui.Document;
65
using Terminal.Gui.Editor;
6+
using Terminal.Gui.Editor.Document;
77
using Terminal.Gui.Input;
88
using Terminal.Gui.ViewBase;
99
using Terminal.Gui.Views;

examples/ted/MarkdownPreview.cs

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
using System.Drawing;
2+
using Terminal.Gui.Drawing;
3+
using Terminal.Gui.Input;
4+
using Terminal.Gui.ViewBase;
5+
using Terminal.Gui.Views;
6+
using Attribute = Terminal.Gui.Drawing.Attribute;
7+
using Color = Terminal.Gui.Drawing.Color;
8+
9+
namespace Ted;
10+
11+
/// <summary>
12+
/// A <see cref="Markdown" /> subclass that highlights the rendered line(s) corresponding to a
13+
/// source line in the editor and raises <see cref="SourceLineClicked" /> when the user clicks
14+
/// in the preview, enabling click-to-navigate back to the editor.
15+
/// </summary>
16+
internal sealed class MarkdownPreview : Markdown
17+
{
18+
private int _highlightSourceLine = -1;
19+
private int _totalSourceLines;
20+
21+
/// <summary>
22+
/// Gets or sets the 0-based source line number to highlight in the preview.
23+
/// Set to -1 to clear the highlight.
24+
/// </summary>
25+
public int HighlightSourceLine
26+
{
27+
get => _highlightSourceLine;
28+
set
29+
{
30+
if (_highlightSourceLine == value)
31+
{
32+
return;
33+
}
34+
35+
_highlightSourceLine = value;
36+
SetNeedsDraw ();
37+
}
38+
}
39+
40+
/// <summary>
41+
/// Gets or sets the total number of source lines in the document.
42+
/// Used for proportional mapping between source lines and rendered lines.
43+
/// </summary>
44+
public int TotalSourceLines
45+
{
46+
get => _totalSourceLines;
47+
set
48+
{
49+
if (_totalSourceLines == value)
50+
{
51+
return;
52+
}
53+
54+
_totalSourceLines = value;
55+
SetNeedsDraw ();
56+
}
57+
}
58+
59+
/// <summary>
60+
/// Raised when the user clicks in the preview. The event arg carries the estimated 0-based
61+
/// source line number corresponding to the click position.
62+
/// </summary>
63+
public event EventHandler<SourceLineClickedEventArgs>? SourceLineClicked;
64+
65+
/// <summary>
66+
/// Maps a 0-based source line number to the corresponding rendered line index using
67+
/// proportional mapping.
68+
/// </summary>
69+
private int MapSourceToRendered (int sourceLine)
70+
{
71+
if (_totalSourceLines <= 1 || LineCount <= 0)
72+
{
73+
return 0;
74+
}
75+
76+
return (int)((long)sourceLine * (LineCount - 1) / (_totalSourceLines - 1));
77+
}
78+
79+
/// <summary>
80+
/// Maps a rendered line index back to an approximate 0-based source line number.
81+
/// </summary>
82+
private int MapRenderedToSource (int renderedLine)
83+
{
84+
if (LineCount <= 1 || _totalSourceLines <= 0)
85+
{
86+
return 0;
87+
}
88+
89+
return (int)((long)renderedLine * (_totalSourceLines - 1) / (LineCount - 1));
90+
}
91+
92+
/// <inheritdoc />
93+
protected override bool OnDrawingContent (DrawContext? context)
94+
{
95+
var result = base.OnDrawingContent (context);
96+
97+
DrawHighlightBar ();
98+
99+
return result;
100+
}
101+
102+
/// <summary>
103+
/// Paints a subtle background highlight on the rendered row corresponding to
104+
/// <see cref="HighlightSourceLine" />.
105+
/// </summary>
106+
private void DrawHighlightBar ()
107+
{
108+
if (_highlightSourceLine < 0 || _totalSourceLines <= 0 || LineCount <= 0)
109+
{
110+
return;
111+
}
112+
113+
var renderedLine = MapSourceToRendered (_highlightSourceLine);
114+
115+
// Check if the highlighted line is within the visible viewport.
116+
var drawRow = renderedLine - Viewport.Y;
117+
118+
if (drawRow < 0 || drawRow >= Viewport.Height)
119+
{
120+
return;
121+
}
122+
123+
// Compute a highlight attribute: shift the background slightly for contrast.
124+
Attribute normalAttr = GetAttributeForRole (VisualRole.Normal);
125+
Color highlightBg = normalAttr.Background.IsDarkColor ()
126+
? normalAttr.Background.GetDimmerColor (0.25, false)
127+
: normalAttr.Background.GetDimmerColor (0.15, true);
128+
129+
// Paint the highlight over the full viewport width by reading existing screen content
130+
// and re-drawing with the highlight background.
131+
Cell[,]? contents = ScreenContents;
132+
133+
if (contents is null)
134+
{
135+
return;
136+
}
137+
138+
// Map the viewport-relative draw row to screen coordinates so we read the correct
139+
// cells regardless of horizontal scroll position (Viewport.X).
140+
Point screenOrigin = ViewportToScreen (new Point (0, drawRow));
141+
var screenRow = screenOrigin.Y;
142+
var screenStartCol = screenOrigin.X;
143+
144+
for (var col = 0; col < Viewport.Width; col++)
145+
{
146+
var sc = screenStartCol + col;
147+
148+
if (screenRow < 0 || screenRow >= contents.GetLength (0) || sc < 0 || sc >= contents.GetLength (1))
149+
{
150+
continue;
151+
}
152+
153+
Cell cell = contents[screenRow, sc];
154+
var grapheme = string.IsNullOrEmpty (cell.Grapheme) ? " " : cell.Grapheme;
155+
156+
// Preserve the foreground color from the original cell but apply highlight background.
157+
Attribute cellAttr = (cell.Attribute ?? normalAttr) with { Background = highlightBg };
158+
SetAttribute (cellAttr);
159+
AddStr (col, drawRow, grapheme);
160+
}
161+
}
162+
163+
/// <inheritdoc />
164+
protected override bool OnMouseEvent (Mouse mouse)
165+
{
166+
if (mouse.Flags.HasFlag (MouseFlags.LeftButtonClicked) && mouse.Position is { } pos)
167+
{
168+
var contentRow = Viewport.Y + pos.Y;
169+
170+
if (contentRow >= 0 && contentRow < LineCount)
171+
{
172+
var sourceLine = MapRenderedToSource (contentRow);
173+
SourceLineClicked?.Invoke (this, new SourceLineClickedEventArgs (sourceLine));
174+
}
175+
}
176+
177+
return base.OnMouseEvent (mouse);
178+
}
179+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Ted;
2+
3+
/// <summary>Event args for <see cref="MarkdownPreview.SourceLineClicked" />.</summary>
4+
internal sealed class SourceLineClickedEventArgs : EventArgs
5+
{
6+
public SourceLineClickedEventArgs (int sourceLine)
7+
{
8+
SourceLine = sourceLine;
9+
}
10+
11+
/// <summary>Gets the estimated 0-based source line number that was clicked.</summary>
12+
public int SourceLine { get; }
13+
}

0 commit comments

Comments
 (0)