SearchCollectionNavigator.cs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. using System;
  2. using System.Linq;
  3. namespace Terminal.Gui {
  4. /// <summary>
  5. /// Changes the index in a collection based on keys pressed
  6. /// and the current state
  7. /// </summary>
  8. class SearchCollectionNavigator {
  9. string state = "";
  10. DateTime lastKeystroke = DateTime.MinValue;
  11. const int TypingDelay = 250;
  12. public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase;
  13. public int CalculateNewIndex (string [] collection, int currentIndex, char keyStruck)
  14. {
  15. // if user presses a letter key
  16. if (char.IsLetterOrDigit (keyStruck) || char.IsPunctuation (keyStruck)) {
  17. // maybe user pressed 'd' and now presses 'd' again.
  18. // a candidate search is things that begin with "dd"
  19. // but if we find none then we must fallback on cycling
  20. // d instead and discard the candidate state
  21. string candidateState = "";
  22. // is it a second or third (etc) keystroke within a short time
  23. if (state.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) {
  24. // "dd" is a candidate
  25. candidateState = state + keyStruck;
  26. } else {
  27. // its a fresh keystroke after some time
  28. // or its first ever key press
  29. state = new string (keyStruck, 1);
  30. }
  31. var idxCandidate = GetNextIndexMatching (collection, currentIndex, candidateState,
  32. // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart"
  33. candidateState.Length > 1);
  34. if (idxCandidate != -1) {
  35. // found "dd" so candidate state is accepted
  36. lastKeystroke = DateTime.Now;
  37. state = candidateState;
  38. return idxCandidate;
  39. }
  40. // nothing matches "dd" so discard it as a candidate
  41. // and just cycle "d" instead
  42. lastKeystroke = DateTime.Now;
  43. idxCandidate = GetNextIndexMatching (collection, currentIndex, state);
  44. // if no changes to current state manifested
  45. if (idxCandidate == currentIndex || idxCandidate == -1) {
  46. // clear history and treat as a fresh letter
  47. ClearState ();
  48. // match on the fresh letter alone
  49. state = new string (keyStruck, 1);
  50. idxCandidate = GetNextIndexMatching (collection, currentIndex, state);
  51. return idxCandidate == -1 ? currentIndex : idxCandidate;
  52. }
  53. // Found another "d" or just leave index as it was
  54. return idxCandidate;
  55. } else {
  56. // clear state because keypress was non letter
  57. ClearState ();
  58. // no change in index for non letter keystrokes
  59. return currentIndex;
  60. }
  61. }
  62. private int GetNextIndexMatching (string [] collection, int currentIndex, string search, bool preferNotToMoveToNewIndexes = false)
  63. {
  64. if (string.IsNullOrEmpty (search)) {
  65. return -1;
  66. }
  67. // find indexes of items that start with the search text
  68. int [] matchingIndexes = collection.Select ((item, idx) => (item, idx))
  69. .Where (k => k.Item1?.StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false)
  70. .Select (k => k.idx)
  71. .ToArray ();
  72. // if there are items beginning with search
  73. if (matchingIndexes.Length > 0) {
  74. // is one of them currently selected?
  75. var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex);
  76. if (currentlySelected == -1) {
  77. // we are not currently selecting any item beginning with the search
  78. // so jump to first item in list that begins with the letter
  79. return matchingIndexes [0];
  80. } else {
  81. // the current index is part of the matching collection
  82. if (preferNotToMoveToNewIndexes) {
  83. // if we would rather not jump around (e.g. user is typing lots of text to get this match)
  84. return matchingIndexes [currentlySelected];
  85. }
  86. // cycle to next (circular)
  87. return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length];
  88. }
  89. }
  90. // nothing starts with the search
  91. return -1;
  92. }
  93. private void ClearState ()
  94. {
  95. state = "";
  96. lastKeystroke = DateTime.MinValue;
  97. }
  98. }
  99. }