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

            XSElement replacingElement;

            Assert.True(location.CanCompleteElement(out replacingElement), "CanCompleteElement");
            Assert.NotNull(replacingElement);
        }
        public void CanCompleteElementInParentWithRelativePath(string testFileName, int line, int column, string expectedParent)
        {
            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 expectedParentPath = XSPath.Parse(expectedParent);

            XSElement replaceElement;

            Assert.True(
                location.CanCompleteElement(out replaceElement, parentPath: expectedParentPath),
                "CanCompleteElement"
                );
            Assert.NotNull(replaceElement);
            Assert.Equal(expectedParent, replaceElement.ParentElement?.Name);
        }
        /// <summary>
        ///     Get completions for item elements.
        /// </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> GetElementCompletions(XmlLocation location, ProjectDocument projectDocument, HashSet <string> existingMetadata)
        {
            Log.Verbose("Evaluate element completions for {XmlLocation:l}", location);

            XSElement replaceElement;

            if (!location.CanCompleteElement(out replaceElement, parentPath: WellKnownElementPaths.Item))
            {
                Log.Verbose("Not offering any element completions for {XmlLocation:l} (not a location where an item metadata element can be created or replaced by completion).", location);

                yield break;
            }

            Range  replaceRange;
            string itemType;

            if (replaceElement != null)
            {
                replaceRange = replaceElement.Range;
                itemType     = replaceElement.ParentElement?.Name;
            }
            else
            {
                replaceRange = location.Position.ToEmptyRange();
                itemType     = location.Node.Path.Parent.Name;
            }

            // These items are handled by PackageReferenceCompletion.
            if (itemType == "PackageReference" || itemType == "DotNetCliToolReference")
            {
                Log.Verbose("Not offering any element completions for {XmlLocation:l} ({ItemType} items are handled by another provider).",
                            location,
                            itemType
                            );

                yield break;
            }

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

                yield break;
            }

            if (replaceElement != null)
            {
                // Don't offer completions for existing metadata.
                existingMetadata.UnionWith(
                    GetExistingMetadataNames(replaceElement)
                    );

                Log.Verbose("Will offer completions to replace item metadata element spanning {Range:l}", replaceRange);
            }
            else
            {
                Log.Verbose("Will offer completions to create item metadata element at {Position:l}", location.Position);
            }

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

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

                string completionLabel = $"<{metadataName}>";

                yield return(new CompletionItem
                {
                    Label = completionLabel,
                    Kind = CompletionItemKind.Field,
                    Detail = $"Item Metadata ({itemType})",
                    Documentation = MSBuildSchemaHelp.ForItemMetadata(itemType, metadataName),
                    SortText = GetItemSortText(completionLabel),
                    TextEdit = new TextEdit
                    {
                        NewText = $"<{metadataName}>$0</{metadataName}>",
                        Range = replaceRange.ToLsp()
                    },
                    InsertTextFormat = InsertTextFormat.Snippet
                });
            }
        }
Example #4
0
        /// <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())
            {
                XSElement replaceElement;
                if (!location.CanCompleteElement(out replaceElement, parentPath: WellKnownElementPaths.PropertyGroup))
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (not a direct child of a 'PropertyGroup' element).", location);

                    return(null);
                }

                Range replaceRange;
                if (replaceElement != null)
                {
                    replaceRange = replaceElement.Range;

                    Log.Verbose("Offering completions to replace element {ElementName} @ {ReplaceRange:l}",
                                replaceElement.Name,
                                replaceRange
                                );
                }
                else
                {
                    replaceRange = location.Position.ToEmptyRange();

                    Log.Verbose("Offering completions to create element @ {ReplaceRange:l}",
                                replaceRange
                                );
                }

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

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

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

            Log.Verbose("Evaluate completions for {XmlLocation:l} (trigger characters = {TriggerCharacters})", location, triggerCharacters);

            using (await projectDocument.Lock.ReaderLockAsync())
            {
                XSElement replaceElement;
                if (!location.CanCompleteElement(out replaceElement, parentPath: WellKnownElementPaths.Project))
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (not a direct child of the 'Project' element).", location);

                    return(null);
                }

                Range targetRange;

                if (replaceElement != null)
                {
                    targetRange = replaceElement.Range;

                    Log.Verbose("Offering completions to replace element {ElementName} @ {ReplaceRange:l}",
                                replaceElement.Name,
                                targetRange
                                );
                }
                else
                {
                    targetRange = location.Position.ToEmptyRange();

                    Log.Verbose("Offering completions to insert element @ {InsertPosition:l}",
                                location.Position
                                );
                }

                // Replace any characters that were typed to trigger the completion.
                HandleTriggerCharacters(triggerCharacters, projectDocument, ref targetRange);

                completions.AddRange(
                    GetCompletionItems(targetRange)
                    );
            }

            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
                                      ));
        }
        /// <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 element completions for {XmlLocation:l} (task completions not enabled in extension settings).", location);

                return(null);
            }

            if (!projectDocument.HasMSBuildProject)
            {
                Log.Verbose("Not offering task element 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 replaceElement;
                if (!location.CanCompleteElement(out replaceElement, parentPath: WellKnownElementPaths.Target))
                {
                    Log.Verbose("Not offering any completions for {XmlLocation:l} (does not represent the direct child of a 'Target' element).", location);

                    return(null);
                }

                Range targetRange = replaceElement?.Range ?? location.Position.ToEmptyRange();

                // Replace any characters that were typed to trigger the completion.
                HandleTriggerCharacters(triggerCharacters, projectDocument, ref targetRange);

                if (replaceElement != null)
                {
                    Log.Verbose("Offering completions to replace element {ElementName} @ {ReplaceRange:l}",
                                replaceElement.Name,
                                targetRange
                                );
                }
                else
                {
                    Log.Verbose("Offering completions to create task element @ {ReplaceRange:l}",
                                targetRange
                                );
                }

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

                completions.AddRange(
                    GetCompletionItems(projectDocument, projectTasks, targetRange)
                    );
            }

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