1- using System . Collections ;
21using System . Collections . Concurrent ;
2+ using System . Text ;
33using Moq ;
44
55namespace TextTests ;
@@ -15,10 +15,6 @@ public class CollectionNavigatorTests
1515 "candle" // 4
1616 } ;
1717
18- private readonly ITestOutputHelper _output ;
19-
20- public CollectionNavigatorTests ( ITestOutputHelper output ) { _output = output ; }
21-
2218 [ Fact ]
2319 public void AtSymbol ( )
2420 {
@@ -30,6 +26,24 @@ public void AtSymbol ()
3026 Assert . Equal ( 4 , n . GetNextMatchingItem ( 3 , 'b' ) ) ;
3127 }
3228
29+ [ Fact ]
30+ public void CustomMatcher_NeverMatches ( )
31+ {
32+ var strings = new [ ] { "apricot" , "arm" , "bat" , "batman" , "bates hotel" , "candle" } ;
33+ int ? current = 0 ;
34+ var n = new CollectionNavigator ( strings ) ;
35+
36+ Mock < ICollectionNavigatorMatcher > matchNone = new ( ) ;
37+
38+ matchNone . Setup ( m => m . IsMatch ( It . IsAny < string > ( ) , It . IsAny < object > ( ) ) ) . Returns ( false ) ;
39+
40+ n . Matcher = matchNone . Object ;
41+
42+ Assert . Equal ( 0 , current = n . GetNextMatchingItem ( current , 'b' ) ) ; // no matches
43+ Assert . Equal ( 0 , current = n . GetNextMatchingItem ( current , 'a' ) ) ; // no matches
44+ Assert . Equal ( 0 , current = n . GetNextMatchingItem ( current , 't' ) ) ; // no matches
45+ }
46+
3347 [ Fact ]
3448 public void Cycling ( )
3549 {
@@ -42,7 +56,7 @@ public void Cycling ()
4256 Assert . Equal ( 2 , n . GetNextMatchingItem ( 4 , 'b' ) ) ;
4357
4458 // cycling with 'a'
45- n = new ( simpleStrings ) ;
59+ n = new CollectionNavigator ( simpleStrings ) ;
4660 Assert . Equal ( 0 , n . GetNextMatchingItem ( null , 'a' ) ) ;
4761 Assert . Equal ( 1 , n . GetNextMatchingItem ( 0 , 'a' ) ) ;
4862
@@ -65,7 +79,7 @@ public void Delay ()
6579 Assert . Equal ( strings . IndexOf ( "$$" ) , current = n . GetNextMatchingItem ( current , '$' ) ) ;
6680 Assert . Equal ( "$$" , n . SearchString ) ;
6781
68- // Delay
82+ // Delay
6983 Thread . Sleep ( n . TypingDelay + 10 ) ;
7084 Assert . Equal ( strings . IndexOf ( "apricot" ) , current = n . GetNextMatchingItem ( current , 'a' ) ) ;
7185 Assert . Equal ( "a" , n . SearchString ) ;
@@ -134,10 +148,54 @@ public void IsCompatibleKey_Does_Not_Allow_Alt_And_Ctrl_Keys (KeyCode keyCode, b
134148 Assert . Equal ( compatible , m . IsCompatibleKey ( keyCode ) ) ;
135149 }
136150
151+ // Copilot - Opus 4.6
152+
153+ /// <summary>
154+ /// Verifies that when AssociatedText is set (e.g. Kitty keyboard protocol),
155+ /// Alt/Ctrl keys are still rejected even though AsRune returns a valid rune.
156+ /// </summary>
157+ [ Theory ]
158+ [ InlineData ( KeyCode . A | KeyCode . AltMask , "a" , false ) ]
159+ [ InlineData ( KeyCode . Z | KeyCode . AltMask , "z" , false ) ]
160+ [ InlineData ( KeyCode . A | KeyCode . CtrlMask , "a" , false ) ]
161+ [ InlineData ( KeyCode . Z | KeyCode . CtrlMask , "z" , false ) ]
162+ [ InlineData ( KeyCode . A | KeyCode . CtrlMask | KeyCode . AltMask , "a" , false ) ]
163+ [ InlineData ( KeyCode . A , "a" , true ) ]
164+ [ InlineData ( KeyCode . A | KeyCode . ShiftMask , "A" , true ) ]
165+ [ InlineData ( KeyCode . Space , " " , true ) ]
166+ public void IsCompatibleKey_WithAssociatedText_RejectsAltAndCtrl ( KeyCode keyCode , string associatedText , bool expected )
167+ {
168+ DefaultCollectionNavigatorMatcher matcher = new ( ) ;
169+ Key key = new ( keyCode ) { AssociatedText = associatedText } ;
170+
171+ // Confirm the rune is valid (non-default, non-control) — this is the scenario
172+ // where the old code (checking only the rune) would have incorrectly returned true.
173+ Rune rune = key . AsRune ;
174+
175+ if ( ! expected )
176+ {
177+ Assert . NotEqual ( default ( Rune ) , rune ) ;
178+ Assert . False ( Rune . IsControl ( rune ) ) ;
179+ }
180+
181+ Assert . Equal ( expected , matcher . IsCompatibleKey ( key ) ) ;
182+ }
183+
137184 [ Fact ]
138185 public void MinimizeMovement_False_ShouldMoveIfMultipleMatches ( )
139186 {
140- var strings = new [ ] { "$$" , "$100.00" , "$101.00" , "$101.10" , "$200.00" , "apricot" , "c" , "car" , "cart" } ;
187+ var strings = new [ ]
188+ {
189+ "$$" ,
190+ "$100.00" ,
191+ "$101.00" ,
192+ "$101.10" ,
193+ "$200.00" ,
194+ "apricot" ,
195+ "c" ,
196+ "car" ,
197+ "cart"
198+ } ;
141199 int ? current = 0 ;
142200 var n = new CollectionNavigator ( strings ) ;
143201 Assert . Equal ( strings . IndexOf ( "$$" ) , current = n . GetNextMatchingItem ( current , "$$" ) ) ;
@@ -173,7 +231,18 @@ public void MinimizeMovement_False_ShouldMoveIfMultipleMatches ()
173231 [ Fact ]
174232 public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches ( )
175233 {
176- var strings = new [ ] { "$$" , "$100.00" , "$101.00" , "$101.10" , "$200.00" , "apricot" , "c" , "car" , "cart" } ;
234+ var strings = new [ ]
235+ {
236+ "$$" ,
237+ "$100.00" ,
238+ "$101.00" ,
239+ "$101.10" ,
240+ "$200.00" ,
241+ "apricot" ,
242+ "c" ,
243+ "car" ,
244+ "cart"
245+ } ;
177246 int ? current = 0 ;
178247 var n = new CollectionNavigator ( strings ) ;
179248 Assert . Equal ( strings . IndexOf ( "$$" ) , current = n . GetNextMatchingItem ( current , "$$" , true ) ) ;
@@ -326,39 +395,11 @@ public void Word ()
326395 Assert . Equal ( strings . IndexOf ( "bat" ) , current = n . GetNextMatchingItem ( current , 'a' ) ) ; // match bat
327396 Assert . Equal ( strings . IndexOf ( "bat" ) , current = n . GetNextMatchingItem ( current , 't' ) ) ; // match bat
328397
329- Assert . Equal (
330- strings . IndexOf ( "bates hotel" ) ,
331- current = n . GetNextMatchingItem ( current , 'e' )
332- ) ; // match bates hotel
398+ Assert . Equal ( strings . IndexOf ( "bates hotel" ) , current = n . GetNextMatchingItem ( current , 'e' ) ) ; // match bates hotel
333399
334- Assert . Equal (
335- strings . IndexOf ( "bates hotel" ) ,
336- current = n . GetNextMatchingItem ( current , 's' )
337- ) ; // match bates hotel
400+ Assert . Equal ( strings . IndexOf ( "bates hotel" ) , current = n . GetNextMatchingItem ( current , 's' ) ) ; // match bates hotel
338401
339- Assert . Equal (
340- strings . IndexOf ( "bates hotel" ) ,
341- current = n . GetNextMatchingItem ( current , ' ' )
342- ) ; // match bates hotel
343- }
344-
345- [ Fact ]
346- public void CustomMatcher_NeverMatches ( )
347- {
348- var strings = new [ ] { "apricot" , "arm" , "bat" , "batman" , "bates hotel" , "candle" } ;
349- int ? current = 0 ;
350- var n = new CollectionNavigator ( strings ) ;
351-
352- Mock < ICollectionNavigatorMatcher > matchNone = new ( ) ;
353-
354- matchNone . Setup ( m => m . IsMatch ( It . IsAny < string > ( ) , It . IsAny < object > ( ) ) )
355- . Returns ( false ) ;
356-
357- n . Matcher = matchNone . Object ;
358-
359- Assert . Equal ( 0 , current = n . GetNextMatchingItem ( current , 'b' ) ) ; // no matches
360- Assert . Equal ( 0 , current = n . GetNextMatchingItem ( current , 'a' ) ) ; // no matches
361- Assert . Equal ( 0 , current = n . GetNextMatchingItem ( current , 't' ) ) ; // no matches
402+ Assert . Equal ( strings . IndexOf ( "bates hotel" ) , current = n . GetNextMatchingItem ( current , ' ' ) ) ; // match bates hotel
362403 }
363404
364405 #region Thread Safety Tests
@@ -371,21 +412,20 @@ public void ThreadSafety_ConcurrentSearchStringAccess ()
371412 var numTasks = 20 ;
372413 ConcurrentBag < Exception > exceptions = new ( ) ;
373414
374- Parallel . For (
375- 0 ,
415+ Parallel . For ( 0 ,
376416 numTasks ,
377417 i =>
378418 {
379419 try
380420 {
381421 // Read SearchString concurrently
382- string searchString = navigator . SearchString ;
422+ _ = navigator . SearchString ;
383423
384424 // Perform navigation operations concurrently
385- int ? result = navigator . GetNextMatchingItem ( 0 , 'a' ) ;
425+ _ = navigator . GetNextMatchingItem ( 0 , 'a' ) ;
386426
387427 // Read SearchString again
388- searchString = navigator . SearchString ;
428+ _ = navigator . SearchString ;
389429 }
390430 catch ( Exception ex )
391431 {
@@ -404,18 +444,17 @@ public void ThreadSafety_ConcurrentCollectionAccess ()
404444 var numTasks = 20 ;
405445 ConcurrentBag < Exception > exceptions = new ( ) ;
406446
407- Parallel . For (
408- 0 ,
447+ Parallel . For ( 0 ,
409448 numTasks ,
410449 i =>
411450 {
412451 try
413452 {
414453 // Access Collection property concurrently
415- IList collection = navigator . Collection ;
454+ _ = navigator . Collection ;
416455
417456 // Perform navigation
418- int ? result = navigator . GetNextMatchingItem ( 0 , ( char ) ( 'a' + i % 3 ) ) ;
457+ _ = navigator . GetNextMatchingItem ( 0 , ( char ) ( 'a' + i % 3 ) ) ;
419458 }
420459 catch ( Exception ex )
421460 {
@@ -435,8 +474,7 @@ public void ThreadSafety_ConcurrentNavigationOperations ()
435474 ConcurrentBag < int ? > results = new ( ) ;
436475 ConcurrentBag < Exception > exceptions = new ( ) ;
437476
438- Parallel . For (
439- 0 ,
477+ Parallel . For ( 0 ,
440478 numTasks ,
441479 i =>
442480 {
@@ -475,8 +513,8 @@ public void ThreadSafety_ConcurrentCollectionModification ()
475513 {
476514 for ( var j = 0 ; j < 100 ; j ++ )
477515 {
478- int ? result = navigator . GetNextMatchingItem ( 0 , 'a' ) ;
479- string searchString = navigator . SearchString ;
516+ _ = navigator . GetNextMatchingItem ( 0 , 'a' ) ;
517+ _ = navigator . SearchString ;
480518 }
481519 }
482520 catch ( Exception ex )
@@ -523,16 +561,27 @@ public void ThreadSafety_ConcurrentCollectionModification ()
523561 [ Fact ]
524562 public void ThreadSafety_ConcurrentSearchStringChanges ( )
525563 {
526- var strings = new [ ] { "apricot" , "arm" , "bat" , "batman" , "candle" , "cat" , "dog" , "elephant" , "fox" , "goat" } ;
564+ var strings = new [ ]
565+ {
566+ "apricot" ,
567+ "arm" ,
568+ "bat" ,
569+ "batman" ,
570+ "candle" ,
571+ "cat" ,
572+ "dog" ,
573+ "elephant" ,
574+ "fox" ,
575+ "goat"
576+ } ;
527577 var navigator = new CollectionNavigator ( strings ) ;
528578 var numTasks = 30 ;
529579 ConcurrentBag < Exception > exceptions = new ( ) ;
530580 ConcurrentBag < string > searchStrings = new ( ) ;
531581
532- Parallel . For (
533- 0 ,
582+ Parallel . For ( 0 ,
534583 numTasks ,
535- i =>
584+ _ =>
536585 {
537586 try
538587 {
@@ -570,8 +619,7 @@ public void ThreadSafety_StressTest_RapidOperations ()
570619 var operationsPerTask = 1000 ;
571620 ConcurrentBag < Exception > exceptions = new ( ) ;
572621
573- Parallel . For (
574- 0 ,
622+ Parallel . For ( 0 ,
575623 numTasks ,
576624 i =>
577625 {
@@ -588,7 +636,7 @@ public void ThreadSafety_StressTest_RapidOperations ()
588636
589637 if ( j % 100 == 0 )
590638 {
591- string searchString = navigator . SearchString ;
639+ _ = navigator . SearchString ;
592640 }
593641 }
594642 }
0 commit comments