AutocompleteBase.cs 5.5 KB

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