diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.Navigation.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.Navigation.cs index 1b8e68d773..9f35136b6b 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.Navigation.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.Navigation.cs @@ -194,8 +194,21 @@ private IDirectoryInfo StringToDirectoryInfo (string path) private static void SuppressIfBadChar (Key k) { - // don't let user type bad letters - var ch = (char)k; + // Only suppress actual typed printable characters. + // Navigation keys (Home/End/etc.) must pass through to key bindings. + if (k.IsAlt || k.IsCtrl) + { + return; + } + + string grapheme = k.AsGrapheme; + + if (grapheme.Length != 1) + { + return; + } + + char ch = grapheme [0]; if (_badChars.Contains (ch)) { diff --git a/Tests/UnitTestsParallelizable/Views/AppendAutocompleteTests.cs b/Tests/UnitTestsParallelizable/Views/AppendAutocompleteTests.cs index 6bf6aa56bf..5249018e0b 100644 --- a/Tests/UnitTestsParallelizable/Views/AppendAutocompleteTests.cs +++ b/Tests/UnitTestsParallelizable/Views/AppendAutocompleteTests.cs @@ -83,6 +83,49 @@ public void ProcessKey_CursorUp_CyclesSuggestions () Assert.Equal (initialIdx, ac.SelectedIdx); } + [Fact] + public void ProcessKey_End_ReturnsFalse_WhenSuggestionExists () + { + // Copilot - End should not be consumed by AppendAutocomplete. + // This lets TextField's normal End command move the cursor. + // Arrange: focused text field with "f" at end and suggestion "fish" + TextField tf = new () { Text = "f" }; + tf.SetFocus (); + tf.MoveEnd (); + + AppendAutocomplete ac = new (tf); + ((SingleWordSuggestionGenerator)ac.SuggestionGenerator).AllSuggestions = ["fish"]; + + AutocompleteContext context = new ([new Cell (Grapheme: "f")], cursorPosition: 1); + ac.GenerateSuggestions (context); + + Assert.NotEmpty (ac.Suggestions); + + // Act + bool result = ac.ProcessKey (Key.End); + + // Assert: End was not consumed by autocomplete + Assert.False (result); + Assert.Equal ("f", tf.Text); + Assert.NotEmpty (ac.Suggestions); + } + + [Fact] + public void ProcessKey_End_ReturnsFalse_WhenNoSuggestions () + { + // Copilot - End key should NOT consume the event when no suggestion is showing, + // allowing the normal MoveEnd command to run. + // Arrange: text field without suggestions + TextField tf = new () { Text = "f" }; + AppendAutocomplete ac = new (tf); + + // Act + bool result = ac.ProcessKey (Key.End); + + // Assert: End was not consumed + Assert.False (result); + } + [Fact] public void AcceptSelectionIfAny_AcceptsSuggestionWhenFocused () { diff --git a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs index 69befbdf15..b8188e3dbe 100644 --- a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs +++ b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs @@ -170,6 +170,57 @@ public void OpenDialog_UsesInnerTableSeparatorsWithoutOuterBorders () Assert.False (tableView.Style.ShowVerticalCellLineForLastColumn); } + [Fact] + public void FileDialog_PathField_End_MovesInsertionPointToEnd () + { + // Copilot + MockFileSystem fs = new (); + fs.AddDirectory ("/testdir"); + using FileDialog fd = new TestableFileDialog (fs); + + FieldInfo? tbPathField = typeof (FileDialog).GetField ("_tbPath", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull (tbPathField); + + TextField tbPath = Assert.IsType (tbPathField!.GetValue (fd)); + tbPath.Text = "/testdir/example.txt"; + + tbPath.NewKeyDownEvent (Key.Home); + Assert.Equal (0, tbPath.InsertionPoint); + + tbPath.NewKeyDownEvent (Key.End); + Assert.Equal (tbPath.Text.Length, tbPath.InsertionPoint); + } + + [Theory] + [InlineData ('"')] + [InlineData ('<')] + [InlineData ('>')] + [InlineData ('|')] + [InlineData ('*')] + [InlineData ('?')] + public void FileDialog_PathField_BadChars_AreSuppressed (char badChar) + { + // Copilot + MockFileSystem fs = new (); + fs.AddDirectory ("/testdir"); + using FileDialog fd = new TestableFileDialog (fs); + + FieldInfo? tbPathField = typeof (FileDialog).GetField ("_tbPath", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull (tbPathField); + + TextField tbPath = Assert.IsType (tbPathField!.GetValue (fd)); + tbPath.Text = "/testdir/"; + tbPath.MoveEnd (); + + int insertionPointBefore = tbPath.InsertionPoint; + string textBefore = tbPath.Text; + + tbPath.NewKeyDownEvent (new Key (badChar)); + + Assert.Equal (textBefore, tbPath.Text); + Assert.Equal (insertionPointBefore, tbPath.InsertionPoint); + } + /// Testable subclass that exposes the internal file-system constructor. private sealed class TestableFileDialog : FileDialog {