mirror of https://github.com/icsharpcode/ILSpy.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
377 lines
11 KiB
377 lines
11 KiB
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) |
|
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.Collections.ObjectModel; |
|
using System.Globalization; |
|
using System.Windows; |
|
using System.Windows.Controls; |
|
using System.Windows.Controls.Primitives; |
|
using System.Windows.Documents; |
|
using System.Windows.Input; |
|
using System.Linq; |
|
|
|
namespace ICSharpCode.AvalonEdit.CodeCompletion |
|
{ |
|
/// <summary> |
|
/// The listbox used inside the CompletionWindow, contains CompletionListBox. |
|
/// </summary> |
|
public class CompletionList : Control |
|
{ |
|
static CompletionList() |
|
{ |
|
DefaultStyleKeyProperty.OverrideMetadata(typeof(CompletionList), |
|
new FrameworkPropertyMetadata(typeof(CompletionList))); |
|
} |
|
|
|
bool isFiltering = true; |
|
/// <summary> |
|
/// If true, the CompletionList is filtered to show only matching items. Also enables search by substring. |
|
/// If false, enables the old behavior: no filtering, search by string.StartsWith. |
|
/// </summary> |
|
public bool IsFiltering { |
|
get { return isFiltering; } |
|
set { isFiltering = value; } |
|
} |
|
|
|
/// <summary> |
|
/// Dependency property for <see cref="EmptyTemplate" />. |
|
/// </summary> |
|
public static readonly DependencyProperty EmptyTemplateProperty = |
|
DependencyProperty.Register("EmptyTemplate", typeof(ControlTemplate), typeof(CompletionList), |
|
new FrameworkPropertyMetadata()); |
|
|
|
/// <summary> |
|
/// Content of EmptyTemplate will be shown when CompletionList contains no items. |
|
/// If EmptyTemplate is null, nothing will be shown. |
|
/// </summary> |
|
public ControlTemplate EmptyTemplate { |
|
get { return (ControlTemplate)GetValue(EmptyTemplateProperty); } |
|
set { SetValue(EmptyTemplateProperty, value); } |
|
} |
|
|
|
/// <summary> |
|
/// Is raised when the completion list indicates that the user has chosen |
|
/// an entry to be completed. |
|
/// </summary> |
|
public event EventHandler InsertionRequested; |
|
|
|
/// <summary> |
|
/// Raises the InsertionRequested event. |
|
/// </summary> |
|
public void RequestInsertion(EventArgs e) |
|
{ |
|
if (InsertionRequested != null) |
|
InsertionRequested(this, e); |
|
} |
|
|
|
CompletionListBox listBox; |
|
|
|
/// <inheritdoc/> |
|
public override void OnApplyTemplate() |
|
{ |
|
base.OnApplyTemplate(); |
|
|
|
listBox = GetTemplateChild("PART_ListBox") as CompletionListBox; |
|
if (listBox != null) { |
|
listBox.ItemsSource = completionData; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Gets the list box. |
|
/// </summary> |
|
public CompletionListBox ListBox { |
|
get { |
|
if (listBox == null) |
|
ApplyTemplate(); |
|
return listBox; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Gets the scroll viewer used in this list box. |
|
/// </summary> |
|
public ScrollViewer ScrollViewer { |
|
get { return listBox != null ? listBox.scrollViewer : null; } |
|
} |
|
|
|
ObservableCollection<ICompletionData> completionData = new ObservableCollection<ICompletionData>(); |
|
|
|
/// <summary> |
|
/// Gets the list to which completion data can be added. |
|
/// </summary> |
|
public IList<ICompletionData> CompletionData { |
|
get { return completionData; } |
|
} |
|
|
|
/// <inheritdoc/> |
|
protected override void OnKeyDown(KeyEventArgs e) |
|
{ |
|
base.OnKeyDown(e); |
|
if (!e.Handled) { |
|
HandleKey(e); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Handles a key press. Used to let the completion list handle key presses while the |
|
/// focus is still on the text editor. |
|
/// </summary> |
|
public void HandleKey(KeyEventArgs e) |
|
{ |
|
if (listBox == null) |
|
return; |
|
|
|
// We have to do some key handling manually, because the default doesn't work with |
|
// our simulated events. |
|
// Also, the default PageUp/PageDown implementation changes the focus, so we avoid it. |
|
switch (e.Key) { |
|
case Key.Down: |
|
e.Handled = true; |
|
listBox.SelectIndex(listBox.SelectedIndex + 1); |
|
break; |
|
case Key.Up: |
|
e.Handled = true; |
|
listBox.SelectIndex(listBox.SelectedIndex - 1); |
|
break; |
|
case Key.PageDown: |
|
e.Handled = true; |
|
listBox.SelectIndex(listBox.SelectedIndex + listBox.VisibleItemCount); |
|
break; |
|
case Key.PageUp: |
|
e.Handled = true; |
|
listBox.SelectIndex(listBox.SelectedIndex - listBox.VisibleItemCount); |
|
break; |
|
case Key.Home: |
|
e.Handled = true; |
|
listBox.SelectIndex(0); |
|
break; |
|
case Key.End: |
|
e.Handled = true; |
|
listBox.SelectIndex(listBox.Items.Count - 1); |
|
break; |
|
case Key.Tab: |
|
case Key.Enter: |
|
e.Handled = true; |
|
RequestInsertion(e); |
|
break; |
|
} |
|
} |
|
|
|
/// <inheritdoc/> |
|
protected override void OnMouseDoubleClick(MouseButtonEventArgs e) |
|
{ |
|
base.OnMouseDoubleClick(e); |
|
if (e.ChangedButton == MouseButton.Left) { |
|
e.Handled = true; |
|
RequestInsertion(e); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Gets/Sets the selected item. |
|
/// </summary> |
|
public ICompletionData SelectedItem { |
|
get { |
|
return (listBox != null ? listBox.SelectedItem : null) as ICompletionData; |
|
} |
|
set { |
|
if (listBox == null && value != null) |
|
ApplyTemplate(); |
|
listBox.SelectedItem = value; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Occurs when the SelectedItem property changes. |
|
/// </summary> |
|
public event SelectionChangedEventHandler SelectionChanged { |
|
add { AddHandler(Selector.SelectionChangedEvent, value); } |
|
remove { RemoveHandler(Selector.SelectionChangedEvent, value); } |
|
} |
|
|
|
// SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once |
|
string currentText; |
|
ObservableCollection<ICompletionData> currentList; |
|
|
|
/// <summary> |
|
/// Selects the best match, and filter the items if turned on using <see cref="IsFiltering" />. |
|
/// </summary> |
|
public void SelectItem(string text) |
|
{ |
|
if (text == currentText) |
|
return; |
|
if (listBox == null) |
|
ApplyTemplate(); |
|
|
|
if (this.IsFiltering) { |
|
SelectItemFiltering(text); |
|
} |
|
else { |
|
SelectItemWithStart(text); |
|
} |
|
currentText = text; |
|
} |
|
|
|
/// <summary> |
|
/// Filters CompletionList items to show only those matching given query, and selects the best match. |
|
/// </summary> |
|
void SelectItemFiltering(string query) |
|
{ |
|
// if the user just typed one more character, don't filter all data but just filter what we are already displaying |
|
var listToFilter = (this.currentList != null && (!string.IsNullOrEmpty(this.currentText)) && (!string.IsNullOrEmpty(query)) && |
|
query.StartsWith(this.currentText, StringComparison.Ordinal)) ? |
|
this.currentList : this.completionData; |
|
|
|
var matchingItems = |
|
from item in listToFilter |
|
let quality = GetMatchQuality(item.Text, query) |
|
where quality > 0 |
|
select new { Item = item, Quality = quality }; |
|
|
|
// e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)" |
|
ICompletionData suggestedItem = listBox.SelectedIndex != -1 ? (ICompletionData)(listBox.Items[listBox.SelectedIndex]) : null; |
|
|
|
var listBoxItems = new ObservableCollection<ICompletionData>(); |
|
int bestIndex = -1; |
|
int bestQuality = -1; |
|
double bestPriority = 0; |
|
int i = 0; |
|
foreach (var matchingItem in matchingItems) { |
|
double priority = matchingItem.Item == suggestedItem ? double.PositiveInfinity : matchingItem.Item.Priority; |
|
int quality = matchingItem.Quality; |
|
if (quality > bestQuality || (quality == bestQuality && (priority > bestPriority))) { |
|
bestIndex = i; |
|
bestPriority = priority; |
|
bestQuality = quality; |
|
} |
|
listBoxItems.Add(matchingItem.Item); |
|
i++; |
|
} |
|
this.currentList = listBoxItems; |
|
listBox.ItemsSource = listBoxItems; |
|
SelectIndexCentered(bestIndex); |
|
} |
|
|
|
/// <summary> |
|
/// Selects the item that starts with the specified query. |
|
/// </summary> |
|
void SelectItemWithStart(string query) |
|
{ |
|
if (string.IsNullOrEmpty(query)) |
|
return; |
|
|
|
int suggestedIndex = listBox.SelectedIndex; |
|
|
|
int bestIndex = -1; |
|
int bestQuality = -1; |
|
double bestPriority = 0; |
|
for (int i = 0; i < completionData.Count; ++i) { |
|
int quality = GetMatchQuality(completionData[i].Text, query); |
|
if (quality < 0) |
|
continue; |
|
|
|
double priority = completionData[i].Priority; |
|
bool useThisItem; |
|
if (bestQuality < quality) { |
|
useThisItem = true; |
|
} else { |
|
if (bestIndex == suggestedIndex) { |
|
useThisItem = false; |
|
} else if (i == suggestedIndex) { |
|
// prefer recommendedItem, regardless of its priority |
|
useThisItem = bestQuality == quality; |
|
} else { |
|
useThisItem = bestQuality == quality && bestPriority < priority; |
|
} |
|
} |
|
if (useThisItem) { |
|
bestIndex = i; |
|
bestPriority = priority; |
|
bestQuality = quality; |
|
} |
|
} |
|
SelectIndexCentered(bestIndex); |
|
} |
|
|
|
void SelectIndexCentered(int bestIndex) |
|
{ |
|
if (bestIndex < 0) { |
|
listBox.ClearSelection(); |
|
} else { |
|
int firstItem = listBox.FirstVisibleItem; |
|
if (bestIndex < firstItem || firstItem + listBox.VisibleItemCount <= bestIndex) { |
|
// CenterViewOn does nothing as CompletionListBox.ScrollViewer is null |
|
listBox.CenterViewOn(bestIndex); |
|
listBox.SelectIndex(bestIndex); |
|
} else { |
|
listBox.SelectIndex(bestIndex); |
|
} |
|
} |
|
} |
|
|
|
int GetMatchQuality(string itemText, string query) |
|
{ |
|
if (itemText == null) |
|
throw new ArgumentNullException("itemText", "ICompletionData.Text returned null"); |
|
|
|
// Qualities: |
|
// 8 = full match case sensitive |
|
// 7 = full match |
|
// 6 = match start case sensitive |
|
// 5 = match start |
|
// 4 = match CamelCase when length of query is 1 or 2 characters |
|
// 3 = match substring case sensitive |
|
// 2 = match sustring |
|
// 1 = match CamelCase |
|
// -1 = no match |
|
if (query == itemText) |
|
return 8; |
|
if (string.Equals(itemText, query, StringComparison.InvariantCultureIgnoreCase)) |
|
return 7; |
|
|
|
if (itemText.StartsWith(query, StringComparison.InvariantCulture)) |
|
return 6; |
|
if (itemText.StartsWith(query, StringComparison.InvariantCultureIgnoreCase)) |
|
return 5; |
|
|
|
bool? camelCaseMatch = null; |
|
if (query.Length <= 2) { |
|
camelCaseMatch = CamelCaseMatch(itemText, query); |
|
if (camelCaseMatch == true) return 4; |
|
} |
|
|
|
// search by substring, if filtering (i.e. new behavior) turned on |
|
if (IsFiltering) { |
|
if (itemText.IndexOf(query, StringComparison.InvariantCulture) >= 0) |
|
return 3; |
|
if (itemText.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0) |
|
return 2; |
|
} |
|
|
|
if (!camelCaseMatch.HasValue) |
|
camelCaseMatch = CamelCaseMatch(itemText, query); |
|
if (camelCaseMatch == true) |
|
return 1; |
|
|
|
return -1; |
|
} |
|
|
|
static bool CamelCaseMatch(string text, string query) |
|
{ |
|
int i = 0; |
|
foreach (char upper in text.Where(c => char.IsUpper(c))) { |
|
if (i > query.Length - 1) |
|
return true; // return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis") |
|
if (char.ToUpper(query[i], CultureInfo.InvariantCulture) != upper) |
|
return false; |
|
i++; |
|
} |
|
if (i >= query.Length) |
|
return true; |
|
return false; |
|
} |
|
} |
|
}
|
|
|