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

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

            using (await projectDocument.Lock.ReaderLockAsync())
            {
                if (!projectDocument.EnableExpressions)
                {
                    return(null);
                }

                ExpressionNode expression;
                Range          expressionRange;
                if (!location.IsExpression(out expression, out expressionRange))
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (not on an expression or a location where an expression can be added).", location);

                    return(null);
                }

                if (expression.Kind != ExpressionKind.Evaluate)
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (this provider only supports MSBuild Evaluation expressions, not {ExpressionKind} expressions).", location, expression.Kind);

                    return(null);
                }

                Log.Verbose("Offering completions to replace Evaluate expression @ {ReplaceRange:l}",
                            expressionRange
                            );

                completions.AddRange(
                    GetCompletionItems(projectDocument, expressionRange)
                    );
            }

            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
                                      ));
        }
        public void IsExpression_Success(string testFileName, int line, int column, ExpressionKind expectedExpressionKind)
        {
            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);

            ExpressionNode actualExpression;
            Range          actualExpressionRange;

            Assert.True(
                location.IsExpression(out actualExpression, out actualExpressionRange),
                "IsExpression"
                );
            Assert.NotNull(actualExpression);

            Assert.Equal(expectedExpressionKind, actualExpression.Kind);
        }
        /// <summary>
        ///     Provide completions for the specified location.
        /// </summary>
        /// <param name="location">
        ///     The <see cref="XmlLocation"/> where completions are requested.
        /// </param>
        /// <param name="triggerCharacters">
        ///     The character(s), if any, that triggered completion.
        /// </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, string triggerCharacters, 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>();

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

            using (await projectDocument.Lock.ReaderLockAsync())
            {
                if (!projectDocument.EnableExpressions)
                {
                    return(null);
                }

                ExpressionNode expression;
                Range          expressionRange;
                if (!location.IsExpression(out expression, out expressionRange))
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (not on an expression or a location where an expression can be added).", location);

                    return(null);
                }

                // AF: This code is getting complicated; refactor, please.

                if (expression is Symbol)
                {
                    expression = expression.Parent; // The containing expression.
                }
                // These are handled by ItemGroupExpressionCompletion.
                if (expression is ItemGroup)
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (location represents an item group expression, not an item metadata expression).", location);

                    return(null);
                }

                bool   offerItemTypes = true; // By default, we offer item types.
                string targetItemType = null;
                if (expression is ItemMetadata metadataExpression && metadataExpression.HasItemType)
                {
                    targetItemType = metadataExpression.ItemType;
                    offerItemTypes = false; // We don't offer item types if one is already specified in the metadata expression.
                }

                bool offerUnqualifiedCompletions = false;
                if (location.IsAttribute(out XSAttribute attribute) && attribute.Element.ParentElement?.Name == "ItemGroup")
                {
                    // An attribute on an item element.
                    targetItemType = targetItemType ?? attribute.Element.Name;
                    offerUnqualifiedCompletions = true;
                }