AppendAutocomplete.cs 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. #nullable disable
  2. 
  3. namespace Terminal.Gui.Views;
  4. /// <summary>
  5. /// Autocomplete for a <see cref="TextField"/> which shows suggestions within the box. Displayed suggestions can
  6. /// be completed using the tab key.
  7. /// </summary>
  8. public class AppendAutocomplete : AutocompleteBase
  9. {
  10. private bool _suspendSuggestions;
  11. private TextField _textField;
  12. /// <summary>Creates a new instance of the <see cref="AppendAutocomplete"/> class.</summary>
  13. public AppendAutocomplete (TextField textField)
  14. {
  15. _textField = textField;
  16. base.SelectionKey = KeyCode.Tab;
  17. Scheme = new Scheme
  18. {
  19. Normal = new Attribute (Color.DarkGray, Color.Black),
  20. Focus = new Attribute (Color.DarkGray, Color.Black),
  21. HotNormal = new Attribute (Color.DarkGray, Color.Black),
  22. HotFocus = new Attribute (Color.DarkGray, Color.Black),
  23. Disabled = new Attribute (Color.DarkGray, Color.Black)
  24. };
  25. }
  26. /// <summary>
  27. /// The color used for rendering the appended text. Note that only <see cref="Scheme.Normal"/> is used and
  28. /// then only <see cref="Attribute.Foreground"/> (Background comes from <see cref="HostControl"/>).
  29. /// </summary>
  30. public override Scheme Scheme { get; set; }
  31. /// <inheritdoc/>
  32. public override View HostControl
  33. {
  34. get => _textField;
  35. set => _textField = (TextField)value;
  36. }
  37. /// <inheritdoc/>
  38. public override void ClearSuggestions ()
  39. {
  40. base.ClearSuggestions ();
  41. _textField.SetNeedsDraw ();
  42. }
  43. /// <inheritdoc/>
  44. public override void GenerateSuggestions (AutocompleteContext context)
  45. {
  46. if (_suspendSuggestions)
  47. {
  48. _suspendSuggestions = false;
  49. return;
  50. }
  51. base.GenerateSuggestions (context);
  52. }
  53. /// <inheritdoc/>
  54. public override bool OnMouseEvent (MouseEventArgs me, bool fromHost = false) { return false; }
  55. /// <inheritdoc/>
  56. public override bool ProcessKey (Key a)
  57. {
  58. Key key = a.KeyCode;
  59. if (key == SelectionKey)
  60. {
  61. return AcceptSelectionIfAny ();
  62. }
  63. if (key == Key.CursorUp)
  64. {
  65. return CycleSuggestion (1);
  66. }
  67. if (key == Key.CursorDown)
  68. {
  69. return CycleSuggestion (-1);
  70. }
  71. if (key == CloseKey && Suggestions.Any ())
  72. {
  73. ClearSuggestions ();
  74. _suspendSuggestions = true;
  75. return true;
  76. }
  77. if (char.IsLetterOrDigit ((char)a))
  78. {
  79. _suspendSuggestions = false;
  80. }
  81. return false;
  82. }
  83. /// <summary>Renders the current suggestion into the <see cref="TextField"/></summary>
  84. public override void RenderOverlay (Point renderAt)
  85. {
  86. if (!MakingSuggestion ())
  87. {
  88. return;
  89. }
  90. // draw it like it's selected, even though it's not
  91. _textField.SetAttribute (
  92. new Attribute (
  93. Scheme.Normal.Foreground,
  94. _textField.GetAttributeForRole(VisualRole.Focus).Background,
  95. Scheme.Normal.Style
  96. )
  97. );
  98. _textField.Move (_textField.Text.Length, 0);
  99. Suggestion suggestion = Suggestions.ElementAt (SelectedIdx);
  100. string fragment = suggestion.Replacement.Substring (suggestion.Remove);
  101. int spaceAvailable = _textField.Viewport.Width - _textField.Text.GetColumns ();
  102. int spaceRequired = fragment.EnumerateRunes ().Sum (c => c.GetColumns ());
  103. if (spaceAvailable < spaceRequired)
  104. {
  105. fragment = new string (
  106. fragment.TakeWhile (c => (spaceAvailable -= ((Rune)c).GetColumns ()) >= 0)
  107. .ToArray ()
  108. );
  109. }
  110. _textField.Driver?.AddStr (fragment);
  111. }
  112. /// <summary>
  113. /// Accepts the current autocomplete suggestion displaying in the text box. Returns true if a valid suggestion was
  114. /// being rendered and acceptable or false if no suggestion was showing.
  115. /// </summary>
  116. /// <returns></returns>
  117. internal bool AcceptSelectionIfAny ()
  118. {
  119. if (MakingSuggestion ())
  120. {
  121. Suggestion insert = Suggestions.ElementAt (SelectedIdx);
  122. string newText = _textField.Text;
  123. newText = newText.Substring (0, newText.Length - insert.Remove);
  124. newText += insert.Replacement;
  125. _textField.Text = newText;
  126. _textField.MoveEnd ();
  127. ClearSuggestions ();
  128. return true;
  129. }
  130. return false;
  131. }
  132. internal void SetTextTo (FileSystemInfo fileSystemInfo)
  133. {
  134. string newText = fileSystemInfo.FullName;
  135. if (fileSystemInfo is DirectoryInfo)
  136. {
  137. newText += Path.DirectorySeparatorChar;
  138. }
  139. _textField.Text = newText;
  140. _textField.MoveEnd ();
  141. }
  142. private bool CycleSuggestion (int direction)
  143. {
  144. if (Suggestions.Count <= 1)
  145. {
  146. return false;
  147. }
  148. SelectedIdx = (SelectedIdx + direction) % Suggestions.Count;
  149. if (SelectedIdx < 0)
  150. {
  151. SelectedIdx = Suggestions.Count () - 1;
  152. }
  153. _textField.SetNeedsDraw ();
  154. return true;
  155. }
  156. /// <summary>
  157. /// Returns true if there is a suggestion that can be made and the control is in a state where user would expect
  158. /// to see auto-complete (i.e. focused and cursor in right place).
  159. /// </summary>
  160. /// <returns></returns>
  161. private bool MakingSuggestion () { return Suggestions.Any () && SelectedIdx != -1 && _textField.HasFocus && _textField.CursorIsAtEnd (); }
  162. }