SingleWordSuggestionGenerator.cs 4.0 KB

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