Free cookie consent management tool by TermsFeed Policy Generator

source: branches/CodeEditor/HeuristicLab.ExtLibs/HeuristicLab.AvalonEdit/5.0.1/AvalonEdit-5.0.1/CodeCompletion/CompletionList.cs @ 11700

Last change on this file since 11700 was 11700, checked in by jkarder, 9 years ago

#2077: created branch and added first version

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