Skip to content
Open
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
52 changes: 43 additions & 9 deletions examples/ted/TedApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ public TedApp ()
Value = Editor.ShowLineNumbers ? CheckState.Checked : CheckState.UnChecked
};

CheckBox convertTabsToSpacesCheckBox = new ()
{
AllowCheckStateNone = false,
CanFocus = false,
Text = "_Convert Tabs To Spaces",
Value = Editor.ConvertTabsToSpaces ? CheckState.Checked : CheckState.UnChecked
};

convertTabsToSpacesCheckBox.ValueChanged += (_, e) =>
{
Editor.ConvertTabsToSpaces = e.NewValue == CheckState.Checked;
};

ThemeDropDown = new DropDownList<ThemeName>
{
Value = ThemeName.DarkPlus,
Expand Down Expand Up @@ -81,29 +94,42 @@ public TedApp ()
#pragma warning restore CS0618 // Type or member is obsolete
};

TabWidthUpDown = new NumericUpDown<int>
IndentationSizeUpDown = new NumericUpDown<int>
{
Value = Editor.TabWidth,
Value = Editor.IndentationSize,
Increment = 1
};

TabWidthUpDown.ValueChanged += (_, e) =>
IndentationSizeUpDown.ValueChanged += (_, e) =>
{
if (Editor.TabWidth == e.NewValue)
if (Editor.IndentationSize == e.NewValue)
{
return;
}

Editor.TabWidth = e.NewValue;
Editor.IndentationSize = e.NewValue;
};

ShowTabsCheckBox = new CheckBox
{
AllowCheckStateNone = false,
CanFocus = false,
Title = "↹",
Value = Editor.ShowTabs ? CheckState.Checked : CheckState.UnChecked
};

ShowTabsCheckBox.ValueChanged += (_, e) =>
{
Editor.ShowTabs = e.NewValue == CheckState.Checked;
};

StatusBar statusBar =
new ([
new Shortcut (KeyFor (Command.Quit), "Quit", Quit),
new Shortcut (Key.Empty, "Themes", null) { MouseHighlightStates = MouseState.None },
new Shortcut { Title = "Themes", CommandView = ThemeDropDown },
new Shortcut (Key.Empty, "Tab", null) { MouseHighlightStates = MouseState.None },
new Shortcut { Title = "Tab", CommandView = TabWidthUpDown },
new Shortcut { Text = "Indent Size", CommandView = IndentationSizeUpDown },
new Shortcut { CommandView = ShowTabsCheckBox },
new Shortcut (Key.Empty, "x, y", null, "Loc") { MouseHighlightStates = MouseState.None },
_fileNameShortcut = new Shortcut (Key.Empty, "<untitled>", Open)
{
Expand Down Expand Up @@ -145,6 +171,11 @@ public TedApp ()
},
CommandView = lineNumbersCheckBox,
HelpText = "Show line numbers"
},
new MenuItem
{
CommandView = convertTabsToSpacesCheckBox,
HelpText = "Insert spaces when Tab is pressed"
}
]),
new MenuBarItem (Strings.menuHelp,
Expand All @@ -165,8 +196,11 @@ [new MenuItem ("_About", "Show About dialog", Action)])
/// <summary>The syntax-highlighting theme selector shown in the status bar.</summary>
public DropDownList<ThemeName> ThemeDropDown { get; }

/// <summary>The tab-width selector shown in the status bar.</summary>
public NumericUpDown<int> TabWidthUpDown { get; }
/// <summary>The indentation-size selector shown in the status bar.</summary>
public NumericUpDown<int> IndentationSizeUpDown { get; }

/// <summary>The status-bar checkbox that toggles visible tab glyphs.</summary>
public CheckBox ShowTabsCheckBox { get; }

/// <summary>The path currently associated with <see cref="Editor" />, or <see langword="null" /> for an untitled buffer.</summary>
public string? CurrentFilePath { get; private set; }
Expand Down
32 changes: 32 additions & 0 deletions specs/03-public-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Public API Notes

This file tracks public API added during pre-alpha work. It is intentionally brief; `specs/00-plan.md`
remains the source of truth for the target architecture.

## D1 Tabs

`Terminal.Gui.Views.Editor` exposes the AvaloniaEdit-aligned tab surface directly:

- `int IndentationSize { get; set; } = 4` controls indentation stops in terminal cells. Values less than 1 are rejected.
- `bool ConvertTabsToSpaces { get; set; }` controls what the Tab key inserts. It never rewrites existing document text.
- `bool ShowTabs { get; set; }` renders a visible glyph in the first cell of each tab expansion.

`Editor.TabWidth` was removed rather than shimmed because the project is still pre-alpha and `IndentationSize`
is the planned AvaloniaEdit-compatible name.

## Rendering Pipeline Slice

The first rendering pipeline types live in `Terminal.Gui.Views.Rendering`:

- `VisualLineBuilder`
- `CellVisualLine`
- `CellVisualLineElement`
- `TextRunElement`
- `TabElement`
- `IVisualLineTransformer`
- `IBackgroundRenderer`
- `VisualLineBuildContext`

`Editor.LineTransformers` and `Editor.BackgroundRenderers` expose the current extension points. This is the
D1-enabling slice only: cache invalidation, folding markers, line-number background rendering, and the full B1/B2
pipeline remain tracked in `specs/00-plan.md`.
47 changes: 47 additions & 0 deletions specs/runs/test-codex-final.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Codex D1 Tabs Final Report

## What I did

- Chose option (b): implemented the slice of the VisualLineBuilder pipeline needed for D1, then implemented tab handling on top of it.
- Added `Terminal.Gui.Views.Rendering` types: `VisualLineBuilder`, `CellVisualLine`, `CellVisualLineElement`, `TextRunElement`, `TabElement`, `IVisualLineTransformer`, `IBackgroundRenderer`, and build context.
- Replaced `Editor.TabWidth` with `Editor.IndentationSize`, and added `ConvertTabsToSpaces`, `ShowTabs`, `LineTransformers`, and `BackgroundRenderers`.
- Moved visible line text drawing through grapheme-aware visual-line elements. `OnDrawingContent` no longer expands `\t` directly or walks rendered text char-by-char.
- Implemented Tab, Shift+Tab, block indent/unindent, and indentation-aware Backspace.
- Updated mouse tab hit-testing to snap to the nearest logical tab edge, with midpoint rounding before the tab.
- Updated ted: status bar uses `Indent Size`, adds a `ShowTabs` checkbox, and Options has `_Convert Tabs To Spaces`.
- Added/updated tests for visual-line tab expansion, grapheme widths, Tab insertion modes, block indent undo grouping, Shift+Tab, indentation Backspace, mouse snapping, ted UI, and tab round trip.
- Added `specs/03-public-api.md` notes for the new `Editor` tab surface and rendering pipeline slice.

## What I skipped

- I did not implement all of B1 as specified in `specs/00-plan.md`: there is no visual-line cache/invalidation, no folding marker element, and no full background-renderer migration for line numbers.
- I did not implement `IIndentationStrategy` / `DefaultIndentationStrategy`; that belongs to A3/D7 and is not present in this checkout.
- I did not migrate line numbers from `OnDrawComplete` to an `IBackgroundRenderer`; that is B2 scope.

## Why

D1 explicitly depends on B1, and shipping another draw-loop tab shortcut would repeat the R1/R2 violation. I implemented the smallest B1-compatible line-element layer that lets tabs be represented as a `TabElement` and makes the D1 behavior testable without taking over the whole B1/B2 work item.

## Validation

- `dotnet build Terminal.Gui.Text.slnx`
- `dotnet run --project tests/Terminal.Gui.Text.Tests`
- `dotnet run --project tests/Terminal.Gui.Editor.Tests`
- `dotnet run --project tests/Terminal.Gui.Editor.IntegrationTests`
- `dotnet format Terminal.Gui.Text.slnx --verify-no-changes --exclude third_party/`

`dotnet tool restore` succeeded. `dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile="Full Cleanup"` failed because the JetBrains CLI did not recognize the solution cleanup profile from this `.slnx` checkout. Running without the profile against the `.slnx` reported no cleanup items. I ran cleanup against the touched projects directly, which used JetBrains' built-in reformat profile.

PR CI result: macOS and Windows passed. Ubuntu failed in the existing ReSharper cleanupcode step because
`dotnet jb cleanupcode Terminal.Gui.Text.slnx --profile="Built-in: Full Cleanup" --no-build --exclude="third_party/**/*"`
returned exit code 3 with `No items were found to cleanup.` I confirmed the current `develop` CI fails the same way
on run `25613103283`, so I decided I cannot make CI green from this PR without changing `.github/workflows/ci.yml`
or the repository-wide cleanup setup, both of which are outside the allowed edit set for this task.

## Total tokens spent

Exact token usage is not exposed inside this Codex session. My best estimate from the transcript size is roughly 120k-160k tokens.

## What I would do differently

If this were a normal feature PR instead of an experiment, I would split the rendering pipeline into a separate B1 PR first, with cache invalidation and line-number/background migration handled in follow-up B2, then land D1 as a smaller PR on top.
7 changes: 7 additions & 0 deletions src/Terminal.Gui.Editor/Editor.Commands.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ private void CreateCommandsAndBindings ()

ApplyKeyBindings (View.DefaultKeyBindings, DefaultKeyBindings);

KeyBindings.Remove (Key.Tab);
KeyBindings.Remove (Key.Tab.WithShift);

MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp);
MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown);
MouseBindings.Add (MouseFlags.WheeledLeft, Command.ScrollLeft);
Expand Down Expand Up @@ -181,6 +184,10 @@ private void CreateCommandsAndBindings ()
{
ReplaceSelection (string.Empty);
}
else if (TryDeleteIndentationLeft ())
{
return true;
}
else if (_caretOffset > 0)
{
_document!.Remove (_caretOffset - 1, 1);
Expand Down
72 changes: 18 additions & 54 deletions src/Terminal.Gui.Editor/Editor.Drawing.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Terminal.Gui.Drivers;
using Terminal.Gui.Text.Document;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views.Rendering;
using Attribute = Terminal.Gui.Drawing.Attribute;

namespace Terminal.Gui.Views;
Expand Down Expand Up @@ -44,7 +45,7 @@ protected override bool OnDrawingContent (DrawContext? context)
}

DocumentLine line = _document.GetLineByNumber (lineIndex + 1);
var text = _document.GetText (line);
string text = _document.GetText (line);
#pragma warning disable CS0618 // Type or member is obsolete — see note at top of OnDrawingContent.
IReadOnlyList<StyledSegment>? segments = syntaxHighlighter?.Highlight (text, SyntaxLanguage);
#pragma warning restore CS0618 // Type or member is obsolete
Expand All @@ -53,13 +54,12 @@ protected override bool OnDrawingContent (DrawContext? context)

DrawLineContent (
row,
text,
line,
visibleStart,
visibleEnd,
segments,
normal,
selected,
line.Offset,
hasSelection,
selStart,
selEnd);
Expand Down Expand Up @@ -91,68 +91,32 @@ private void PrepareSyntaxHighlighter (ISyntaxHighlighter? syntaxHighlighter, in

private void DrawLineContent (
int row,
string text,
DocumentLine line,
int visibleStart,
int visibleEnd,
IReadOnlyList<StyledSegment>? segments,
Attribute normal,
Attribute selected,
int lineOffset,
bool hasSelection,
int selStart,
int selEnd)
{
var visualColumn = 0;
var segmentIndex = 0;
var segmentEnd = segments is { Count: > 0 } ? segments[0].Text.Length : int.MaxValue;

for (var i = 0; i < text.Length; i++)
CellVisualLine visualLine = BuildVisualLine (
line,
segments,
normal,
selected,
hasSelection ? selStart : 0,
hasSelection ? selEnd : 0);

foreach (IBackgroundRenderer renderer in BackgroundRenderers)
{
while (segments is not null && i >= segmentEnd && segmentIndex + 1 < segments.Count)
{
segmentIndex++;
segmentEnd += segments[segmentIndex].Text.Length;
}

Attribute attribute = segments is null
? normal
: segments[segmentIndex].Attribute ?? normal;

if (hasSelection && lineOffset + i >= selStart && lineOffset + i < selEnd)
{
attribute = selected;
}

var c = text[i];
var width = GetVisualWidthForCharacter (c, visualColumn, TabWidth);
var charVisualStart = visualColumn;
var charVisualEnd = visualColumn + width;

if (charVisualEnd <= visibleStart)
{
visualColumn = charVisualEnd;

continue;
}

if (charVisualStart >= visibleEnd)
{
break;
}

var drawStart = Math.Max (charVisualStart, visibleStart);
var drawEnd = Math.Min (charVisualEnd, visibleEnd);

if (drawEnd > drawStart)
{
SetAttribute (attribute);
AddStr (
drawStart - visibleStart,
row,
c == '\t' ? new (' ', drawEnd - drawStart) : c.ToString ());
}
renderer.Draw (this, visualLine, row, Viewport);
}

visualColumn = charVisualEnd;
foreach (CellVisualLineElement element in visualLine.Elements)
{
element.Draw (this, 0, row, visibleStart, visibleEnd);
}
}

Expand Down
Loading
Loading