AbstractTypeImportCompletionProvider.cs 18.8 KB
Newer Older
G
Gen Lu 已提交
1 2
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

G
Gen Lu 已提交
3
using System;
4
using System.Collections.Generic;
G
Gen Lu 已提交
5
using System.Collections.Immutable;
6
using System.Diagnostics;
7
using System.Linq;
G
Gen Lu 已提交
8 9 10
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.AddImports;
11
using Microsoft.CodeAnalysis.Completion.Log;
12
using Microsoft.CodeAnalysis.Completion.Providers.ImportCompletion;
13
using Microsoft.CodeAnalysis.Debugging;
G
Gen Lu 已提交
14 15 16 17
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Experiments;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Internal.Log;
G
Gen Lu 已提交
18
using Microsoft.CodeAnalysis.LanguageServices;
19
using Microsoft.CodeAnalysis.PooledObjects;
G
Gen Lu 已提交
20
using Microsoft.CodeAnalysis.Shared;
G
Gen Lu 已提交
21 22
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
23
using Microsoft.CodeAnalysis.Simplification;
24
using Microsoft.CodeAnalysis.Text;
G
Gen Lu 已提交
25 26 27 28 29

namespace Microsoft.CodeAnalysis.Completion.Providers
{
    internal abstract partial class AbstractTypeImportCompletionProvider : CommonCompletionProvider
    {
G
Gen Lu 已提交
30 31
        private bool? _isTypeImportCompletionExperimentEnabled = null;

G
Gen Lu 已提交
32 33
        protected abstract Task<SyntaxContext> CreateContextAsync(Document document, int position, CancellationToken cancellationToken);

34
        protected abstract ImmutableArray<string> GetImportedNamespaces(
G
Gen Lu 已提交
35 36 37
            SyntaxNode location,
            SemanticModel semanticModel,
            CancellationToken cancellationToken);
G
Gen Lu 已提交
38

G
Gen Lu 已提交
39 40
        // Telemetry shows that the average processing time with cache warmed up for 99th percentile is ~700ms.
        // Therefore we set the timeout to 1s to ensure it only applies to the case that cache is cold.
41
        internal static int TimeoutInMilliseconds { get; set; } = 1000;
G
Gen Lu 已提交
42

G
Gen Lu 已提交
43 44 45
        public override async Task ProvideCompletionsAsync(CompletionContext completionContext)
        {
            var cancellationToken = completionContext.CancellationToken;
46
            var document = completionContext.Document;
G
Gen Lu 已提交
47
            var workspace = document.Project.Solution.Workspace;
G
Gen Lu 已提交
48

G
Gen Lu 已提交
49
            var importCompletionOptionValue = completionContext.Options.GetOption(CompletionOptions.ShowItemsFromUnimportedNamespaces, document.Project.Language);
G
Gen Lu 已提交
50

G
Gen Lu 已提交
51
            // Don't trigger import completion if the option value is "default" and the experiment is disabled for the user. 
G
Gen Lu 已提交
52
            if (importCompletionOptionValue == false ||
G
Gen Lu 已提交
53
                (importCompletionOptionValue == null && !IsTypeImportCompletionExperimentEnabled(workspace)))
G
Gen Lu 已提交
54 55 56 57
            {
                return;
            }

G
Gen Lu 已提交
58
            var syntaxContext = await CreateContextAsync(document, completionContext.Position, cancellationToken).ConfigureAwait(false);
G
Gen Lu 已提交
59
            if (!syntaxContext.IsTypeContext)
G
Gen Lu 已提交
60
            {
G
Gen Lu 已提交
61 62
                return;
            }
G
Gen Lu 已提交
63

64 65
            using (Logger.LogBlock(FunctionId.Completion_TypeImportCompletionProvider_GetCompletionItemsAsync, cancellationToken))
            using (var telemetryCounter = new TelemetryCounter())
G
Gen Lu 已提交
66
            {
G
Gen Lu 已提交
67
                await AddCompletionItemsAsync(completionContext, syntaxContext, telemetryCounter, cancellationToken).ConfigureAwait(false);
68 69 70
            }
        }

G
Gen Lu 已提交
71 72 73 74 75 76 77 78 79 80 81
        private bool IsTypeImportCompletionExperimentEnabled(Workspace workspace)
        {
            if (!_isTypeImportCompletionExperimentEnabled.HasValue)
            {
                var experimentationService = workspace.Services.GetService<IExperimentationService>();
                _isTypeImportCompletionExperimentEnabled = experimentationService.IsExperimentEnabled(WellKnownExperimentNames.TypeImportCompletion);
            }

            return _isTypeImportCompletionExperimentEnabled == true;
        }

G
Gen Lu 已提交
82
        private async Task AddCompletionItemsAsync(CompletionContext completionContext, SyntaxContext syntaxContext, TelemetryCounter telemetryCounter, CancellationToken cancellationToken)
83 84 85
        {
            var document = completionContext.Document;
            var project = document.Project;
86
            var workspace = project.Solution.Workspace;
87
            var typeImportCompletionService = document.GetLanguageService<ITypeImportCompletionService>();
G
Gen Lu 已提交
88

89 90 91
            // Find all namespaces in scope at current cursor location, 
            // which will be used to filter so the provider only returns out-of-scope types.
            var namespacesInScope = GetNamespacesInScope(document, syntaxContext, cancellationToken);
G
Gen Lu 已提交
92

G
Gen Lu 已提交
93 94
            var tasksToGetCompletionItems = ArrayBuilder<Task<ImmutableArray<CompletionItem>>>.GetInstance();

95 96
            // Get completion items from current project. 
            var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
G
Gen Lu 已提交
97 98 99 100 101 102 103 104 105
            tasksToGetCompletionItems.Add(Task.Run(() => typeImportCompletionService.GetTopLevelTypesAsync(
                project,
                syntaxContext,
                isInternalsVisible: true,
                cancellationToken)));

            // Get declarations from directly referenced projects and PEs.
            // This can be parallelized because we don't add items to CompletionContext
            // until all the collected tasks are completed.
G
Gen Lu 已提交
106
            var referencedAssemblySymbols = compilation.GetReferencedAssemblySymbols();
G
Gen Lu 已提交
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
            tasksToGetCompletionItems.AddRange(
                referencedAssemblySymbols.Select(symbol => Task.Run(() => HandleReferenceAsync(symbol))));

            // We want to timebox the operation that might need to traverse all the type symbols and populate the cache, 
            // the idea is not to block completion for too long (likely to happen the first time import completion is triggered).
            // The trade-off is we might not provide unimported types until the cache is warmed up.
            var combinedTask = Task.WhenAll(tasksToGetCompletionItems.ToImmutableAndFree());
            if (await Task.WhenAny(combinedTask, Task.Delay(TimeoutInMilliseconds, cancellationToken)).ConfigureAwait(false) == combinedTask)
            {
                // No timeout. We now have all completion items ready. 
                var completionItemsToAdd = await combinedTask.ConfigureAwait(false);
                foreach (var completionItems in completionItemsToAdd)
                {
                    AddItems(completionItems, completionContext, namespacesInScope, telemetryCounter);
                }
            }
            else
            {
                // If timed out, we don't want to cancel the computation so next time the cache would be populated.
                // We do not keep track if previous compuation for a given project/PE reference is still running. So there's a chance 
                // we queue same computation again later. However, we expect such computation for an individual reference to be relatively 
                // fast so the actual cycles wasted would be insignificant.
                telemetryCounter.TimedOut = true;
            }

            telemetryCounter.ReferenceCount = referencedAssemblySymbols.Length;

            return;

            async Task<ImmutableArray<CompletionItem>> HandleReferenceAsync(IAssemblySymbol referencedAssemblySymbol)
137 138
            {
                cancellationToken.ThrowIfCancellationRequested();
G
Gen Lu 已提交
139

140
                // Skip reference with only non-global alias.
G
Gen Lu 已提交
141 142
                var metadataReference = compilation.GetMetadataReference(referencedAssemblySymbol);

143 144
                if (metadataReference.Properties.Aliases.IsEmpty ||
                    metadataReference.Properties.Aliases.Any(alias => alias == MetadataReferenceProperties.GlobalAlias))
145
                {
G
Gen Lu 已提交
146
                    var assemblyProject = project.Solution.GetProject(referencedAssemblySymbol, cancellationToken);
147 148
                    if (assemblyProject != null && assemblyProject.SupportsCompilation)
                    {
G
Gen Lu 已提交
149
                        return await typeImportCompletionService.GetTopLevelTypesAsync(
150
                            assemblyProject,
151
                            syntaxContext,
G
Gen Lu 已提交
152
                            isInternalsVisible: compilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(referencedAssemblySymbol),
153 154 155 156
                            cancellationToken).ConfigureAwait(false);
                    }
                    else if (metadataReference is PortableExecutableReference peReference)
                    {
G
Gen Lu 已提交
157
                        return typeImportCompletionService.GetTopLevelTypesFromPEReference(
158 159 160
                            project.Solution,
                            compilation,
                            peReference,
161
                            syntaxContext,
G
Gen Lu 已提交
162
                            isInternalsVisible: compilation.Assembly.IsSameAssemblyOrHasFriendAccessTo(referencedAssemblySymbol),
163 164
                            cancellationToken);
                    }
G
Gen Lu 已提交
165
                }
166

G
Gen Lu 已提交
167 168
                return ImmutableArray<CompletionItem>.Empty;
            }
169

G
Gen Lu 已提交
170
            static void AddItems(ImmutableArray<CompletionItem> items, CompletionContext completionContext, HashSet<string> namespacesInScope, TelemetryCounter counter)
G
Gen Lu 已提交
171
            {
G
Gen Lu 已提交
172
                foreach (var item in items)
G
Gen Lu 已提交
173
                {
174
                    var containingNamespace = TypeImportCompletionItem.GetContainingNamespace(item);
175 176
                    if (!namespacesInScope.Contains(containingNamespace))
                    {
G
Gen Lu 已提交
177 178 179 180
                        // We can return cached item directly, item's span will be fixed by completion service.
                        // On the other hand, because of this (i.e. mutating the  span of cached item for each run),
                        // the provider can not be used as a service by components that might be run in parallel 
                        // with completion, which would be a race.
181
                        completionContext.AddItem(item);
G
Gen Lu 已提交
182
                        counter.ItemsCount++; ;
183
                    }
G
Gen Lu 已提交
184 185 186 187
                }
            }
        }

188
        private HashSet<string> GetNamespacesInScope(Document document, SyntaxContext syntaxContext, CancellationToken cancellationToken)
G
Gen Lu 已提交
189 190
        {
            var semanticModel = syntaxContext.SemanticModel;
191 192 193 194 195
            var importedNamespaces = GetImportedNamespaces(syntaxContext.LeftToken.Parent, semanticModel, cancellationToken);

            // This hashset will be used to match namespace names, so it must have the same case-sensitivity as the source language.
            var syntaxFacts = document.GetLanguageService<ISyntaxFactsService>();
            var namespacesInScope = new HashSet<string>(importedNamespaces, syntaxFacts.StringComparer);
G
Gen Lu 已提交
196 197 198 199 200

            // Get containing namespaces.
            var namespaceSymbol = semanticModel.GetEnclosingNamespace(syntaxContext.Position, cancellationToken);
            while (namespaceSymbol != null)
            {
201
                namespacesInScope.Add(namespaceSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat));
G
Gen Lu 已提交
202 203 204
                namespaceSymbol = namespaceSymbol.ContainingNamespace;
            }

205
            return namespacesInScope;
G
Gen Lu 已提交
206 207
        }

208
        internal override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem completionItem, TextSpan completionListSpan, char? commitKey, CancellationToken cancellationToken)
G
Gen Lu 已提交
209
        {
210 211 212
            var containingNamespace = TypeImportCompletionItem.GetContainingNamespace(completionItem);
            Debug.Assert(containingNamespace != null);

G
Gen Lu 已提交
213
            if (await ShouldCompleteWithFullyQualifyTypeName().ConfigureAwait(false))
214 215 216 217 218 219 220
            {
                var fullyQualifiedName = $"{containingNamespace}.{completionItem.DisplayText}";
                var change = new TextChange(completionListSpan, fullyQualifiedName);

                return CompletionChange.Create(change);
            }
            else
221 222 223 224
            {
                // Find context node so we can use it to decide where to insert using/imports.
                var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                var root = await tree.GetRootAsync(cancellationToken).ConfigureAwait(false);
225
                var addImportContextNode = root.FindToken(completionListSpan.Start, findInsideTrivia: true).Parent;
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255

                // Add required using/imports directive.                              
                var addImportService = document.GetLanguageService<IAddImportsService>();
                var optionSet = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false);
                var placeSystemNamespaceFirst = optionSet.GetOption(GenerationOptions.PlaceSystemNamespaceFirst, document.Project.Language);
                var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
                var importNode = CreateImport(document, containingNamespace);

                var rootWithImport = addImportService.AddImport(compilation, root, addImportContextNode, importNode, placeSystemNamespaceFirst);
                var documentWithImport = document.WithSyntaxRoot(rootWithImport);
                var formattedDocumentWithImport = await Formatter.FormatAsync(documentWithImport, Formatter.Annotation, cancellationToken: cancellationToken).ConfigureAwait(false);

                var builder = ArrayBuilder<TextChange>.GetInstance();

                // Get text change for add improt
                var importChanges = await formattedDocumentWithImport.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false);
                builder.AddRange(importChanges);

                // Create text change for complete type name.
                //
                // Note: Don't try to obtain TextChange for completed type name by replacing the text directly, 
                //       then use Document.GetTextChangesAsync on document created from the changed text. This is
                //       because it will do a diff and return TextChanges with minimum span instead of actual 
                //       replacement span.
                //
                //       For example: If I'm typing "asd", the completion provider could be triggered after "a"
                //       is typed. Then if I selected type "AsnEncodedData" to commit, by using the approach described 
                //       above, we will get a TextChange of "AsnEncodedDat" with 0 length span, instead of a change of 
                //       the full display text with a span of length 1. This will later mess up span-tracking and end up 
                //       with "AsnEncodedDatasd" in the code.
256
                builder.Add(new TextChange(completionListSpan, completionItem.DisplayText));
257 258 259 260 261 262 263

                // Then get the combined change
                var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false);
                var newText = text.WithChanges(builder);

                return CompletionChange.Create(Utilities.Collapse(newText, builder.ToImmutableAndFree()));
            }
264

G
Gen Lu 已提交
265
            async Task<bool> ShouldCompleteWithFullyQualifyTypeName()
266
            {
267
                var workspace = document.Project.Solution.Workspace;
268

269 270 271
                // Certain types of workspace don't support document change, e.g. DebuggerIntellisense
                if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument))
                {
G
Gen Lu 已提交
272
                    return true;
273 274 275 276 277 278
                }

                // During an EnC session, adding import is not supported.
                var encService = workspace.Services.GetService<IDebuggingWorkspaceService>()?.EditAndContinueServiceOpt;
                if (encService?.EditSession != null)
                {
G
Gen Lu 已提交
279
                    return true;
280
                }
G
Gen Lu 已提交
281 282 283 284 285

                // Certain documents, e.g. Razor document, don't support adding imports
                var documentSupportsFeatureService = workspace.Services.GetService<IDocumentSupportsFeatureService>();
                if (!documentSupportsFeatureService.SupportsRefactorings(document))
                {
G
Gen Lu 已提交
286
                    return true;
G
Gen Lu 已提交
287
                }
288

289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
                // We might need to qualify unimported types to use them in an import directive, because they only affect members of the containing
                // import container (e.g. namespace/class/etc. declarations).
                //
                // For example, `List` and `StringBuilder` both need to be fully qualified below: 
                // 
                //      using CollectionOfStringBuilders = System.Collections.Generic.List<System.Text.StringBuilder>;
                //
                // However, if we are typing in an C# using directive that is inside a nested import container (i.e. inside a namespace declaration block), 
                // then we can add an using in the outer import container instead (this is not allowed in VB). 
                //
                // For example:
                //
                //      using System.Collections.Generic;
                //      using System.Text;
                //
                //      namespace Foo
                //      {
                //          using CollectionOfStringBuilders = List<StringBuilder>;
                //      }
                //
                // Here we will always choose to qualify the unimported type, just to be consistent and keeps things simple.
                var syntaxContext = await CreateContextAsync(document, completionListSpan.Start, cancellationToken).ConfigureAwait(false);
G
Gen Lu 已提交
311
                return syntaxContext.IsInImportsDirective;
312
            }
G
Gen Lu 已提交
313 314 315 316 317 318 319 320
        }

        private static SyntaxNode CreateImport(Document document, string namespaceName)
        {
            var syntaxGenerator = SyntaxGenerator.GetGenerator(document);
            return syntaxGenerator.NamespaceImportDeclaration(namespaceName).WithAdditionalAnnotations(Formatter.Annotation);
        }

G
Gen Lu 已提交
321 322
        protected override Task<CompletionDescription> GetDescriptionWorkerAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
            => TypeImportCompletionItem.GetCompletionDescriptionAsync(document, item, cancellationToken);
G
Gen Lu 已提交
323

324
        private class TelemetryCounter : IDisposable
G
Gen Lu 已提交
325
        {
326
            private readonly int _tick;
327

G
Gen Lu 已提交
328 329
            public int ItemsCount { get; set; }

330 331
            public int ReferenceCount { get; set; }

G
Gen Lu 已提交
332 333
            public bool TimedOut { get; set; }

334 335 336 337 338 339
            public TelemetryCounter()
            {
                _tick = Environment.TickCount;
            }

            public void Dispose()
G
Gen Lu 已提交
340
            {
341 342 343 344
                var delta = Environment.TickCount - _tick;
                CompletionProvidersLogger.LogTypeImportCompletionTicksDataPoint(delta);
                CompletionProvidersLogger.LogTypeImportCompletionItemCountDataPoint(ItemsCount);
                CompletionProvidersLogger.LogTypeImportCompletionReferenceCountDataPoint(ReferenceCount);
G
Gen Lu 已提交
345 346 347 348 349

                if (TimedOut)
                {
                    CompletionProvidersLogger.LogTypeImportCompletionTimeout();
                }
G
Gen Lu 已提交
350 351
            }
        }
G
Gen Lu 已提交
352 353
    }
}