SingleWordSuggestionGenerator.cs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. #nullable disable
  2. namespace Terminal.Gui.Views;
  3. /// <summary>
  4. /// <see cref="ISuggestionGenerator"/> which suggests from a collection of words those that match the
  5. /// <see cref="AutocompleteContext"/>. You can update <see cref="AllSuggestions"/> at any time to change candidates
  6. /// considered for autocomplete.
  7. /// </summary>
  8. public class SingleWordSuggestionGenerator : ISuggestionGenerator
  9. {
  10. /// <summary>The full set of all strings that can be suggested.</summary>
  11. /// <returns></returns>
  12. public virtual List<string> AllSuggestions { get; set; } = new ();
  13. /// <inheritdoc/>
  14. public IEnumerable<Suggestion> GenerateSuggestions (AutocompleteContext context)
  15. {
  16. // if there is nothing to pick from
  17. if (AllSuggestions.Count == 0)
  18. {
  19. return [];
  20. }
  21. List<string> line = context.CurrentLine.Select (c => c.Grapheme).ToList ();
  22. string currentWord = IdxToWord (line, context.CursorPosition, out int startIdx);
  23. context.CursorPosition = startIdx < 1 ? startIdx : Math.Min (startIdx + 1, line.Count);
  24. if (string.IsNullOrWhiteSpace (currentWord))
  25. {
  26. return Enumerable.Empty<Suggestion> ();
  27. }
  28. return AllSuggestions.Where (
  29. o =>
  30. o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase)
  31. && !o.Equals (currentWord, StringComparison.CurrentCultureIgnoreCase)
  32. )
  33. .Select (o => new Suggestion (currentWord.Length, o))
  34. .ToList ()
  35. .AsReadOnly ();
  36. }
  37. /// <summary>
  38. /// Return true if the given symbol should be considered part of a word and can be contained in matches. Base
  39. /// behavior is to use <see cref="char.IsLetterOrDigit(char)"/>
  40. /// </summary>
  41. /// <param name="text">The text.</param>
  42. /// <returns></returns>
  43. public virtual bool IsWordChar (string text)
  44. {
  45. return !string.IsNullOrEmpty (text)
  46. && Rune.IsLetterOrDigit (text.EnumerateRunes ().First ());
  47. }
  48. /// <summary>
  49. /// <para>
  50. /// Given a <paramref name="line"/> of characters, returns the word which ends at <paramref name="idx"/> or null.
  51. /// Also returns null if the <paramref name="idx"/> is positioned in the middle of a word.
  52. /// </para>
  53. /// <para>
  54. /// Use this method to determine whether autocomplete should be shown when the cursor is at a given point in a
  55. /// line and to get the word from which suggestions should be generated. Use the <paramref name="columnOffset"/> to
  56. /// indicate if search the word at left (negative), at right (positive) or at the current column (zero) which is
  57. /// the default.
  58. /// </para>
  59. /// </summary>
  60. /// <param name="line"></param>
  61. /// <param name="idx"></param>
  62. /// <param name="startIdx">The start index of the word.</param>
  63. /// <param name="columnOffset"></param>
  64. /// <returns></returns>
  65. protected virtual string IdxToWord (List<string> line, int idx, out int startIdx, int columnOffset = 0)
  66. {
  67. var sb = new StringBuilder ();
  68. startIdx = idx;
  69. // get the ending word index
  70. while (startIdx < line.Count)
  71. {
  72. if (IsWordChar (line [startIdx]))
  73. {
  74. startIdx++;
  75. }
  76. else
  77. {
  78. break;
  79. }
  80. }
  81. // It isn't a word char then there is no way to autocomplete that word
  82. if (startIdx == idx && columnOffset != 0)
  83. {
  84. return null;
  85. }
  86. // we are at the end of a word. Work out what has been typed so far
  87. while (startIdx-- > 0)
  88. {
  89. if (IsWordChar (line [startIdx]))
  90. {
  91. sb.Insert (0, line [startIdx]);
  92. }
  93. else
  94. {
  95. break;
  96. }
  97. }
  98. startIdx = Math.Max (startIdx, 0);
  99. return sb.ToString ();
  100. }
  101. }