PageRenderTime 75ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/AvalonEdit/ICSharpCode.AvalonEdit/CodeCompletion/CompletionList.cs

https://github.com/lumimies/ILSpy
C# | 377 lines | 264 code | 40 blank | 73 comment | 73 complexity | 4b5a50c50c5d99e8cac72155cac6b202 MD5 | raw file
  1. // Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt)
  2. // This code is distributed under the GNU LGPL (for details please see \doc\license.txt)
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Collections.ObjectModel;
  6. using System.Globalization;
  7. using System.Windows;
  8. using System.Windows.Controls;
  9. using System.Windows.Controls.Primitives;
  10. using System.Windows.Documents;
  11. using System.Windows.Input;
  12. using System.Linq;
  13. namespace ICSharpCode.AvalonEdit.CodeCompletion
  14. {
  15. /// <summary>
  16. /// The listbox used inside the CompletionWindow, contains CompletionListBox.
  17. /// </summary>
  18. public class CompletionList : Control
  19. {
  20. static CompletionList()
  21. {
  22. DefaultStyleKeyProperty.OverrideMetadata(typeof(CompletionList),
  23. new FrameworkPropertyMetadata(typeof(CompletionList)));
  24. }
  25. bool isFiltering = true;
  26. /// <summary>
  27. /// If true, the CompletionList is filtered to show only matching items. Also enables search by substring.
  28. /// If false, enables the old behavior: no filtering, search by string.StartsWith.
  29. /// </summary>
  30. public bool IsFiltering {
  31. get { return isFiltering; }
  32. set { isFiltering = value; }
  33. }
  34. /// <summary>
  35. /// Dependency property for <see cref="EmptyTemplate" />.
  36. /// </summary>
  37. public static readonly DependencyProperty EmptyTemplateProperty =
  38. DependencyProperty.Register("EmptyTemplate", typeof(ControlTemplate), typeof(CompletionList),
  39. new FrameworkPropertyMetadata());
  40. /// <summary>
  41. /// Content of EmptyTemplate will be shown when CompletionList contains no items.
  42. /// If EmptyTemplate is null, nothing will be shown.
  43. /// </summary>
  44. public ControlTemplate EmptyTemplate {
  45. get { return (ControlTemplate)GetValue(EmptyTemplateProperty); }
  46. set { SetValue(EmptyTemplateProperty, value); }
  47. }
  48. /// <summary>
  49. /// Is raised when the completion list indicates that the user has chosen
  50. /// an entry to be completed.
  51. /// </summary>
  52. public event EventHandler InsertionRequested;
  53. /// <summary>
  54. /// Raises the InsertionRequested event.
  55. /// </summary>
  56. public void RequestInsertion(EventArgs e)
  57. {
  58. if (InsertionRequested != null)
  59. InsertionRequested(this, e);
  60. }
  61. CompletionListBox listBox;
  62. /// <inheritdoc/>
  63. public override void OnApplyTemplate()
  64. {
  65. base.OnApplyTemplate();
  66. listBox = GetTemplateChild("PART_ListBox") as CompletionListBox;
  67. if (listBox != null) {
  68. listBox.ItemsSource = completionData;
  69. }
  70. }
  71. /// <summary>
  72. /// Gets the list box.
  73. /// </summary>
  74. public CompletionListBox ListBox {
  75. get {
  76. if (listBox == null)
  77. ApplyTemplate();
  78. return listBox;
  79. }
  80. }
  81. /// <summary>
  82. /// Gets the scroll viewer used in this list box.
  83. /// </summary>
  84. public ScrollViewer ScrollViewer {
  85. get { return listBox != null ? listBox.scrollViewer : null; }
  86. }
  87. ObservableCollection<ICompletionData> completionData = new ObservableCollection<ICompletionData>();
  88. /// <summary>
  89. /// Gets the list to which completion data can be added.
  90. /// </summary>
  91. public IList<ICompletionData> CompletionData {
  92. get { return completionData; }
  93. }
  94. /// <inheritdoc/>
  95. protected override void OnKeyDown(KeyEventArgs e)
  96. {
  97. base.OnKeyDown(e);
  98. if (!e.Handled) {
  99. HandleKey(e);
  100. }
  101. }
  102. /// <summary>
  103. /// Handles a key press. Used to let the completion list handle key presses while the
  104. /// focus is still on the text editor.
  105. /// </summary>
  106. public void HandleKey(KeyEventArgs e)
  107. {
  108. if (listBox == null)
  109. return;
  110. // We have to do some key handling manually, because the default doesn't work with
  111. // our simulated events.
  112. // Also, the default PageUp/PageDown implementation changes the focus, so we avoid it.
  113. switch (e.Key) {
  114. case Key.Down:
  115. e.Handled = true;
  116. listBox.SelectIndex(listBox.SelectedIndex + 1);
  117. break;
  118. case Key.Up:
  119. e.Handled = true;
  120. listBox.SelectIndex(listBox.SelectedIndex - 1);
  121. break;
  122. case Key.PageDown:
  123. e.Handled = true;
  124. listBox.SelectIndex(listBox.SelectedIndex + listBox.VisibleItemCount);
  125. break;
  126. case Key.PageUp:
  127. e.Handled = true;
  128. listBox.SelectIndex(listBox.SelectedIndex - listBox.VisibleItemCount);
  129. break;
  130. case Key.Home:
  131. e.Handled = true;
  132. listBox.SelectIndex(0);
  133. break;
  134. case Key.End:
  135. e.Handled = true;
  136. listBox.SelectIndex(listBox.Items.Count - 1);
  137. break;
  138. case Key.Tab:
  139. case Key.Enter:
  140. e.Handled = true;
  141. RequestInsertion(e);
  142. break;
  143. }
  144. }
  145. /// <inheritdoc/>
  146. protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
  147. {
  148. base.OnMouseDoubleClick(e);
  149. if (e.ChangedButton == MouseButton.Left) {
  150. e.Handled = true;
  151. RequestInsertion(e);
  152. }
  153. }
  154. /// <summary>
  155. /// Gets/Sets the selected item.
  156. /// </summary>
  157. public ICompletionData SelectedItem {
  158. get {
  159. return (listBox != null ? listBox.SelectedItem : null) as ICompletionData;
  160. }
  161. set {
  162. if (listBox == null && value != null)
  163. ApplyTemplate();
  164. listBox.SelectedItem = value;
  165. }
  166. }
  167. /// <summary>
  168. /// Occurs when the SelectedItem property changes.
  169. /// </summary>
  170. public event SelectionChangedEventHandler SelectionChanged {
  171. add { AddHandler(Selector.SelectionChangedEvent, value); }
  172. remove { RemoveHandler(Selector.SelectionChangedEvent, value); }
  173. }
  174. // SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once
  175. string currentText;
  176. ObservableCollection<ICompletionData> currentList;
  177. /// <summary>
  178. /// Selects the best match, and filter the items if turned on using <see cref="IsFiltering" />.
  179. /// </summary>
  180. public void SelectItem(string text)
  181. {
  182. if (text == currentText)
  183. return;
  184. if (listBox == null)
  185. ApplyTemplate();
  186. if (this.IsFiltering) {
  187. SelectItemFiltering(text);
  188. }
  189. else {
  190. SelectItemWithStart(text);
  191. }
  192. currentText = text;
  193. }
  194. /// <summary>
  195. /// Filters CompletionList items to show only those matching given query, and selects the best match.
  196. /// </summary>
  197. void SelectItemFiltering(string query)
  198. {
  199. // if the user just typed one more character, don't filter all data but just filter what we are already displaying
  200. var listToFilter = (this.currentList != null && (!string.IsNullOrEmpty(this.currentText)) && (!string.IsNullOrEmpty(query)) &&
  201. query.StartsWith(this.currentText, StringComparison.Ordinal)) ?
  202. this.currentList : this.completionData;
  203. var matchingItems =
  204. from item in listToFilter
  205. let quality = GetMatchQuality(item.Text, query)
  206. where quality > 0
  207. select new { Item = item, Quality = quality };
  208. // e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)"
  209. ICompletionData suggestedItem = listBox.SelectedIndex != -1 ? (ICompletionData)(listBox.Items[listBox.SelectedIndex]) : null;
  210. var listBoxItems = new ObservableCollection<ICompletionData>();
  211. int bestIndex = -1;
  212. int bestQuality = -1;
  213. double bestPriority = 0;
  214. int i = 0;
  215. foreach (var matchingItem in matchingItems) {
  216. double priority = matchingItem.Item == suggestedItem ? double.PositiveInfinity : matchingItem.Item.Priority;
  217. int quality = matchingItem.Quality;
  218. if (quality > bestQuality || (quality == bestQuality && (priority > bestPriority))) {
  219. bestIndex = i;
  220. bestPriority = priority;
  221. bestQuality = quality;
  222. }
  223. listBoxItems.Add(matchingItem.Item);
  224. i++;
  225. }
  226. this.currentList = listBoxItems;
  227. listBox.ItemsSource = listBoxItems;
  228. SelectIndexCentered(bestIndex);
  229. }
  230. /// <summary>
  231. /// Selects the item that starts with the specified query.
  232. /// </summary>
  233. void SelectItemWithStart(string query)
  234. {
  235. if (string.IsNullOrEmpty(query))
  236. return;
  237. int suggestedIndex = listBox.SelectedIndex;
  238. int bestIndex = -1;
  239. int bestQuality = -1;
  240. double bestPriority = 0;
  241. for (int i = 0; i < completionData.Count; ++i) {
  242. int quality = GetMatchQuality(completionData[i].Text, query);
  243. if (quality < 0)
  244. continue;
  245. double priority = completionData[i].Priority;
  246. bool useThisItem;
  247. if (bestQuality < quality) {
  248. useThisItem = true;
  249. } else {
  250. if (bestIndex == suggestedIndex) {
  251. useThisItem = false;
  252. } else if (i == suggestedIndex) {
  253. // prefer recommendedItem, regardless of its priority
  254. useThisItem = bestQuality == quality;
  255. } else {
  256. useThisItem = bestQuality == quality && bestPriority < priority;
  257. }
  258. }
  259. if (useThisItem) {
  260. bestIndex = i;
  261. bestPriority = priority;
  262. bestQuality = quality;
  263. }
  264. }
  265. SelectIndexCentered(bestIndex);
  266. }
  267. void SelectIndexCentered(int bestIndex)
  268. {
  269. if (bestIndex < 0) {
  270. listBox.ClearSelection();
  271. } else {
  272. int firstItem = listBox.FirstVisibleItem;
  273. if (bestIndex < firstItem || firstItem + listBox.VisibleItemCount <= bestIndex) {
  274. // CenterViewOn does nothing as CompletionListBox.ScrollViewer is null
  275. listBox.CenterViewOn(bestIndex);
  276. listBox.SelectIndex(bestIndex);
  277. } else {
  278. listBox.SelectIndex(bestIndex);
  279. }
  280. }
  281. }
  282. int GetMatchQuality(string itemText, string query)
  283. {
  284. if (itemText == null)
  285. throw new ArgumentNullException("itemText", "ICompletionData.Text returned null");
  286. // Qualities:
  287. // 8 = full match case sensitive
  288. // 7 = full match
  289. // 6 = match start case sensitive
  290. // 5 = match start
  291. // 4 = match CamelCase when length of query is 1 or 2 characters
  292. // 3 = match substring case sensitive
  293. // 2 = match sustring
  294. // 1 = match CamelCase
  295. // -1 = no match
  296. if (query == itemText)
  297. return 8;
  298. if (string.Equals(itemText, query, StringComparison.InvariantCultureIgnoreCase))
  299. return 7;
  300. if (itemText.StartsWith(query, StringComparison.InvariantCulture))
  301. return 6;
  302. if (itemText.StartsWith(query, StringComparison.InvariantCultureIgnoreCase))
  303. return 5;
  304. bool? camelCaseMatch = null;
  305. if (query.Length <= 2) {
  306. camelCaseMatch = CamelCaseMatch(itemText, query);
  307. if (camelCaseMatch == true) return 4;
  308. }
  309. // search by substring, if filtering (i.e. new behavior) turned on
  310. if (IsFiltering) {
  311. if (itemText.IndexOf(query, StringComparison.InvariantCulture) >= 0)
  312. return 3;
  313. if (itemText.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0)
  314. return 2;
  315. }
  316. if (!camelCaseMatch.HasValue)
  317. camelCaseMatch = CamelCaseMatch(itemText, query);
  318. if (camelCaseMatch == true)
  319. return 1;
  320. return -1;
  321. }
  322. static bool CamelCaseMatch(string text, string query)
  323. {
  324. int i = 0;
  325. foreach (char upper in text.Where(c => char.IsUpper(c))) {
  326. if (i > query.Length - 1)
  327. return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis")
  328. if (char.ToUpper(query[i], CultureInfo.InvariantCulture) != upper)
  329. return false;
  330. i++;
  331. }
  332. if (i >= query.Length)
  333. return true;
  334. return false;
  335. }
  336. }
  337. }