diff --git a/src/Terminal.Gui.Editor/AnsiInputProcessorState.cs b/src/Terminal.Gui.Editor/AnsiInputProcessorState.cs new file mode 100644 index 0000000..5b74953 --- /dev/null +++ b/src/Terminal.Gui.Editor/AnsiInputProcessorState.cs @@ -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); + } +} diff --git a/src/Terminal.Gui.Editor/Editor.Indentation.cs b/src/Terminal.Gui.Editor/Editor.Indentation.cs index 926be3a..9c3d82f 100644 --- a/src/Terminal.Gui.Editor/Editor.Indentation.cs +++ b/src/Terminal.Gui.Editor/Editor.Indentation.cs @@ -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 lines = HasSelection && SelectionSpansMultipleLines () @@ -78,6 +87,8 @@ private bool Unindent () if (removals.Count == 0) { + AnsiInputProcessorState.ClearPendingPrintableSuppression (App); + return true; } @@ -102,6 +113,8 @@ private bool Unindent () AdjustOffsetAfterRemovals (selectionEnd, removals)); } + AnsiInputProcessorState.ClearPendingPrintableSuppression (App); + return true; } diff --git a/tests/Terminal.Gui.Editor.IntegrationTests/EditorTabTests.cs b/tests/Terminal.Gui.Editor.IntegrationTests/EditorTabTests.cs index abf1038..1d6dc62 100644 --- a/tests/Terminal.Gui.Editor.IntegrationTests/EditorTabTests.cs +++ b/tests/Terminal.Gui.Editor.IntegrationTests/EditorTabTests.cs @@ -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; @@ -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 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 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 (); + } }