// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Elfie.Model; using Microsoft.CodeAnalysis.Elfie.Model.Structures; using Microsoft.CodeAnalysis.Elfie.Model.Tree; using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Packaging; using Microsoft.CodeAnalysis.Shared.Options; using Microsoft.CodeAnalysis.SymbolSearch; using Microsoft.Internal.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; using Microsoft.VisualStudio.Settings; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Shell.Settings; using Roslyn.Utilities; using VSShell = Microsoft.VisualStudio.Shell; namespace Microsoft.VisualStudio.LanguageServices.SymbolSearch { /// /// A service which enables searching for packages matching certain criteria. /// It works against an database to find results. /// /// This implementation also spawns a task which will attempt to keep that database up to /// date by downloading patches on a daily basis. /// [ExportWorkspaceService(typeof(ISymbolSearchService)), Shared] internal partial class SymbolSearchService : AbstractDelayStartedService, ISymbolSearchService { private readonly Workspace _workspace; private ConcurrentDictionary _sourceToDatabase = new ConcurrentDictionary(); [ImportingConstructor] public SymbolSearchService( VisualStudioWorkspaceImpl workspace, VSShell.SVsServiceProvider serviceProvider) : this(workspace, workspace.Services.GetService(), new RemoteControlService(), new LogService((IVsActivityLog)serviceProvider.GetService(typeof(SVsActivityLog))), new DelayService(), new IOService(), new PatchService(), new DatabaseFactoryService(), new ShellSettingsManager(serviceProvider).GetApplicationDataFolder(ApplicationDataFolder.LocalSettings), // Report all exceptions we encounter, but don't crash on them. FatalError.ReportWithoutCrash, new CancellationTokenSource()) { } /// /// For testing purposes only. /// internal SymbolSearchService( Workspace workspace, IPackageInstallerService installerService, IRemoteControlService remoteControlService, ILogService logService, IDelayService delayService, IIOService ioService, IPatchService patchService, IDatabaseFactoryService databaseFactoryService, string localSettingsDirectory, Func reportAndSwallowException, CancellationTokenSource cancellationTokenSource) : base(workspace, SymbolSearchOptions.Enabled, SymbolSearchOptions.SuggestForTypesInReferenceAssemblies, SymbolSearchOptions.SuggestForTypesInNuGetPackages) { if (remoteControlService == null) { // If we can't access the file update service, then there's nothing we can do. return; } _workspace = workspace; _installerService = installerService; _delayService = delayService; _ioService = ioService; _logService = logService; _remoteControlService = remoteControlService; _patchService = patchService; _databaseFactoryService = databaseFactoryService; _localSettingsDirectory = localSettingsDirectory; _reportAndSwallowException = reportAndSwallowException; _cancellationTokenSource = cancellationTokenSource; _cancellationToken = _cancellationTokenSource.Token; } protected override void EnableService() { // When our service is enabled hook up to package source changes. // We need to know when the list of sources have changed so we can // kick off the work to process them. _installerService.PackageSourcesChanged += OnPackageSourcesChanged; } private void OnPackageSourcesChanged(object sender, EventArgs e) { StartWorking(); } protected override void StartWorking() { // Kick off a database update. Wait a few seconds before starting so we don't // interfere too much with solution loading. var sources = _installerService.PackageSources; // Always pull down the nuget.org index. It contains the MS reference assembly index // inside of it. var allSources = sources.Concat(new PackageSource(NugetOrgSource, source: null)); foreach (var source in allSources) { Task.Run(() => UpdateSourceInBackgroundAsync(source.Name)); } } protected override void StopWorking() { _installerService.PackageSourcesChanged -= OnPackageSourcesChanged; _cancellationTokenSource.Cancel(); } public IEnumerable FindPackagesWithType( string source, string name, int arity, CancellationToken cancellationToken) { IAddReferenceDatabaseWrapper databaseWrapper; if (!_sourceToDatabase.TryGetValue(source, out databaseWrapper)) { // Don't have a database to search. yield break; } var database = databaseWrapper.Database; if (name == "var") { // never find anything named 'var'. yield break; } var query = new MemberQuery(name, isFullSuffix: true, isFullNamespace: false); var symbols = new PartialArray(100); if (query.TryFindMembers(database, ref symbols)) { var types = FilterToViableTypes(symbols); var typesFromPackagesUsedInOtherProjects = new List(); var typesFromPackagesNotUsedInOtherProjects = new List(); foreach (var type in types) { // Ignore any reference assembly results. if (type.PackageName.ToString() != MicrosoftAssemblyReferencesName) { var packageName = type.PackageName.ToString(); if (_installerService.GetInstalledVersions(packageName).Any()) { typesFromPackagesUsedInOtherProjects.Add(type); } else { typesFromPackagesNotUsedInOtherProjects.Add(type); } } } var result = new List(); // We always returm types from packages that we've use elsewhere in the project. int? bestRank = null; foreach (var type in typesFromPackagesUsedInOtherProjects) { yield return CreateResult(database, type); } // For all other hits include as long as the popularity is high enough. // Popularity ranks are in powers of two. So if two packages differ by // one rank, then one is at least twice as popular as the next. Two // ranks would be four times as popular. Three ranks = 8 times, etc. // etc. We keep packages that within 1 rank of the best package we find. foreach (var type in typesFromPackagesNotUsedInOtherProjects) { var rank = GetRank(type); bestRank = bestRank == null ? rank : Math.Max(bestRank.Value, rank); if (Math.Abs(bestRank.Value - rank) > 1) { yield break; } yield return CreateResult(database, type); } } } public IEnumerable FindReferenceAssembliesWithType( string name, int arity, CancellationToken cancellationToken) { // Our reference assembly data is stored in the nuget.org DB. IAddReferenceDatabaseWrapper databaseWrapper; if (!_sourceToDatabase.TryGetValue(NugetOrgSource, out databaseWrapper)) { // Don't have a database to search. yield break; } var database = databaseWrapper.Database; if (name == "var") { // never find anything named 'var'. yield break; } var query = new MemberQuery(name, isFullSuffix: true, isFullNamespace: false); var symbols = new PartialArray(100); if (query.TryFindMembers(database, ref symbols)) { var types = FilterToViableTypes(symbols); foreach (var type in types) { // Only look at reference assembly results. if (type.PackageName.ToString() == MicrosoftAssemblyReferencesName) { var nameParts = new List(); GetFullName(nameParts, type.FullName.Parent); yield return new ReferenceAssemblyWithTypeResult( type.AssemblyName.ToString(), type.Name.ToString(), containingNamespaceNames: nameParts); } } } } private List FilterToViableTypes(PartialArray symbols) { // Don't return nested types. Currently their value does not seem worth // it given all the extra stuff we'd have to plumb through. Namely // going down the "using static" code path and whatnot. return new List( from symbol in symbols where this.IsType(symbol) && !this.IsType(symbol.Parent()) select symbol); } private PackageWithTypeResult CreateResult(AddReferenceDatabase database, Symbol type) { var nameParts = new List(); GetFullName(nameParts, type.FullName.Parent); var packageName = type.PackageName.ToString(); var version = database.GetPackageVersion(type.Index).ToString(); return new PackageWithTypeResult( packageName: packageName, typeName: type.Name.ToString(), version: version, containingNamespaceNames: nameParts); } private int GetRank(Symbol symbol) { Symbol rankingSymbol; int rank; if (!TryGetRankingSymbol(symbol, out rankingSymbol) || !int.TryParse(rankingSymbol.Name.ToString(), out rank)) { return 0; } return rank; } private bool TryGetRankingSymbol(Symbol symbol, out Symbol rankingSymbol) { for (var current = symbol; current.IsValid; current = current.Parent()) { if (current.Type == SymbolType.Package || current.Type == SymbolType.Version) { return TryGetRankingSymbolForPackage(current, out rankingSymbol); } } rankingSymbol = default(Symbol); return false; } private bool TryGetRankingSymbolForPackage(Symbol package, out Symbol rankingSymbol) { for (var child = package.FirstChild(); child.IsValid; child = child.NextSibling()) { if (child.Type == SymbolType.PopularityRank) { rankingSymbol = child; return true; } } rankingSymbol = default(Symbol); return false; } private bool IsType(Symbol symbol) { return symbol.Type.IsType(); } private void GetFullName(List nameParts, Path8 path) { if (!path.IsEmpty) { GetFullName(nameParts, path.Parent); nameParts.Add(path.Name.ToString()); } } } }