Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ ordering, `*.ans` must stay `binary` in `.gitattributes`, keep viewports small,
Two NuGet packages with a strict dependency direction:

- **`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`.
- **`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.
- **`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`.
- **`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`.

The boundary matters: anything that takes a dependency on `Terminal.Gui` types belongs in `Terminal.Gui.Editor`, never in `Terminal.Gui.Editor`.
Expand Down Expand Up @@ -205,7 +205,7 @@ private void ExtendCaretBy (int delta)
- **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.
- **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.
- **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.
- **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.
- **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.
- **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.

### Testing convention
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using BenchmarkDotNet.Attributes;
using Terminal.Gui.Document;
using Terminal.Gui.Editor.Document;

namespace Terminal.Gui.Editor.Benchmarks;

Expand Down
2 changes: 1 addition & 1 deletion benchmarks/Terminal.Gui.Editor.Benchmarks/EditorHarness.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Terminal.Gui.App;
using Terminal.Gui.Document;
using Terminal.Gui.Drawing;
using Terminal.Gui.Drivers;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Testing;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
Expand Down
4 changes: 2 additions & 2 deletions benchmarks/Terminal.Gui.Editor.Benchmarks/FindBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using BenchmarkDotNet.Attributes;
using Terminal.Gui.Document;
using Terminal.Gui.Document.Search;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Editor.Document.Search;

namespace Terminal.Gui.Editor.Benchmarks;

Expand Down
4 changes: 2 additions & 2 deletions benchmarks/Terminal.Gui.Editor.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using BenchmarkDotNet.Running;
using Terminal.Gui.Document;
using Terminal.Gui.Document.Search;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Editor.Document.Search;

if (args.Length > 0 && args[0] == "--quick-find")
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using BenchmarkDotNet.Attributes;
using Terminal.Gui.Document;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Editor.Rendering;
using Attribute = Terminal.Gui.Drawing.Attribute;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using BenchmarkDotNet.Attributes;
using Terminal.Gui.Document;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Editor.Rendering;
using Attribute = Terminal.Gui.Drawing.Attribute;

Expand Down
2 changes: 1 addition & 1 deletion examples/prompt/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// outputs to stdout on Enter, exits silently on Esc.

using Terminal.Gui.App;
using Terminal.Gui.Document;
using Terminal.Gui.Editor;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Input;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
Expand Down
179 changes: 179 additions & 0 deletions examples/ted/MarkdownPreview.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
using System.Drawing;
using Terminal.Gui.Drawing;
using Terminal.Gui.Input;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
using Attribute = Terminal.Gui.Drawing.Attribute;
using Color = Terminal.Gui.Drawing.Color;

namespace Ted;

/// <summary>
/// A <see cref="Markdown" /> subclass that highlights the rendered line(s) corresponding to a
/// source line in the editor and raises <see cref="SourceLineClicked" /> when the user clicks
/// in the preview, enabling click-to-navigate back to the editor.
/// </summary>
internal sealed class MarkdownPreview : Markdown
{
private int _highlightSourceLine = -1;
private int _totalSourceLines;

/// <summary>
/// Gets or sets the 0-based source line number to highlight in the preview.
/// Set to -1 to clear the highlight.
/// </summary>
public int HighlightSourceLine
{
get => _highlightSourceLine;
set
{
if (_highlightSourceLine == value)
{
return;
}

_highlightSourceLine = value;
SetNeedsDraw ();
}
}

/// <summary>
/// Gets or sets the total number of source lines in the document.
/// Used for proportional mapping between source lines and rendered lines.
/// </summary>
public int TotalSourceLines
{
get => _totalSourceLines;
set
{
if (_totalSourceLines == value)
{
return;
}

_totalSourceLines = value;
SetNeedsDraw ();
}
}

/// <summary>
/// Raised when the user clicks in the preview. The event arg carries the estimated 0-based
/// source line number corresponding to the click position.
/// </summary>
public event EventHandler<SourceLineClickedEventArgs>? SourceLineClicked;

/// <summary>
/// Maps a 0-based source line number to the corresponding rendered line index using
/// proportional mapping.
/// </summary>
private int MapSourceToRendered (int sourceLine)
{
if (_totalSourceLines <= 1 || LineCount <= 0)
{
return 0;
}

return (int)((long)sourceLine * (LineCount - 1) / (_totalSourceLines - 1));
}

/// <summary>
/// Maps a rendered line index back to an approximate 0-based source line number.
/// </summary>
private int MapRenderedToSource (int renderedLine)
{
if (LineCount <= 1 || _totalSourceLines <= 0)
{
return 0;
}

return (int)((long)renderedLine * (_totalSourceLines - 1) / (LineCount - 1));
}

/// <inheritdoc />
protected override bool OnDrawingContent (DrawContext? context)
{
var result = base.OnDrawingContent (context);

DrawHighlightBar ();

return result;
}

/// <summary>
/// Paints a subtle background highlight on the rendered row corresponding to
/// <see cref="HighlightSourceLine" />.
/// </summary>
private void DrawHighlightBar ()
{
if (_highlightSourceLine < 0 || _totalSourceLines <= 0 || LineCount <= 0)
{
return;
}

var renderedLine = MapSourceToRendered (_highlightSourceLine);

// Check if the highlighted line is within the visible viewport.
var drawRow = renderedLine - Viewport.Y;

if (drawRow < 0 || drawRow >= Viewport.Height)
{
return;
}

// Compute a highlight attribute: shift the background slightly for contrast.
Attribute normalAttr = GetAttributeForRole (VisualRole.Normal);
Color highlightBg = normalAttr.Background.IsDarkColor ()
? normalAttr.Background.GetDimmerColor (0.25, false)
: normalAttr.Background.GetDimmerColor (0.15, true);

// Paint the highlight over the full viewport width by reading existing screen content
// and re-drawing with the highlight background.
Cell[,]? contents = ScreenContents;

if (contents is null)
{
return;
}

// Map the viewport-relative draw row to screen coordinates so we read the correct
// cells regardless of horizontal scroll position (Viewport.X).
Point screenOrigin = ViewportToScreen (new Point (0, drawRow));
var screenRow = screenOrigin.Y;
var screenStartCol = screenOrigin.X;

for (var col = 0; col < Viewport.Width; col++)
{
var sc = screenStartCol + col;

if (screenRow < 0 || screenRow >= contents.GetLength (0) || sc < 0 || sc >= contents.GetLength (1))
{
continue;
}

Cell cell = contents[screenRow, sc];
var grapheme = string.IsNullOrEmpty (cell.Grapheme) ? " " : cell.Grapheme;

// Preserve the foreground color from the original cell but apply highlight background.
Attribute cellAttr = (cell.Attribute ?? normalAttr) with { Background = highlightBg };
SetAttribute (cellAttr);
AddStr (col, drawRow, grapheme);
}
}

/// <inheritdoc />
protected override bool OnMouseEvent (Mouse mouse)
{
if (mouse.Flags.HasFlag (MouseFlags.LeftButtonClicked) && mouse.Position is { } pos)
{
var contentRow = Viewport.Y + pos.Y;

if (contentRow >= 0 && contentRow < LineCount)
Comment thread
tig marked this conversation as resolved.
{
var sourceLine = MapRenderedToSource (contentRow);
SourceLineClicked?.Invoke (this, new SourceLineClickedEventArgs (sourceLine));
}
}

return base.OnMouseEvent (mouse);
}
}
13 changes: 13 additions & 0 deletions examples/ted/SourceLineClickedEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Ted;

/// <summary>Event args for <see cref="MarkdownPreview.SourceLineClicked" />.</summary>
internal sealed class SourceLineClickedEventArgs : EventArgs
{
public SourceLineClickedEventArgs (int sourceLine)
{
SourceLine = sourceLine;
}

/// <summary>Gets the estimated 0-based source line number that was clicked.</summary>
public int SourceLine { get; }
}
4 changes: 2 additions & 2 deletions examples/ted/TedApp.FileOperations.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Text;
using Terminal.Gui.Document;
using Terminal.Gui.Highlighting;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Editor.Highlighting;
using Terminal.Gui.Resources;
using Terminal.Gui.Views;

Expand Down
45 changes: 43 additions & 2 deletions examples/ted/TedApp.MarkdownPreview.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Ted;

public sealed partial class TedApp
{
private Markdown? _markdownPreview;
private MarkdownPreview? _markdownPreview;
private bool _syncingScroll;

/// <summary>Toggle state used by the View menu item that shows or hides the Markdown preview pane.</summary>
Expand Down Expand Up @@ -66,13 +66,14 @@ private void ShowMarkdownPreview ()
return;
}

_markdownPreview = new Markdown
_markdownPreview = new MarkdownPreview
{
X = Pos.Right (Editor),
Y = Editor.Y,
Width = Dim.Fill (),
Height = Editor.Height,
Text = Editor.Document?.Text ?? string.Empty,
TotalSourceLines = Editor.Document?.LineCount ?? 1,
ViewportSettings = ViewportSettingsFlags.HasScrollBars,
SyntaxHighlighter = new TextMateSyntaxHighlighter ()
};
Expand All @@ -91,6 +92,13 @@ private void ShowMarkdownPreview ()
{
Editor.Document.Changed += OnDocumentChangedForPreview;
}

// Track caret movement to highlight the corresponding line in the preview.
Editor.CaretChanged += OnEditorCaretChangedForPreview;
UpdatePreviewHighlight ();

// Click in preview navigates the editor caret.
_markdownPreview.SourceLineClicked += OnPreviewSourceLineClicked;
}

private void HideMarkdownPreview ()
Expand All @@ -102,6 +110,8 @@ private void HideMarkdownPreview ()

Editor.ViewportChanged -= OnEditorViewportChanged;
_markdownPreview.ViewportChanged -= OnPreviewViewportChanged;
Editor.CaretChanged -= OnEditorCaretChangedForPreview;
_markdownPreview.SourceLineClicked -= OnPreviewSourceLineClicked;

if (Editor.Document is not null)
{
Expand Down Expand Up @@ -190,6 +200,35 @@ private void OnDocumentChangedForPreview (object? sender, EventArgs e)
}

_markdownPreview.Text = Editor.Document?.Text ?? string.Empty;
_markdownPreview.TotalSourceLines = Editor.Document?.LineCount ?? 1;
}

private void OnEditorCaretChangedForPreview (object? sender, EventArgs e)
{
UpdatePreviewHighlight ();
}

private void UpdatePreviewHighlight ()
{
if (_markdownPreview is null || Editor.Document is null)
{
return;
}

var sourceLine = Editor.Document.GetLineByOffset (Editor.CaretOffset).LineNumber - 1;
_markdownPreview.HighlightSourceLine = sourceLine;
}

private void OnPreviewSourceLineClicked (object? sender, SourceLineClickedEventArgs e)
{
if (Editor.Document is null)
{
return;
}

var lineNumber = Math.Clamp (e.SourceLine + 1, 1, Editor.Document.LineCount);
Editor.CaretOffset = Editor.Document.GetLineByNumber (lineNumber).Offset;
Editor.SetFocus ();
}

/// <summary>
Expand All @@ -212,5 +251,7 @@ private void RefreshPreviewDocument ()
}

_markdownPreview.Text = Editor.Document?.Text ?? string.Empty;
_markdownPreview.TotalSourceLines = Editor.Document?.LineCount ?? 1;
UpdatePreviewHighlight ();
}
}
6 changes: 3 additions & 3 deletions examples/ted/TedApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
using System.Text;
using Terminal.Gui.App;
using Terminal.Gui.Configuration;
using Terminal.Gui.Document;
using Terminal.Gui.Document.Folding;
using Terminal.Gui.Drawing;
using Terminal.Gui.Editor;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Editor.Document.Folding;
using Terminal.Gui.Editor.Indentation;
using Terminal.Gui.Input;
using Terminal.Gui.Resources;
using Terminal.Gui.Text.Indentation;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;

Expand Down
2 changes: 1 addition & 1 deletion examples/ted/WordCompletionProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Terminal.Gui.Document;
using Terminal.Gui.Editor.Completion;
using Terminal.Gui.Editor.Document;
using Terminal.Gui.Input;

namespace Ted;
Expand Down
Loading
Loading