210 lines
8.1 KiB
C#
Raw Normal View History

2021-09-20 18:20:01 +02:00
/*
Copyright (C) 2014-2019 de4dot@gmail.com
This file is part of dnSpy
dnSpy is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
dnSpy is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with dnSpy. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
namespace dnSpy.Contracts.Language.Intellisense {
/// <summary>
/// <see cref="Completion"/> collection
/// </summary>
public class DsCompletionSet : CompletionSet {
readonly Completion[] allCompletions;
readonly Completion[] allCompletionBuilders;
readonly FilteredCompletionCollection filteredCompletions;
readonly FilteredCompletionCollection filteredCompletionBuilders;
/// <summary>
/// Gets the filtered <see cref="Completion"/>s
/// </summary>
public override IList<Completion> Completions => filteredCompletions;
/// <summary>
/// Gets the filtered <see cref="Completion"/> builders
/// </summary>
public override IList<Completion> CompletionBuilders => filteredCompletionBuilders;
/// <summary>
/// Gets or sets the text tracking span to which this completion applies
/// </summary>
public override ITrackingSpan ApplicableTo {
get => base.ApplicableTo;
protected set {
searchText = null;
base.ApplicableTo = value;
}
}
/// <summary>
/// Gets the filters
/// </summary>
public virtual IReadOnlyList<DsIntellisenseFilter> Filters { get; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="moniker">Unique non-localized identifier</param>
/// <param name="displayName">Name shown in the UI if there are multiple <see cref="CompletionSet"/>s</param>
/// <param name="applicableTo">Span that will be modified when a <see cref="Completion"/> gets committed</param>
/// <param name="completions">Completion items</param>
/// <param name="completionBuilders">Completion builders</param>
/// <param name="filters">Filters or null</param>
public DsCompletionSet(string moniker, string displayName, ITrackingSpan applicableTo, IEnumerable<Completion> completions, IEnumerable<Completion> completionBuilders, IReadOnlyList<DsIntellisenseFilter>? filters)
: base(moniker, displayName, applicableTo, Array.Empty<Completion>(), Array.Empty<Completion>()) {
allCompletions = completions.ToArray();
allCompletionBuilders = completionBuilders.ToArray();
filteredCompletions = new FilteredCompletionCollection(allCompletions);
filteredCompletionBuilders = new FilteredCompletionCollection(allCompletionBuilders);
Filters = filters ?? Array.Empty<DsIntellisenseFilter>();
}
string SearchText {
get {
var atSpan = ApplicableTo;
if (atSpan is null)
return string.Empty;
if (searchText is null || atSpan.TextBuffer.CurrentSnapshot.Version.VersionNumber != searchTextVersion) {
searchTextVersion = atSpan.TextBuffer.CurrentSnapshot.Version.VersionNumber;
searchText = atSpan.GetText(atSpan.TextBuffer.CurrentSnapshot);
}
return searchText;
}
}
string? searchText;
int searchTextVersion = -1;
/// <summary>
/// Gets highlighted text spans or null
/// </summary>
/// <param name="displayText">Text shown in the UI</param>
/// <returns></returns>
public override IReadOnlyList<Span>? GetHighlightedSpansInDisplayText(string displayText) =>
CreateCompletionFilter(SearchText).GetMatchSpans(displayText);
/// <summary>
/// Creates a <see cref="ICompletionFilter"/>
/// </summary>
/// <param name="searchText">Search text</param>
/// <returns></returns>
public virtual ICompletionFilter CreateCompletionFilter(string searchText) => new CompletionFilter(searchText);
/// <summary>
/// Uses <see cref="DsCompletionSet.Filters"/> to filter <paramref name="completions"/>
/// </summary>
/// <param name="filteredResult">Result</param>
/// <param name="completions">Completion items to filter</param>
protected virtual void Filter(List<Completion> filteredResult, IList<Completion> completions) => filteredResult.AddRange(completions);
/// <summary>
/// Filters the list. <see cref="SelectBestMatch"/> should be called after this method
/// </summary>
public override void Filter() {
Debug2.Assert(ApplicableTo is not null, "You must initialize " + nameof(ApplicableTo) + " before calling this method");
var inputText = SearchText;
var filteredList = new List<Completion>(allCompletions.Length);
Filter(filteredList, allCompletions);
IList<Completion> finalList;
if (inputText.Length < CompletionConstants.MimimumSearchLengthForFilter)
finalList = filteredList;
else {
var list = new List<Completion>(filteredList.Count);
finalList = list;
var completionFilter = CreateCompletionFilter(inputText);
foreach (var c in filteredList) {
if (completionFilter.IsMatch(c))
list.Add(c);
}
}
if (finalList.Count != 0)
filteredCompletions.SetNewFilteredCollection(finalList);
}
/// <summary>
/// Selects the best match and should be called after <see cref="Filter()"/>
/// </summary>
public override void SelectBestMatch() =>
SelectionStatus = GetBestMatch() ?? new CompletionSelectionStatus(null, false, false);
readonly struct MruSelection {
public Completion Completion { get; }
public int Index { get; }
public MruSelection(Completion completion, int index) {
Completion = completion;
Index = index;
}
}
/// <summary>
/// Gets the best match in <see cref="CompletionSet.Completions"/>
/// </summary>
/// <returns></returns>
protected virtual CompletionSelectionStatus GetBestMatch() {
Debug2.Assert(ApplicableTo is not null, "You must initialize " + nameof(ApplicableTo) + " before calling this method");
var inputText = SearchText;
var completionFilter = CreateCompletionFilter(inputText);
int matches = 0;
var selector = new BestMatchSelector(inputText);
var mruSelectionCase = default(MruSelection);
var mruSelection = default(MruSelection);
if (inputText.Length > 0) {
Debug2.Assert(searchText is not null);
foreach (var completion in Completions) {
if (!completionFilter.IsMatch(completion))
continue;
matches++;
selector.Select(completion);
if (completion.DisplayText.StartsWith(searchText, StringComparison.Ordinal)) {
int currentMruIndex = GetMruIndex(completion);
if (mruSelectionCase.Completion is null || currentMruIndex < mruSelectionCase.Index)
mruSelectionCase = new MruSelection(completion, currentMruIndex);
}
else if (completion.DisplayText.StartsWith(searchText, StringComparison.OrdinalIgnoreCase)) {
int currentMruIndex = GetMruIndex(completion);
if (mruSelection.Completion is null || currentMruIndex < mruSelection.Index)
mruSelection = new MruSelection(completion, currentMruIndex);
}
}
}
// If it was an exact match, don't use the MRU-selected completion. Eg.
// local 'i' exists, and we previously typed 'int', and we've just typed 'i',
// then select 'i' and not 'int'
var selectedCompletion = mruSelectionCase.Completion ?? mruSelection.Completion ?? selector.Result;
if (selector.Result is not null && inputText.Equals(selector.Result!.TryGetFilterText(), StringComparison.OrdinalIgnoreCase))
selectedCompletion = selector.Result;
bool isSelected = selectedCompletion is not null;
bool isUnique = matches == 1;
return new CompletionSelectionStatus(selectedCompletion, isSelected, isUnique);
}
/// <summary>
/// Gets the MRU index of <paramref name="completion"/>
/// </summary>
/// <param name="completion">Completion item</param>
/// <returns></returns>
protected virtual int GetMruIndex(Completion completion) => int.MaxValue;
}
}