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

http://github.com/icsharpcode/ILSpy · C# · 416 lines · 276 code · 42 blank · 98 comment · 81 complexity · 5f5acb6d4b0e69644ec475ffe59373c9 MD5 · raw file

  1. // Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
  2. //
  3. // Permission is hereby granted, free of charge, to any person obtaining a copy of this
  4. // software and associated documentation files (the "Software"), to deal in the Software
  5. // without restriction, including without limitation the rights to use, copy, modify, merge,
  6. // publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
  7. // to whom the Software is furnished to do so, subject to the following conditions:
  8. //
  9. // The above copyright notice and this permission notice shall be included in all copies or
  10. // substantial portions of the Software.
  11. //
  12. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
  13. // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
  14. // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
  15. // FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
  16. // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  17. // DEALINGS IN THE SOFTWARE.
  18. using System;
  19. using System.Collections.Generic;
  20. using System.Collections.ObjectModel;
  21. using System.Globalization;
  22. using System.Windows;
  23. using System.Windows.Controls;
  24. using System.Windows.Controls.Primitives;
  25. using System.Windows.Documents;
  26. using System.Windows.Input;
  27. using System.Linq;
  28. using ICSharpCode.AvalonEdit.Utils;
  29. namespace ICSharpCode.AvalonEdit.CodeCompletion
  30. {
  31. /// <summary>
  32. /// The listbox used inside the CompletionWindow, contains CompletionListBox.
  33. /// </summary>
  34. public class CompletionList : Control
  35. {
  36. static CompletionList()
  37. {
  38. DefaultStyleKeyProperty.OverrideMetadata(typeof(CompletionList),
  39. new FrameworkPropertyMetadata(typeof(CompletionList)));
  40. }
  41. bool isFiltering = true;
  42. /// <summary>
  43. /// If true, the CompletionList is filtered to show only matching items. Also enables search by substring.
  44. /// If false, enables the old behavior: no filtering, search by string.StartsWith.
  45. /// </summary>
  46. public bool IsFiltering {
  47. get { return isFiltering; }
  48. set { isFiltering = value; }
  49. }
  50. /// <summary>
  51. /// Dependency property for <see cref="EmptyTemplate" />.
  52. /// </summary>
  53. public static readonly DependencyProperty EmptyTemplateProperty =
  54. DependencyProperty.Register("EmptyTemplate", typeof(ControlTemplate), typeof(CompletionList),
  55. new FrameworkPropertyMetadata());
  56. /// <summary>
  57. /// Content of EmptyTemplate will be shown when CompletionList contains no items.
  58. /// If EmptyTemplate is null, nothing will be shown.
  59. /// </summary>
  60. public ControlTemplate EmptyTemplate {
  61. get { return (ControlTemplate)GetValue(EmptyTemplateProperty); }
  62. set { SetValue(EmptyTemplateProperty, value); }
  63. }
  64. /// <summary>
  65. /// Is raised when the completion list indicates that the user has chosen
  66. /// an entry to be completed.
  67. /// </summary>
  68. public event EventHandler InsertionRequested;
  69. /// <summary>
  70. /// Raises the InsertionRequested event.
  71. /// </summary>
  72. public void RequestInsertion(EventArgs e)
  73. {
  74. if (InsertionRequested != null)
  75. InsertionRequested(this, e);
  76. }
  77. CompletionListBox listBox;
  78. /// <inheritdoc/>
  79. public override void OnApplyTemplate()
  80. {
  81. base.OnApplyTemplate();
  82. listBox = GetTemplateChild("PART_ListBox") as CompletionListBox;
  83. if (listBox != null) {
  84. listBox.ItemsSource = completionData;
  85. }
  86. }
  87. /// <summary>
  88. /// Gets the list box.
  89. /// </summary>
  90. public CompletionListBox ListBox {
  91. get {
  92. if (listBox == null)
  93. ApplyTemplate();
  94. return listBox;
  95. }
  96. }
  97. /// <summary>
  98. /// Gets the scroll viewer used in this list box.
  99. /// </summary>
  100. public ScrollViewer ScrollViewer {
  101. get { return listBox != null ? listBox.scrollViewer : null; }
  102. }
  103. ObservableCollection<ICompletionData> completionData = new ObservableCollection<ICompletionData>();
  104. /// <summary>
  105. /// Gets the list to which completion data can be added.
  106. /// </summary>
  107. public IList<ICompletionData> CompletionData {
  108. get { return completionData; }
  109. }
  110. /// <inheritdoc/>
  111. protected override void OnKeyDown(KeyEventArgs e)
  112. {
  113. base.OnKeyDown(e);
  114. if (!e.Handled) {
  115. HandleKey(e);
  116. }
  117. }
  118. /// <summary>
  119. /// Handles a key press. Used to let the completion list handle key presses while the
  120. /// focus is still on the text editor.
  121. /// </summary>
  122. public void HandleKey(KeyEventArgs e)
  123. {
  124. if (listBox == null)
  125. return;
  126. // We have to do some key handling manually, because the default doesn't work with
  127. // our simulated events.
  128. // Also, the default PageUp/PageDown implementation changes the focus, so we avoid it.
  129. switch (e.Key) {
  130. case Key.Down:
  131. e.Handled = true;
  132. listBox.SelectIndex(listBox.SelectedIndex + 1);
  133. break;
  134. case Key.Up:
  135. e.Handled = true;
  136. listBox.SelectIndex(listBox.SelectedIndex - 1);
  137. break;
  138. case Key.PageDown:
  139. e.Handled = true;
  140. listBox.SelectIndex(listBox.SelectedIndex + listBox.VisibleItemCount);
  141. break;
  142. case Key.PageUp:
  143. e.Handled = true;
  144. listBox.SelectIndex(listBox.SelectedIndex - listBox.VisibleItemCount);
  145. break;
  146. case Key.Home:
  147. e.Handled = true;
  148. listBox.SelectIndex(0);
  149. break;
  150. case Key.End:
  151. e.Handled = true;
  152. listBox.SelectIndex(listBox.Items.Count - 1);
  153. break;
  154. case Key.Tab:
  155. case Key.Enter:
  156. e.Handled = true;
  157. RequestInsertion(e);
  158. break;
  159. }
  160. }
  161. /// <inheritdoc/>
  162. protected override void OnMouseDoubleClick(MouseButtonEventArgs e)
  163. {
  164. base.OnMouseDoubleClick(e);
  165. if (e.ChangedButton == MouseButton.Left) {
  166. // only process double clicks on the ListBoxItems, not on the scroll bar
  167. if (ExtensionMethods.VisualAncestorsAndSelf(e.OriginalSource as DependencyObject).TakeWhile(obj => obj != this).Any(obj => obj is ListBoxItem)) {
  168. e.Handled = true;
  169. RequestInsertion(e);
  170. }
  171. }
  172. }
  173. /// <summary>
  174. /// Gets/Sets the selected item.
  175. /// </summary>
  176. /// <remarks>
  177. /// The setter of this property does not scroll to the selected item.
  178. /// You might want to also call <see cref="ScrollIntoView"/>.
  179. /// </remarks>
  180. public ICompletionData SelectedItem {
  181. get {
  182. return (listBox != null ? listBox.SelectedItem : null) as ICompletionData;
  183. }
  184. set {
  185. if (listBox == null && value != null)
  186. ApplyTemplate();
  187. if (listBox != null) // may still be null if ApplyTemplate fails, or if listBox and value both are null
  188. listBox.SelectedItem = value;
  189. }
  190. }
  191. /// <summary>
  192. /// Scrolls the specified item into view.
  193. /// </summary>
  194. public void ScrollIntoView(ICompletionData item)
  195. {
  196. if (listBox == null)
  197. ApplyTemplate();
  198. if (listBox != null)
  199. listBox.ScrollIntoView(item);
  200. }
  201. /// <summary>
  202. /// Occurs when the SelectedItem property changes.
  203. /// </summary>
  204. public event SelectionChangedEventHandler SelectionChanged {
  205. add { AddHandler(Selector.SelectionChangedEvent, value); }
  206. remove { RemoveHandler(Selector.SelectionChangedEvent, value); }
  207. }
  208. // SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once
  209. string currentText;
  210. ObservableCollection<ICompletionData> currentList;
  211. /// <summary>
  212. /// Selects the best match, and filter the items if turned on using <see cref="IsFiltering" />.
  213. /// </summary>
  214. public void SelectItem(string text)
  215. {
  216. if (text == currentText)
  217. return;
  218. if (listBox == null)
  219. ApplyTemplate();
  220. if (this.IsFiltering) {
  221. SelectItemFiltering(text);
  222. }
  223. else {
  224. SelectItemWithStart(text);
  225. }
  226. currentText = text;
  227. }
  228. /// <summary>
  229. /// Filters CompletionList items to show only those matching given query, and selects the best match.
  230. /// </summary>
  231. void SelectItemFiltering(string query)
  232. {
  233. // if the user just typed one more character, don't filter all data but just filter what we are already displaying
  234. var listToFilter = (this.currentList != null && (!string.IsNullOrEmpty(this.currentText)) && (!string.IsNullOrEmpty(query)) &&
  235. query.StartsWith(this.currentText, StringComparison.Ordinal)) ?
  236. this.currentList : this.completionData;
  237. var matchingItems =
  238. from item in listToFilter
  239. let quality = GetMatchQuality(item.Text, query)
  240. where quality > 0
  241. select new { Item = item, Quality = quality };
  242. // e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)"
  243. ICompletionData suggestedItem = listBox.SelectedIndex != -1 ? (ICompletionData)(listBox.Items[listBox.SelectedIndex]) : null;
  244. var listBoxItems = new ObservableCollection<ICompletionData>();
  245. int bestIndex = -1;
  246. int bestQuality = -1;
  247. double bestPriority = 0;
  248. int i = 0;
  249. foreach (var matchingItem in matchingItems) {
  250. double priority = matchingItem.Item == suggestedItem ? double.PositiveInfinity : matchingItem.Item.Priority;
  251. int quality = matchingItem.Quality;
  252. if (quality > bestQuality || (quality == bestQuality && (priority > bestPriority))) {
  253. bestIndex = i;
  254. bestPriority = priority;
  255. bestQuality = quality;
  256. }
  257. listBoxItems.Add(matchingItem.Item);
  258. i++;
  259. }
  260. this.currentList = listBoxItems;
  261. listBox.ItemsSource = listBoxItems;
  262. SelectIndexCentered(bestIndex);
  263. }
  264. /// <summary>
  265. /// Selects the item that starts with the specified query.
  266. /// </summary>
  267. void SelectItemWithStart(string query)
  268. {
  269. if (string.IsNullOrEmpty(query))
  270. return;
  271. int suggestedIndex = listBox.SelectedIndex;
  272. int bestIndex = -1;
  273. int bestQuality = -1;
  274. double bestPriority = 0;
  275. for (int i = 0; i < completionData.Count; ++i) {
  276. int quality = GetMatchQuality(completionData[i].Text, query);
  277. if (quality < 0)
  278. continue;
  279. double priority = completionData[i].Priority;
  280. bool useThisItem;
  281. if (bestQuality < quality) {
  282. useThisItem = true;
  283. } else {
  284. if (bestIndex == suggestedIndex) {
  285. useThisItem = false;
  286. } else if (i == suggestedIndex) {
  287. // prefer recommendedItem, regardless of its priority
  288. useThisItem = bestQuality == quality;
  289. } else {
  290. useThisItem = bestQuality == quality && bestPriority < priority;
  291. }
  292. }
  293. if (useThisItem) {
  294. bestIndex = i;
  295. bestPriority = priority;
  296. bestQuality = quality;
  297. }
  298. }
  299. SelectIndexCentered(bestIndex);
  300. }
  301. void SelectIndexCentered(int bestIndex)
  302. {
  303. if (bestIndex < 0) {
  304. listBox.ClearSelection();
  305. } else {
  306. int firstItem = listBox.FirstVisibleItem;
  307. if (bestIndex < firstItem || firstItem + listBox.VisibleItemCount <= bestIndex) {
  308. // CenterViewOn does nothing as CompletionListBox.ScrollViewer is null
  309. listBox.CenterViewOn(bestIndex);
  310. listBox.SelectIndex(bestIndex);
  311. } else {
  312. listBox.SelectIndex(bestIndex);
  313. }
  314. }
  315. }
  316. int GetMatchQuality(string itemText, string query)
  317. {
  318. if (itemText == null)
  319. throw new ArgumentNullException("itemText", "ICompletionData.Text returned null");
  320. // Qualities:
  321. // 8 = full match case sensitive
  322. // 7 = full match
  323. // 6 = match start case sensitive
  324. // 5 = match start
  325. // 4 = match CamelCase when length of query is 1 or 2 characters
  326. // 3 = match substring case sensitive
  327. // 2 = match substring
  328. // 1 = match CamelCase
  329. // -1 = no match
  330. if (query == itemText)
  331. return 8;
  332. if (string.Equals(itemText, query, StringComparison.InvariantCultureIgnoreCase))
  333. return 7;
  334. if (itemText.StartsWith(query, StringComparison.InvariantCulture))
  335. return 6;
  336. if (itemText.StartsWith(query, StringComparison.InvariantCultureIgnoreCase))
  337. return 5;
  338. bool? camelCaseMatch = null;
  339. if (query.Length <= 2) {
  340. camelCaseMatch = CamelCaseMatch(itemText, query);
  341. if (camelCaseMatch == true) return 4;
  342. }
  343. // search by substring, if filtering (i.e. new behavior) turned on
  344. if (IsFiltering) {
  345. if (itemText.IndexOf(query, StringComparison.InvariantCulture) >= 0)
  346. return 3;
  347. if (itemText.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0)
  348. return 2;
  349. }
  350. if (!camelCaseMatch.HasValue)
  351. camelCaseMatch = CamelCaseMatch(itemText, query);
  352. if (camelCaseMatch == true)
  353. return 1;
  354. return -1;
  355. }
  356. static bool CamelCaseMatch(string text, string query)
  357. {
  358. // We take the first letter of the text regardless of whether or not it's upper case so we match
  359. // against camelCase text as well as PascalCase text ("cct" matches "camelCaseText")
  360. var theFirstLetterOfEachWord = text.Take(1).Concat(text.Skip(1).Where(char.IsUpper));
  361. int i = 0;
  362. foreach (var letter in theFirstLetterOfEachWord) {
  363. if (i > query.Length - 1)
  364. return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis")
  365. if (char.ToUpperInvariant(query[i]) != char.ToUpperInvariant(letter))
  366. return false;
  367. i++;
  368. }
  369. if (i >= query.Length)
  370. return true;
  371. return false;
  372. }
  373. }
  374. }