SingleWordSuggestionGenerator.cs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  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 Enumerable.Empty<Suggestion> ();
  20. }
  21. List<Rune> line = context.CurrentLine.Select (c => c.Rune).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="rune">The rune.</param>
  42. /// <returns></returns>
  43. public virtual bool IsWordChar (Rune rune) { return char.IsLetterOrDigit ((char)rune.Value); }
  44. /// <summary>
  45. /// <para>
  46. /// Given a <paramref name="line"/> of characters, returns the word which ends at <paramref name="idx"/> or null.
  47. /// Also returns null if the <paramref name="idx"/> is positioned in the middle of a word.
  48. /// </para>
  49. /// <para>
  50. /// Use this method to determine whether autocomplete should be shown when the cursor is at a given point in a
  51. /// line and to get the word from which suggestions should be generated. Use the <paramref name="columnOffset"/> to
  52. /// indicate if search the word at left (negative), at right (positive) or at the current column (zero) which is
  53. /// the default.
  54. /// </para>
  55. /// </summary>
  56. /// <param name="line"></param>
  57. /// <param name="idx"></param>
  58. /// <param name="startIdx">The start index of the word.</param>
  59. /// <param name="columnOffset"></param>
  60. /// <returns></returns>
  61. protected virtual string IdxToWord (List<Rune> line, int idx, out int startIdx, int columnOffset = 0)
  62. {
  63. var sb = new StringBuilder ();
  64. startIdx = idx;
  65. // get the ending word index
  66. while (startIdx < line.Count)
  67. {
  68. if (IsWordChar (line [startIdx]))
  69. {
  70. startIdx++;
  71. }
  72. else
  73. {
  74. break;
  75. }
  76. }
  77. // It isn't a word char then there is no way to autocomplete that word
  78. if (startIdx == idx && columnOffset != 0)
  79. {
  80. return null;
  81. }
  82. // we are at the end of a word. Work out what has been typed so far
  83. while (startIdx-- > 0)
  84. {
  85. if (IsWordChar (line [startIdx]))
  86. {
  87. sb.Insert (0, (char)line [startIdx].Value);
  88. }
  89. else
  90. {
  91. break;
  92. }
  93. }
  94. startIdx = Math.Max (startIdx, 0);
  95. return sb.ToString ();
  96. }
  97. }