AbstractTypeImportCompletionProvider.cs 20.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 41 42
        // 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.
        internal int TimeoutInMilliseconds => 1000;

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);

213 214
            var (shouldFullyQualify, needsSimplification) = await ShouldCompleteWithFullyQualifyTypeName().ConfigureAwait(false);
            if (shouldFullyQualify)
215 216 217 218
            {
                var fullyQualifiedName = $"{containingNamespace}.{completionItem.DisplayText}";
                var change = new TextChange(completionListSpan, fullyQualifiedName);

219 220 221 222 223
                if (needsSimplification)
                {
                    change = await GetSimplifiedChange(document, change, cancellationToken).ConfigureAwait(false);
                }

224 225 226
                return CompletionChange.Create(change);
            }
            else
227 228 229 230
            {
                // 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);
231
                var addImportContextNode = root.FindToken(completionListSpan.Start, findInsideTrivia: true).Parent;
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261

                // 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.
262
                builder.Add(new TextChange(completionListSpan, completionItem.DisplayText));
263 264 265 266 267 268 269

                // 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()));
            }
270

271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293
            // Apply the change of fully qualified type name, then try to simplify it.
            static async Task<TextChange> GetSimplifiedChange(Document document, TextChange fullyQualifiedChange, CancellationToken cancellationToken)
            {
                var originalTree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
                var originalText = await originalTree.GetTextAsync(cancellationToken).ConfigureAwait(false);

                var fullyQualifiedText = originalText.WithChanges(fullyQualifiedChange);
                var fullyQualifiedTree = originalTree.WithChangedText(fullyQualifiedText);
                var fullyQualifiedRoot = await fullyQualifiedTree.GetRootAsync(cancellationToken).ConfigureAwait(false);
                var fullyQualifiedSpan = new TextSpan(fullyQualifiedChange.Span.Start, fullyQualifiedChange.NewText.Length);
                var fullyQualifiedNode = fullyQualifiedRoot.FindNode(fullyQualifiedSpan);
                var fullyQualifiedNodeWithAnnotation = fullyQualifiedNode.WithAdditionalAnnotations(Simplifier.Annotation);

                var annotatedRoot = fullyQualifiedRoot.ReplaceNode(fullyQualifiedNode, fullyQualifiedNodeWithAnnotation);
                var annotatedDocument = document.WithSyntaxRoot(annotatedRoot);

                var simplifiedDocument = await Simplifier.ReduceAsync(annotatedDocument, cancellationToken: cancellationToken).ConfigureAwait(false);

                var changes = await simplifiedDocument.GetTextChangesAsync(document, cancellationToken).ConfigureAwait(false);
                return Utilities.Collapse(originalText, changes.ToImmutableArray());
            }

            async Task<(bool shouldFullyQualify, bool needsSimplification)> ShouldCompleteWithFullyQualifyTypeName()
294
            {
295
                var workspace = document.Project.Solution.Workspace;
296

297 298 299
                // Certain types of workspace don't support document change, e.g. DebuggerIntellisense
                if (!workspace.CanApplyChange(ApplyChangesKind.ChangeDocument))
                {
300
                    return (true, false);
301 302 303 304 305 306
                }

                // During an EnC session, adding import is not supported.
                var encService = workspace.Services.GetService<IDebuggingWorkspaceService>()?.EditAndContinueServiceOpt;
                if (encService?.EditSession != null)
                {
307
                    return (true, false);
308
                }
G
Gen Lu 已提交
309 310 311 312 313

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

317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
                // 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);
                return (syntaxContext.IsInImportsDirective, true);
340
            }
G
Gen Lu 已提交
341 342 343 344 345 346 347 348
        }

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

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

352
        private class TelemetryCounter : IDisposable
G
Gen Lu 已提交
353
        {
354
            private readonly int _tick;
355

G
Gen Lu 已提交
356 357
            public int ItemsCount { get; set; }

358 359
            public int ReferenceCount { get; set; }

G
Gen Lu 已提交
360 361
            public bool TimedOut { get; set; }

362 363 364 365 366 367
            public TelemetryCounter()
            {
                _tick = Environment.TickCount;
            }

            public void Dispose()
G
Gen Lu 已提交
368
            {
369 370 371 372
                var delta = Environment.TickCount - _tick;
                CompletionProvidersLogger.LogTypeImportCompletionTicksDataPoint(delta);
                CompletionProvidersLogger.LogTypeImportCompletionItemCountDataPoint(ItemsCount);
                CompletionProvidersLogger.LogTypeImportCompletionReferenceCountDataPoint(ReferenceCount);
G
Gen Lu 已提交
373 374 375 376 377

                if (TimedOut)
                {
                    CompletionProvidersLogger.LogTypeImportCompletionTimeout();
                }
G
Gen Lu 已提交
378 379
            }
        }
G
Gen Lu 已提交
380 381
    }
}