Click here to Skip to main content
15,886,710 members
Articles / Desktop Programming / WPF

Using AvalonEdit (WPF Text Editor)

Rate me:
Please Sign up or sign in to vote.
4.97/5 (271 votes)
1 Apr 2013LGPL313 min read 1.8M   72.3K   534  
AvalonEdit is an extensible Open-Source text editor with support for syntax highlighting and folding.
// 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;
		}
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Germany Germany
I am the lead developer on the SharpDevelop open source project.

Comments and Discussions