PageRenderTime 51ms CodeModel.GetById 19ms app.highlight 23ms RepoModel.GetById 2ms app.codeStats 0ms

/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
 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		/// <remarks>
196		/// The setter of this property does not scroll to the selected item.
197		/// You might want to also call <see cref="ScrollIntoView"/>.
198		/// </remarks>
199		public ICompletionData SelectedItem {
200			get {
201				return (listBox != null ? listBox.SelectedItem : null) as ICompletionData;
202			}
203			set {
204				if (listBox == null && value != null)
205					ApplyTemplate();
206				if (listBox != null) // may still be null if ApplyTemplate fails, or if listBox and value both are null
207					listBox.SelectedItem = value;
208			}
209		}
210		
211		/// <summary>
212		/// Scrolls the specified item into view.
213		/// </summary>
214		public void ScrollIntoView(ICompletionData item)
215		{
216			if (listBox == null)
217				ApplyTemplate();
218			if (listBox != null)
219				listBox.ScrollIntoView(item);
220		}
221		
222		/// <summary>
223		/// Occurs when the SelectedItem property changes.
224		/// </summary>
225		public event SelectionChangedEventHandler SelectionChanged {
226			add { AddHandler(Selector.SelectionChangedEvent, value); }
227			remove { RemoveHandler(Selector.SelectionChangedEvent, value); }
228		}
229		
230		// SelectItem gets called twice for every typed character (once from FormatLine), this helps execute SelectItem only once
231		string currentText;
232		ObservableCollection<ICompletionData> currentList;
233		
234		/// <summary>
235		/// Selects the best match, and filter the items if turned on using <see cref="IsFiltering" />.
236		/// </summary>
237		public void SelectItem(string text)
238		{
239			if (text == currentText)
240				return;
241			if (listBox == null)
242				ApplyTemplate();
243			
244			if (this.IsFiltering) {
245				SelectItemFiltering(text);
246			}
247			else {
248				SelectItemWithStart(text);
249			}
250			currentText = text;
251		}
252		
253		/// <summary>
254		/// Filters CompletionList items to show only those matching given query, and selects the best match.
255		/// </summary>
256		void SelectItemFiltering(string query)
257		{
258			// if the user just typed one more character, don't filter all data but just filter what we are already displaying
259			var listToFilter = (this.currentList != null && (!string.IsNullOrEmpty(this.currentText)) && (!string.IsNullOrEmpty(query)) &&
260			                    query.StartsWith(this.currentText, StringComparison.Ordinal)) ?
261				this.currentList : this.completionData;
262			
263			var matchingItems =
264				from item in listToFilter
265				let quality = GetMatchQuality(item.Text, query)
266				where quality > 0
267				select new { Item = item, Quality = quality };
268			
269			// e.g. "DateTimeKind k = (*cc here suggests DateTimeKind*)"
270			ICompletionData suggestedItem = listBox.SelectedIndex != -1 ? (ICompletionData)(listBox.Items[listBox.SelectedIndex]) : null;
271			
272			var listBoxItems = new ObservableCollection<ICompletionData>();
273			int bestIndex = -1;
274			int bestQuality = -1;
275			double bestPriority = 0;
276			int i = 0;
277			foreach (var matchingItem in matchingItems) {
278				double priority = matchingItem.Item == suggestedItem ? double.PositiveInfinity : matchingItem.Item.Priority;
279				int quality = matchingItem.Quality;
280				if (quality > bestQuality || (quality == bestQuality && (priority > bestPriority))) {
281					bestIndex = i;
282					bestPriority = priority;
283					bestQuality = quality;
284				}
285				listBoxItems.Add(matchingItem.Item);
286				i++;
287			}
288			this.currentList = listBoxItems;
289			listBox.ItemsSource = listBoxItems;
290			SelectIndexCentered(bestIndex);
291		}
292		
293		/// <summary>
294		/// Selects the item that starts with the specified query.
295		/// </summary>
296		void SelectItemWithStart(string query)
297		{
298			if (string.IsNullOrEmpty(query))
299				return;
300			
301			int suggestedIndex = listBox.SelectedIndex;
302			
303			int bestIndex = -1;
304			int bestQuality = -1;
305			double bestPriority = 0;
306			for (int i = 0; i < completionData.Count; ++i) {
307				int quality = GetMatchQuality(completionData[i].Text, query);
308				if (quality < 0)
309					continue;
310				
311				double priority = completionData[i].Priority;
312				bool useThisItem;
313				if (bestQuality < quality) {
314					useThisItem = true;
315				} else {
316					if (bestIndex == suggestedIndex) {
317						useThisItem = false;
318					} else if (i == suggestedIndex) {
319						// prefer recommendedItem, regardless of its priority
320						useThisItem = bestQuality == quality;
321					} else {
322						useThisItem = bestQuality == quality && bestPriority < priority;
323					}
324				}
325				if (useThisItem) {
326					bestIndex = i;
327					bestPriority = priority;
328					bestQuality = quality;
329				}
330			}
331			SelectIndexCentered(bestIndex);
332		}
333
334		void SelectIndexCentered(int bestIndex)
335		{
336			if (bestIndex < 0) {
337				listBox.ClearSelection();
338			} else {
339				int firstItem = listBox.FirstVisibleItem;
340				if (bestIndex < firstItem || firstItem + listBox.VisibleItemCount <= bestIndex) {
341					// CenterViewOn does nothing as CompletionListBox.ScrollViewer is null
342					listBox.CenterViewOn(bestIndex);
343					listBox.SelectIndex(bestIndex);
344				} else {
345					listBox.SelectIndex(bestIndex);
346				}
347			}
348		}
349
350		int GetMatchQuality(string itemText, string query)
351		{
352			if (itemText == null)
353				throw new ArgumentNullException("itemText", "ICompletionData.Text returned null");
354			
355			// Qualities:
356			//  	8 = full match case sensitive
357			// 		7 = full match
358			// 		6 = match start case sensitive
359			//		5 = match start
360			//		4 = match CamelCase when length of query is 1 or 2 characters
361			// 		3 = match substring case sensitive
362			//		2 = match substring
363			//		1 = match CamelCase
364			//		-1 = no match
365			if (query == itemText)
366				return 8;
367			if (string.Equals(itemText, query, StringComparison.InvariantCultureIgnoreCase))
368				return 7;
369			
370			if (itemText.StartsWith(query, StringComparison.InvariantCulture))
371				return 6;
372			if (itemText.StartsWith(query, StringComparison.InvariantCultureIgnoreCase))
373				return 5;
374			
375			bool? camelCaseMatch = null;
376			if (query.Length <= 2) {
377				camelCaseMatch = CamelCaseMatch(itemText, query);
378				if (camelCaseMatch == true) return 4;
379			}
380			
381			// search by substring, if filtering (i.e. new behavior) turned on
382			if (IsFiltering) {
383				if (itemText.IndexOf(query, StringComparison.InvariantCulture) >= 0)
384					return 3;
385				if (itemText.IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0)
386					return 2;
387			}
388				
389			if (!camelCaseMatch.HasValue)
390				camelCaseMatch = CamelCaseMatch(itemText, query);
391			if (camelCaseMatch == true)
392				return 1;
393			
394			return -1;
395		}
396		
397		static bool CamelCaseMatch(string text, string query)
398		{
399			// We take the first letter of the text regardless of whether or not it's upper case so we match
400			// against camelCase text as well as PascalCase text ("cct" matches "camelCaseText")
401			var theFirstLetterOfEachWord = text.Take(1).Concat(text.Skip(1).Where(char.IsUpper));
402			
403			int i = 0;
404			foreach (var letter in theFirstLetterOfEachWord) {
405				if (i > query.Length - 1)
406					return true;	// return true here for CamelCase partial match ("CQ" matches "CodeQualityAnalysis")
407				if (char.ToUpperInvariant(query[i]) != char.ToUpperInvariant(letter))
408					return false;
409				i++;
410			}
411			if (i >= query.Length)
412				return true;
413			return false;
414		}
415	}
416}