public void CanCompleteAttribute(string testFileName, int line, int column, string expectedElementName, PaddingType expectedPadding)
        {
            Position testPosition = new Position(line, column);

            string            testXml   = LoadTestFile("TestFiles", testFileName + ".xml");
            TextPositions     positions = new TextPositions(testXml);
            XmlDocumentSyntax document  = Parser.ParseText(testXml);

            XmlLocator  locator  = new XmlLocator(document, positions);
            XmlLocation location = locator.Inspect(testPosition);

            Assert.NotNull(location);

            XSPath elementPath = XSPath.Parse(expectedElementName);

            XSElement   element;
            XSAttribute replaceAttribute;
            PaddingType needsPadding;

            Assert.True(
                location.CanCompleteAttribute(out element, out replaceAttribute, out needsPadding, onElementWithPath: elementPath),
                "CanCompleteAttribute"
                );
            Assert.NotNull(element);
            Assert.Null(replaceAttribute);
            Assert.Equal(expectedPadding, needsPadding);
        }
        /// <summary>
        ///     Get completions for item attributes.
        /// </summary>
        /// <param name="location">
        ///     The <see cref="XmlLocation"/> where completions are requested.
        /// </param>
        /// <param name="projectDocument">
        ///     The <see cref="ProjectDocument"/> that contains the <paramref name="location"/>.
        /// </param>
        /// <param name="existingMetadata">
        ///     Metadata already declared on the item.
        /// </param>
        /// <returns>
        ///     A sequence of <see cref="CompletionItem"/>s.
        /// </returns>
        IEnumerable <CompletionItem> GetAttributeCompletions(XmlLocation location, ProjectDocument projectDocument, HashSet <string> existingMetadata)
        {
            Log.Verbose("Evaluate attribute completions for {XmlLocation:l}", location);

            XSElement   itemElement;
            XSAttribute replaceAttribute;
            PaddingType needsPadding;

            if (!location.CanCompleteAttribute(out itemElement, out replaceAttribute, out needsPadding))
            {
                Log.Verbose("Not offering any attribute completions for {XmlLocation:l} (not a location where we can offer attribute completion.", location);

                yield break;
            }

            // Must be an item element.
            if (!itemElement.HasParentPath(WellKnownElementPaths.ItemGroup))
            {
                Log.Verbose("Not offering any attribute completions for {XmlLocation:l} (element is not a direct child of a 'PropertyGroup' element).", location);

                yield break;
            }

            string itemType = itemElement.Name;

            if (String.IsNullOrWhiteSpace(itemType))
            {
                Log.Verbose("Not offering any attribute completions for {XmlLocation:l} (element represents a new, unnamed, item group).",
                            location,
                            itemType
                            );

                yield break;
            }

            if (MSBuildSchemaHelp.ForItemType(itemType) == null)
            {
                Log.Verbose("Not offering any attribute completions for {XmlLocation:l} ({ItemType} is not a well-known item type).",
                            location,
                            itemType
                            );

                yield break;
            }

            Log.Verbose("Will offer attribute completions for {XmlLocation:l} (padding: {NeedsPadding})", location, needsPadding);

            // Don't offer completions for existing metadata.
            existingMetadata.UnionWith(
                GetExistingMetadataNames(itemElement)
                );

            Range replaceRange = replaceAttribute?.Range ?? location.Position.ToEmptyRange();

            foreach (string metadataName in MSBuildSchemaHelp.WellKnownItemMetadataNames(itemType))
            {
                if (existingMetadata.Contains(metadataName))
                {
                    continue;
                }

                if (MSBuildHelper.IsWellKnownItemMetadata(metadataName))
                {
                    continue;
                }

                yield return(new CompletionItem
                {
                    Label = metadataName,
                    Kind = CompletionItemKind.Field,
                    Detail = "Item Metadata",
                    Documentation = MSBuildSchemaHelp.ForItemMetadata(itemType, metadataName),
                    SortText = GetItemSortText(metadataName),
                    TextEdit = new TextEdit
                    {
                        NewText = $"{metadataName}=\"$0\"".WithPadding(needsPadding),
                        Range = replaceRange.ToLsp()
                    },
                    InsertTextFormat = InsertTextFormat.Snippet
                });
            }
        }
        /// <summary>
        ///     Provide completions for the specified location.
        /// </summary>
        /// <param name="location">
        ///     The <see cref="XmlLocation"/> where completions are requested.
        /// </param>
        /// <param name="projectDocument">
        ///     The <see cref="ProjectDocument"/> that contains the <paramref name="location"/>.
        /// </param>
        /// <param name="cancellationToken">
        ///     A <see cref="CancellationToken"/> that can be used to cancel the operation.
        /// </param>
        /// <returns>
        ///     A <see cref="Task{TResult}"/> that resolves either a <see cref="CompletionList"/>s, or <c>null</c> if no completions are provided.
        /// </returns>
        public override async Task <CompletionList> ProvideCompletions(XmlLocation location, ProjectDocument projectDocument, CancellationToken cancellationToken = default(CancellationToken))
        {
            if (location == null)
            {
                throw new ArgumentNullException(nameof(location));
            }

            if (projectDocument == null)
            {
                throw new ArgumentNullException(nameof(projectDocument));
            }

            List <CompletionItem> completions = new List <CompletionItem>();

            using (await projectDocument.Lock.ReaderLockAsync())
            {
                XSElement   element;
                XSAttribute replaceAttribute;
                PaddingType needsPadding;
                if (!location.CanCompleteAttribute(out element, out replaceAttribute, out needsPadding))
                {
                    return(null);
                }

                // Must be a valid item element.
                if (!element.IsValid || !element.HasParentPath(WellKnownElementPaths.ItemGroup))
                {
                    return(null);
                }

                Range replaceRange = replaceAttribute?.Range ?? location.Position.ToEmptyRange();

                completions.AddRange(
                    WellKnownItemAttributes.Except(
                        element.AttributeNames
                        )
                    .Select(attributeName => new CompletionItem
                {
                    Label         = attributeName,
                    Detail        = "Attribute",
                    Documentation =
                        MSBuildSchemaHelp.ForItemMetadata(itemType: element.Name, metadataName: attributeName)
                        ??
                        MSBuildSchemaHelp.ForAttribute(element.Name, attributeName),
                    Kind     = CompletionItemKind.Field,
                    SortText = GetItemSortText(attributeName),
                    TextEdit = new TextEdit
                    {
                        NewText = $"{attributeName}=\"$1\"$0".WithPadding(needsPadding),
                        Range   = replaceRange.ToLsp()
                    },
                    InsertTextFormat = InsertTextFormat.Snippet
                })
                    );
            }

            if (completions.Count == 0)
            {
                return(null);
            }

            return(new CompletionList(completions, isIncomplete: false));
        }
        /// <summary>
        ///     Provide completions for the specified location.
        /// </summary>
        /// <param name="location">
        ///     The <see cref="XmlLocation"/> where completions are requested.
        /// </param>
        /// <param name="projectDocument">
        ///     The <see cref="ProjectDocument"/> that contains the <paramref name="location"/>.
        /// </param>
        /// <param name="triggerCharacters">
        ///     The character(s), if any, that triggered completion.
        /// </param>
        /// <param name="cancellationToken">
        ///     A <see cref="CancellationToken"/> that can be used to cancel the operation.
        /// </param>
        /// <returns>
        ///     A <see cref="Task{TResult}"/> that resolves either a <see cref="CompletionList"/>s, or <c>null</c> if no completions are provided.
        /// </returns>
        public override async Task <CompletionList> ProvideCompletions(XmlLocation location, ProjectDocument projectDocument, string triggerCharacters, CancellationToken cancellationToken = default(CancellationToken))
        {
            if (location == null)
            {
                throw new ArgumentNullException(nameof(location));
            }

            if (projectDocument == null)
            {
                throw new ArgumentNullException(nameof(projectDocument));
            }

            if (!projectDocument.Workspace.Configuration.Language.CompletionsFromProject.Contains(CompletionSource.Task))
            {
                Log.Verbose("Not offering task attribute completions for {XmlLocation:l} (task completions not enabled in extension settings).", location);

                return(null);
            }

            if (!projectDocument.HasMSBuildProject)
            {
                Log.Verbose("Not offering task attribute completions for {XmlLocation:l} (underlying MSBuild project is not loaded).", location);

                return(null);
            }

            List <CompletionItem> completions = new List <CompletionItem>();

            Log.Verbose("Evaluate completions for {XmlLocation:l}", location);

            using (await projectDocument.Lock.ReaderLockAsync())
            {
                XSElement   taskElement;
                XSAttribute replaceAttribute;
                PaddingType needsPadding;
                if (!location.CanCompleteAttribute(out taskElement, out replaceAttribute, out needsPadding))
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (not a location an attribute can be created or replaced by completion).", location);

                    return(null);
                }

                if (taskElement.ParentElement?.Name != "Target")
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (attribute is not on an element that's a direct child of a 'Target' element).", location);

                    return(null);
                }

                Dictionary <string, MSBuildTaskMetadata> projectTasks = await GetProjectTasks(projectDocument);

                MSBuildTaskMetadata taskMetadata;
                if (!projectTasks.TryGetValue(taskElement.Name, out taskMetadata))
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (no metadata available for task {TaskName}).", location, taskElement.Name);

                    return(null);
                }

                Range replaceRange = replaceAttribute?.Range ?? location.Position.ToEmptyRange();
                if (replaceAttribute != null)
                {
                    Log.Verbose("Offering completions to replace attribute {AttributeName} @ {ReplaceRange:l}",
                                replaceAttribute.Name,
                                replaceRange
                                );
                }
                else
                {
                    Log.Verbose("Offering completions to create attribute @ {ReplaceRange:l}",
                                replaceRange
                                );
                }

                HashSet <string> existingAttributeNames = new HashSet <string>(
                    taskElement.AttributeNames
                    );
                if (replaceAttribute != null)
                {
                    existingAttributeNames.Remove(replaceAttribute.Name);
                }

                completions.AddRange(
                    GetCompletionItems(projectDocument, taskMetadata, existingAttributeNames, replaceRange, needsPadding)
                    );
            }

            Log.Verbose("Offering {CompletionCount} completion(s) for {XmlLocation:l}", completions.Count, location);

            if (completions.Count == 0)
            {
                return(null);
            }

            return(new CompletionList(completions,
                                      isIncomplete: false // Consider this list to be exhaustive
                                      ));
        }