378 lines
11 KiB
C#
378 lines
11 KiB
C#
// 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;
|
|
}
|
|
}
|
|
}
|