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
34 changes: 34 additions & 0 deletions src/Terminal.Gui.Editor/AnsiInputProcessorState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Reflection;
using Terminal.Gui.App;
using Terminal.Gui.Drivers;

namespace Terminal.Gui.Editor;

internal static class AnsiInputProcessorState
{
private const string PendingPrintableSuppressionFieldName = "_pendingPrintableSuppression";

public static void ClearPendingPrintableSuppression (IApplication? app)
{
if (app?.Driver?.GetInputProcessor () is not AnsiInputProcessor processor)
{
return;
}

// Terminal.Gui 2.1.1-develop.98 suppresses the next printable fallback key after parsing
// ANSI Shift+Tab (ESC [ Z) because Shift+Tab reports Tab as printable text. Until TG exposes
// public input-processor state for this, clear that one-shot suppression after the editor
// handles Unindent so the user's next Tab reaches us. If TG renames this private field, the
// type checks below intentionally no-op; the only consequence is the original Tab suppression.
FieldInfo? field = typeof (AnsiInputProcessor).GetField (
PendingPrintableSuppressionFieldName,
BindingFlags.Instance | BindingFlags.NonPublic);

if (field?.FieldType != typeof (string))
{
return;
}

field.SetValue (processor, string.Empty);
}
}
15 changes: 14 additions & 1 deletion src/Terminal.Gui.Editor/Editor.Indentation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,21 @@ private bool Unindent ()

if (ReadOnly)
{
AnsiInputProcessorState.ClearPendingPrintableSuppression (App);

return true;
}

if (HasMultipleCarets)
{
return MultiCaretUnindent ();
var handled = MultiCaretUnindent ();

if (handled)
{
AnsiInputProcessorState.ClearPendingPrintableSuppression (App);
}

return handled;
}

List<DocumentLine> lines = HasSelection && SelectionSpansMultipleLines ()
Expand All @@ -78,6 +87,8 @@ private bool Unindent ()

if (removals.Count == 0)
{
AnsiInputProcessorState.ClearPendingPrintableSuppression (App);

return true;
}

Expand All @@ -102,6 +113,8 @@ private bool Unindent ()
AdjustOffsetAfterRemovals (selectionEnd, removals));
}

AnsiInputProcessorState.ClearPendingPrintableSuppression (App);

return true;
}

Expand Down
41 changes: 41 additions & 0 deletions tests/Terminal.Gui.Editor.IntegrationTests/EditorTabTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Codex - GPT-5

using Terminal.Gui.Editor.IntegrationTests.Testing;
using Terminal.Gui.Drivers;
using Terminal.Gui.Input;
using Terminal.Gui.Testing;
using Ted;
using Xunit;

namespace Terminal.Gui.Editor.IntegrationTests;
Expand Down Expand Up @@ -144,4 +146,43 @@ public async Task Backspace_At_End_Of_Leading_Whitespace_Removes_One_Indentation
Assert.Equal ("alpha", fx.Top.Editor.Document!.Text);
Assert.Equal (0, fx.Top.Editor.CaretOffset);
}

[Fact]
public async Task Ted_RawAnsi_Tab_After_ShiftTab_Reindents_Line_On_First_Keypress ()
{
await using AppFixture<TedApp> fx = new (() => new TedApp ());
fx.Top.Editor.SetFocus ();
fx.Top.Editor.Document!.Text = "hello world";
fx.Top.Editor.CaretOffset = 0;

InjectAnsi (fx, "\t");

Assert.Equal (" hello world", fx.Top.Editor.Document.Text);
Assert.Equal (4, fx.Top.Editor.CaretOffset);

InjectAnsi (fx, "\u001b[Z");

Assert.Equal ("hello world", fx.Top.Editor.Document.Text);
Assert.Equal (0, fx.Top.Editor.CaretOffset);

InjectAnsi (fx, "\t");

Assert.Equal (" hello world", fx.Top.Editor.Document.Text);
Assert.Equal (4, fx.Top.Editor.CaretOffset);
}

private static void InjectAnsi (AppFixture<TedApp> fx, string sequence)
{
if (fx.App.Driver!.GetInputProcessor () is not AnsiInputProcessor processor)
{
throw new InvalidOperationException ("ANSI input processor is required for raw ANSI input tests.");
}

foreach (var ch in sequence)
{
processor.InputQueue.Enqueue (ch);
}

processor.ProcessQueue ();
}
}
Loading