using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using OpenTap.Translation;
namespace OpenTap.Package
{
internal class AutoCorrectException : Exception
{
public AutoCorrectException(string message) : base(message)
{
}
}
internal static class AutoCorrectPackageNames
{
///
/// Correct each name in the string array based on the available packages in the repositories.
/// If a package name does not exist, an exception is thrown.
///
///
///
///
public static string[] Correct(string[] names, IEnumerable repositories)
{
if (names == null || names.Length == 0) return names;
// Copy the input array to use as return value
var result = names.ToArray();
var repos = repositories.ToArray();
List onlinePackages = null;
var packageCache = PackageRepositoryHelpers.DetermineRepositoryType(new Uri(PackageCacheHelper.PackageCacheDirectory).AbsoluteUri);
var knownPackages = Installation.Current.GetPackages().Select(p => p.Name)
.Concat(packageCache.GetPackageNames()).ToHashSet();
for (int i = 0; i < names.Length; i++)
{
var name = names[i];
if (File.Exists(name)) continue;
var notFoundMessage = $"Package '{name}' not found.";
if (string.IsNullOrWhiteSpace(name) || knownPackages.Contains(name))
{
result[i] = name;
continue;
}
// Checking the repositories is slow. Postpone it as much as possible.
// In most cases we can resolve a correctly spelled package from local caches
if (onlinePackages == null)
{
onlinePackages = repos.SelectMany(r => r.GetPackageNames()).ToList();
foreach (var pkg in onlinePackages)
{
knownPackages.Add(pkg);
}
if (knownPackages.Contains(name))
{
result[i] = name;
continue;
}
}
var matchThreshold = 3;
var matcher = new FuzzyMatcher(name, matchThreshold);
var matchList = knownPackages.Select(matcher.Score).Where(m => m.Score <= matchThreshold).ToArray();
// If any match is almost a perfect match, only consider perfect matches
if (matchList.Any(m => m.Score == 0))
matchList = matchList.Where(m => m.Score == 0).ToArray();
var scores = matchList.OrderBy(m => m.Score).Take(5).ToArray();
if (scores.Length == 0)
{
// There are no packages
throw new AutoCorrectException(notFoundMessage);
}
var options = scores.Select(s => s.Candidate).ToList();
var req = new AutoCorrectRequest(name, options);
UserInput.Request(req);
if (req.Choice == req.NegativeAnswer)
throw new AutoCorrectException(notFoundMessage);
if (req.Choice == req.Yes)
result[i] = options[0];
else result[i] = req.Choice;
}
return result;
}
}
[Display("Correct package name?")]
class AutoCorrectRequest : IStringLocalizer
{
public string NegativeAnswer => Options.Length == 1 ? No : Cancel;
public string Yes => this.Translate("Yes");
public string No => this.Translate("No");
public string Cancel => this.Translate("Cancel");
public string SingleOptionMessage => this.TranslateFormat("Package '{0}' not found. Did you mean '{1}'?").Format(Package, Options.FirstOrDefault());
public string MultipleOptionsMessage => this.TranslateFormat("Package '{0}' not found. Did you mean:").Format(Package);
[Layout(LayoutMode.FullRow)]
[Browsable(true)]
public string Message => Options.Length == 1 ? SingleOptionMessage : MultipleOptionsMessage;
// If there is only one option, provide a yes/no dialog
// Otherwise, provide a ranked list
public string[] Choices => Options.Length == 1 ? [Yes, No] : [.. Options, Cancel];
[Submit]
[Layout(LayoutMode.FullRow | LayoutMode.FloatBottom)]
[AvailableValues(nameof(Choices))]
public string Choice { get; set; }
private readonly string Package;
private readonly string[] Options = [];
public AutoCorrectRequest(string package, IEnumerable options)
{
Package = package;
Options = [.. options];
Choice = Choices[0];
}
public AutoCorrectRequest()
{
// default constructor needed so the type can be instantiated without parameters
}
}
}