AutocompleteBase.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Collections.ObjectModel;
  4. using System.Linq;
  5. using System.Text;
  6. using Rune = System.Rune;
  7. namespace Terminal.Gui {
  8. public interface ISuggestionGenerator
  9. {
  10. /// <summary>
  11. /// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
  12. /// match with the current cursor position/text in the <see cref="HostControl"/>.
  13. /// </summary>
  14. /// <param name="columnOffset">The column offset. Current (zero - default), left (negative), right (positive).</param>
  15. IEnumerable<string> GenerateSuggestions (List<Rune> currentLine, int idx);
  16. bool IsWordChar (Rune rune);
  17. }
  18. public class SingleWordSuggestionGenerator : ISuggestionGenerator
  19. {
  20. /// <summary>
  21. /// The full set of all strings that can be suggested.
  22. /// </summary>
  23. /// <returns></returns>
  24. public virtual List<string> AllSuggestions { get; set; } = new List<string> ();
  25. public IEnumerable<string> GenerateSuggestions (List<Rune> currentLine, int idx)
  26. {
  27. // if there is nothing to pick from
  28. if (AllSuggestions.Count == 0) {
  29. return Enumerable.Empty<string>();
  30. }
  31. var currentWord = IdxToWord (currentLine, idx);
  32. if (string.IsNullOrWhiteSpace (currentWord)) {
  33. return Enumerable.Empty<string>();
  34. } else {
  35. return AllSuggestions.Where (o =>
  36. o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) &&
  37. !o.Equals (currentWord, StringComparison.CurrentCultureIgnoreCase)
  38. ).ToList ().AsReadOnly ();
  39. }
  40. }
  41. /// <summary>
  42. /// Return true if the given symbol should be considered part of a word
  43. /// and can be contained in matches. Base behavior is to use <see cref="char.IsLetterOrDigit(char)"/>
  44. /// </summary>
  45. /// <param name="rune"></param>
  46. /// <returns></returns>
  47. public virtual bool IsWordChar (Rune rune)
  48. {
  49. return Char.IsLetterOrDigit ((char)rune);
  50. }
  51. /// <summary>
  52. /// <para>
  53. /// Given a <paramref name="line"/> of characters, returns the word which ends at <paramref name="idx"/>
  54. /// or null. Also returns null if the <paramref name="idx"/> is positioned in the middle of a word.
  55. /// </para>
  56. ///
  57. /// <para>
  58. /// Use this method to determine whether autocomplete should be shown when the cursor is at
  59. /// a given point in a line and to get the word from which suggestions should be generated.
  60. /// Use the <paramref name="columnOffset"/> to indicate if search the word at left (negative),
  61. /// at right (positive) or at the current column (zero) which is the default.
  62. /// </para>
  63. /// </summary>
  64. /// <param name="line"></param>
  65. /// <param name="idx"></param>
  66. /// <param name="columnOffset"></param>
  67. /// <returns></returns>
  68. protected virtual string IdxToWord (List<Rune> line, int idx, int columnOffset = 0)
  69. {
  70. StringBuilder sb = new StringBuilder ();
  71. var endIdx = idx;
  72. // get the ending word index
  73. while (endIdx < line.Count) {
  74. if (IsWordChar (line [endIdx])) {
  75. endIdx++;
  76. } else {
  77. break;
  78. }
  79. }
  80. // It isn't a word char then there is no way to autocomplete that word
  81. if (endIdx == idx && columnOffset != 0) {
  82. return null;
  83. }
  84. // we are at the end of a word. Work out what has been typed so far
  85. while (endIdx-- > 0) {
  86. if (IsWordChar (line [endIdx])) {
  87. sb.Insert (0, (char)line [endIdx]);
  88. } else {
  89. break;
  90. }
  91. }
  92. return sb.ToString ();
  93. }
  94. }
  95. public abstract class AutocompleteBase : IAutocomplete {
  96. public abstract View HostControl { get; set; }
  97. public bool PopupInsideContainer { get; set; }
  98. public ISuggestionGenerator SuggestionGenerator {get;set;} = new SingleWordSuggestionGenerator();
  99. /// <summary>
  100. /// The maximum width of the autocomplete dropdown
  101. /// </summary>
  102. public virtual int MaxWidth { get; set; } = 10;
  103. /// <summary>
  104. /// The maximum number of visible rows in the autocomplete dropdown to render
  105. /// </summary>
  106. public virtual int MaxHeight { get; set; } = 6;
  107. /// <inheritdoc/>
  108. /// <summary>
  109. /// True if the autocomplete should be considered open and visible
  110. /// </summary>
  111. public virtual bool Visible { get; set; }
  112. /// <summary>
  113. /// The strings that form the current list of suggestions to render
  114. /// based on what the user has typed so far.
  115. /// </summary>
  116. public virtual ReadOnlyCollection<string> Suggestions { get; set; } = new ReadOnlyCollection<string> (new string [0]);
  117. /// <summary>
  118. /// The currently selected index into <see cref="Suggestions"/> that the user has highlighted
  119. /// </summary>
  120. public virtual int SelectedIdx { get; set; }
  121. /// <inheritdoc/>
  122. public abstract ColorScheme ColorScheme { get; set; }
  123. /// <inheritdoc/>
  124. public virtual Key SelectionKey { get; set; } = Key.Enter;
  125. /// <inheritdoc/>
  126. public virtual Key CloseKey { get; set; } = Key.Esc;
  127. /// <inheritdoc/>
  128. public virtual Key Reopen { get; set; } = Key.Space | Key.CtrlMask | Key.AltMask;
  129. /// <inheritdoc/>
  130. public abstract bool MouseEvent (MouseEvent me, bool fromHost = false);
  131. /// <inheritdoc/>
  132. public abstract bool ProcessKey (KeyEvent kb);
  133. /// <inheritdoc/>
  134. public abstract void RenderOverlay (Point renderAt);
  135. /// <summary>
  136. /// Clears <see cref="Suggestions"/>
  137. /// </summary>
  138. public virtual void ClearSuggestions ()
  139. {
  140. Suggestions = Enumerable.Empty<string> ().ToList ().AsReadOnly ();
  141. }
  142. /// <summary>
  143. /// Populates <see cref="Suggestions"/> with all strings in <see cref="AllSuggestions"/> that
  144. /// match with the current cursor position/text in the <see cref="HostControl"/>
  145. /// </summary>
  146. /// <param name="columnOffset">The column offset.</param>
  147. public virtual void GenerateSuggestions (List<Rune> currentLine, int idx)
  148. {
  149. Suggestions = SuggestionGenerator.GenerateSuggestions(currentLine, idx).ToList().AsReadOnly();
  150. EnsureSelectedIdxIsValid ();
  151. }
  152. /// <summary>
  153. /// Updates <see cref="SelectedIdx"/> to be a valid index within <see cref="Suggestions"/>
  154. /// </summary>
  155. public virtual void EnsureSelectedIdxIsValid ()
  156. {
  157. SelectedIdx = Math.Max (0, Math.Min (Suggestions.Count - 1, SelectedIdx));
  158. }
  159. }
  160. }