/// <summary>
        ///     Get hover content for an MSBuild condition.
        /// </summary>
        /// <param name="elementName">
        ///     The name of the element that contains the Condition attribute.
        /// </param>
        /// <param name="condition">
        ///     The raw (unevaluated) condition.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer Condition(string elementName, string condition)
        {
            if (String.IsNullOrWhiteSpace(elementName))
            {
                throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'elementName'.", nameof(elementName));
            }

            if (String.IsNullOrWhiteSpace(condition))
            {
                return(null);
            }

            string evaluatedCondition = _projectDocument.MSBuildProject.ExpandString(condition);

            List <MarkedString> content = new List <MarkedString>
            {
                "Condition",
                $"Evaluated: `{evaluatedCondition}`"
            };

            string helpLink = MSBuildSchemaHelp.HelpLinkForElement("*.Condition");

            if (!String.IsNullOrWhiteSpace(helpLink))
            {
                content.Add(
                    $"[Help]({helpLink})"
                    );
            }

            return(new MarkedStringContainer(content));
        }
        /// <summary>
        ///     Create a <see cref="CompletionItem"/> for the specified MSBuild task element.
        /// </summary>
        /// <param name="taskName">
        ///     The MSBuild task name.
        /// </param>
        /// <param name="taskMetadata">
        ///     The MSBuild task's metadata.
        /// </param>
        /// <param name="replaceRange">
        ///     The range of text that will be replaced by the completion.
        /// </param>
        /// <returns>
        ///     The <see cref="CompletionItem"/>.
        /// </returns>
        CompletionItem TaskElementCompletionItem(string taskName, MSBuildTaskMetadata taskMetadata, LspModels.Range replaceRange)
        {
            MSBuildTaskParameterMetadata[] requiredParameters = taskMetadata.Parameters.Where(parameter => parameter.IsRequired).ToArray();
            string requiredAttributes = String.Join(" ", requiredParameters.Select(
                                                        (parameter, index) => $"{parameter.Name}=\"${index + 1}\""
                                                        ));
            string attributePadding = (requiredAttributes.Length > 0) ? " " : String.Empty;

            string restOfElement = " />$0";

            if (taskMetadata.Parameters.Any(parameter => parameter.IsOutput))
            {
                // Create Outputs sub-element if there are any output parameters.
                restOfElement = $">\n\t${requiredParameters.Length + 1}\n</{taskName}>$0";
            }

            return(new CompletionItem
            {
                Label = $"<{taskName}>",
                Detail = "Task",
                Documentation = MSBuildSchemaHelp.ForTask(taskName),
                SortText = $"{Priority:0000}<{taskName}>",
                TextEdit = new TextEdit
                {
                    NewText = $"<{taskName}{attributePadding}{requiredAttributes}{restOfElement}",
                    Range = replaceRange
                },
                InsertTextFormat = InsertTextFormat.Snippet
            });
        }
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildImport"/>.
        /// </summary>
        /// <param name="import">
        ///     The <see cref="MSBuildImport"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer Import(MSBuildImport import)
        {
            if (import == null)
            {
                throw new ArgumentNullException(nameof(import));
            }

            List <MarkedString> content = new List <MarkedString>
            {
                $"Import: `{import.Name}`"
            };

            StringBuilder imports = new StringBuilder("Imports:");

            imports.AppendLine();
            foreach (string projectFile in import.ImportedProjectFiles)
            {
                imports.AppendLine($"* [{Path.GetFileName(projectFile)}]({VSCodeDocumentUri.FromFileSystemPath(projectFile)})");
            }

            content.Add(
                imports.ToString()
                );

            string helpLink = MSBuildSchemaHelp.HelpLinkForElement(import.Element.Name);

            if (!String.IsNullOrWhiteSpace(helpLink))
            {
                content.Add(
                    $"[Help]({helpLink})"
                    );
            }

            return(new MarkedStringContainer(content));
        }
        /// <summary>
        ///     Get task attribute completions.
        /// </summary>
        /// <param name="projectDocument">
        ///     The <see cref="ProjectDocument"/> for which completions will be offered.
        /// </param>
        /// <param name="taskMetadata">
        ///     Metadata for the task whose parameters are being offered as completions.
        /// </param>
        /// <param name="existingAttributeNames">
        ///     Existing parameter names (if any) declared on the element.
        /// </param>
        /// <param name="replaceRange">
        ///     The range of text to be replaced by the completions.
        /// </param>
        /// <param name="needsPadding">
        ///     The type of padding (if any) required.
        /// </param>
        /// <returns>
        ///     A sequence of <see cref="CompletionItem"/>s.
        /// </returns>
        public IEnumerable <CompletionItem> GetCompletionItems(ProjectDocument projectDocument, MSBuildTaskMetadata taskMetadata, HashSet <string> existingAttributeNames, Range replaceRange, PaddingType needsPadding)
        {
            if (replaceRange == null)
            {
                throw new ArgumentNullException(nameof(replaceRange));
            }

            LspModels.Range replaceRangeLsp = replaceRange.ToLsp();

            foreach (MSBuildTaskParameterMetadata taskParameter in taskMetadata.Parameters.OrderBy(parameter => parameter.Name))
            {
                if (existingAttributeNames.Contains(taskParameter.Name))
                {
                    continue;
                }

                if (taskParameter.IsOutput)
                {
                    continue;
                }

                string parameterDocumentation = MSBuildSchemaHelp.ForTaskParameter(taskMetadata.Name, taskParameter.Name);

                yield return(TaskParameterCompletionItem(taskMetadata.Name, taskParameter, parameterDocumentation, replaceRangeLsp, needsPadding));
            }
        }
        /// <summary>
        ///     Get hover content for an XML element that does not directly correspond to an <see cref="MSBuildObject"/>.
        /// </summary>
        /// <param name="element">
        ///     The <see cref="XSElement"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer Element(XSElement element)
        {
            if (element == null)
            {
                throw new ArgumentNullException(nameof(element));
            }

            string elementDescription = MSBuildSchemaHelp.ForElement(element.Name);

            if (String.IsNullOrWhiteSpace(elementDescription))
            {
                return(null);
            }

            List <MarkedString> content = new List <MarkedString>
            {
                elementDescription
            };

            string helpLink = MSBuildSchemaHelp.HelpLinkForElement(element.Name);

            if (!String.IsNullOrWhiteSpace(helpLink))
            {
                content.Add(
                    $"[Help]({helpLink})"
                    );
            }

            return(new MarkedStringContainer(content));
        }
Example #6
0
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildUnusedProperty"/>.
        /// </summary>
        /// <param name="unusedProperty">
        ///     The <see cref="MSBuildUnusedProperty"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer UnusedProperty(MSBuildUnusedProperty unusedProperty)
        {
            if (unusedProperty == null)
            {
                throw new ArgumentNullException(nameof(unusedProperty));
            }

            List <MarkedString> content = new List <MarkedString>
            {
                $"Unused Property: `{unusedProperty.Name}` (condition is false)"
            };

            string propertyHelp = MSBuildSchemaHelp.ForProperty(unusedProperty.Name);

            if (propertyHelp != null)
            {
                content.Add(propertyHelp);
            }

            content.Add(
                $"Value would have been: `{unusedProperty.Value}`"
                );

            return(new MarkedStringContainer(content));
        }
Example #7
0
        /// <summary>
        ///     Get item element completions.
        /// </summary>
        /// <param name="projectDocument">
        ///     The <see cref="ProjectDocument"/> for which completions will be offered.
        /// </param>
        /// <param name="replaceRange">
        ///     The range of text to be replaced by the completions.
        /// </param>
        /// <returns>
        ///     A sequence of <see cref="CompletionItem"/>s.
        /// </returns>
        public IEnumerable <CompletionItem> GetCompletionItems(ProjectDocument projectDocument, Range replaceRange)
        {
            if (replaceRange == null)
            {
                throw new ArgumentNullException(nameof(replaceRange));
            }

            LspModels.Range replaceRangeLsp = replaceRange.ToLsp();

            HashSet <string> offeredItemNames = new HashSet <string>
            {
                "PackageReference",
                "DotNetCliToolReference"
            };

            // Well-known (but standard-format) properties.

            foreach (string wellKnownItemName in MSBuildSchemaHelp.WellKnownItemTypes)
            {
                if (!offeredItemNames.Add(wellKnownItemName))
                {
                    continue;
                }

                yield return(ItemCompletion(wellKnownItemName, replaceRangeLsp,
                                            description: MSBuildSchemaHelp.ForItemType(wellKnownItemName)
                                            ));
            }

            if (!projectDocument.HasMSBuildProject)
            {
                yield break; // Without a valid MSBuild project (even a cached one will do), we can't inspect existing MSBuild properties.
            }
            if (!projectDocument.Workspace.Configuration.Language.CompletionsFromProject.Contains(CompletionSource.ItemType))
            {
                yield break;
            }

            int otherItemPriority = Priority + 10;

            string[] otherItemNames =
                projectDocument.MSBuildProject.Properties
                .Select(item => item.Name)
                .Where(itemName => !itemName.StartsWith("_"))     // Ignore private item types.
                .ToArray();
            foreach (string itemName in otherItemNames)
            {
                if (!offeredItemNames.Add(itemName))
                {
                    continue;
                }

                yield return(ItemCompletion(itemName, replaceRangeLsp, otherItemPriority,
                                            description: $"I don't know anything about the '{itemName}' item type, but it's defined in this project (or a project that it imports); you can override its value by specifying it here."
                                            ));
            }
        }
Example #8
0
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildProperty"/>.
        /// </summary>
        /// <param name="property">
        ///     The <see cref="MSBuildProperty"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer Property(MSBuildProperty property)
        {
            if (property == null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            List <MarkedString> content = new List <MarkedString>
            {
                $"Property: `{property.Name}`"
            };

            string propertyHelp = MSBuildSchemaHelp.ForProperty(property.Name);

            if (propertyHelp != null)
            {
                content.Add(propertyHelp);
            }

            if (property.IsOverridden)
            {
                Position overridingDeclarationPosition = property.DeclaringXml.Location.ToNative();

                StringBuilder overrideDescription = new StringBuilder();
                string        declarationFile     = property.DeclaringXml.Location.File;
                if (declarationFile != property.Property.Xml.Location.File)
                {
                    Uri declarationDocumentUri = VSCodeDocumentUri.FromFileSystemPath(declarationFile);
                    overrideDescription.AppendLine(
                        $"Value overridden at {overridingDeclarationPosition} in [{Path.GetFileName(declarationFile)}]({declarationDocumentUri})."
                        );
                }
                else
                {
                    overrideDescription.AppendLine($"Value overridden at {overridingDeclarationPosition} in this file.");
                }

                overrideDescription.AppendLine();
                overrideDescription.AppendLine();
                overrideDescription.AppendLine(
                    $"Unused value: `{property.DeclaringXml.Value}`"
                    );
                overrideDescription.AppendLine();
                overrideDescription.AppendLine(
                    $"Actual value: `{property.Value}`"
                    );

                content.Add(overrideDescription.ToString());
            }
            else
            {
                content.Add($"Value: `{property.Value}`");
            }

            return(new MarkedStringContainer(content));
        }
Example #9
0
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildUnusedItemGroup"/>.
        /// </summary>
        /// <param name="unusedItemGroup">
        ///     The <see cref="MSBuildUnusedItemGroup"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer UnusedItemGroup(MSBuildUnusedItemGroup unusedItemGroup)
        {
            if (unusedItemGroup == null)
            {
                throw new ArgumentNullException(nameof(unusedItemGroup));
            }

            string condition          = unusedItemGroup.Condition;
            string evaluatedCondition = _projectDocument.MSBuildProject.ExpandString(condition);

            List <MarkedString> content = new List <MarkedString>
            {
                $"Unused Item Group: `{unusedItemGroup.OriginatingElement.ItemType}` (condition is false)"
            };

            string itemTypeHelp = MSBuildSchemaHelp.ForItemType(unusedItemGroup.Name);

            if (itemTypeHelp != null)
            {
                content.Add(itemTypeHelp);
            }

            StringBuilder descriptionContent = new StringBuilder();

            string[] includes = unusedItemGroup.Includes.ToArray();
            descriptionContent.AppendLine(
                $"Include: `{unusedItemGroup.OriginatingElement.Include}`  "
                );
            descriptionContent.AppendLine();
            descriptionContent.Append(
                $"Would have evaluated to {unusedItemGroup.Items.Count} item"
                );
            if (!unusedItemGroup.HasSingleItem)
            {
                descriptionContent.Append("s");
            }
            descriptionContent.AppendLine(":");

            foreach (string include in includes.Take(5))
            {
                // TODO: Consider making hyperlinks for includes that map to files which exist.
                descriptionContent.AppendLine(
                    $"* `{include}`"
                    );
            }
            if (includes.Length > 5)
            {
                descriptionContent.AppendLine("* ...");
            }

            content.Add(
                descriptionContent.ToString()
                );

            return(new MarkedStringContainer(content));
        }
        /// <summary>
        ///     Get item group element completions.
        /// </summary>
        /// <param name="projectDocument">
        ///     The <see cref="ProjectDocument"/> for which completions will be offered.
        /// </param>
        /// <param name="replaceRange">
        ///     The range of text to be replaced by the completions.
        /// </param>
        /// <returns>
        ///     A sequence of <see cref="CompletionItem"/>s.
        /// </returns>
        public IEnumerable <CompletionItem> GetCompletionItems(ProjectDocument projectDocument, Range replaceRange)
        {
            if (replaceRange == null)
            {
                throw new ArgumentNullException(nameof(replaceRange));
            }

            LspModels.Range replaceRangeLsp = replaceRange.ToLsp();

            HashSet <string> offeredItemGroupNames = new HashSet <string>
            {
                "*" // Skip virtual item type representing well-known metadata.
            };

            // Well-known item types.
            foreach (string itemType in MSBuildSchemaHelp.WellKnownItemTypes)
            {
                if (!offeredItemGroupNames.Add(itemType))
                {
                    continue;
                }

                yield return(ItemGroupCompletionItem(itemType, replaceRangeLsp,
                                                     description: MSBuildSchemaHelp.ForItemType(itemType)
                                                     ));
            }

            if (!projectDocument.HasMSBuildProject)
            {
                yield break; // Without a valid MSBuild project (even a cached one will do), we can't inspect existing MSBuild properties.
            }
            if (!projectDocument.Workspace.Configuration.Language.CompletionsFromProject.Contains(CompletionSource.ItemType))
            {
                yield break;
            }

            int otherItemGroupPriority = Priority + 10;

            string[] otherItemTypes =
                projectDocument.MSBuildProject.ItemTypes
                .Where(itemType => !itemType.StartsWith("_"))     // Ignore private item groups.
                .ToArray();
            foreach (string otherItemType in otherItemTypes)
            {
                if (!offeredItemGroupNames.Add(otherItemType))
                {
                    continue;
                }

                yield return(ItemGroupCompletionItem(otherItemType, replaceRangeLsp, otherItemGroupPriority,
                                                     description: "Item group defined in this project (or a project it imports)."
                                                     ));
            }
        }
        /// <summary>
        ///     Get property element completions.
        /// </summary>
        /// <param name="projectDocument">
        ///     The <see cref="ProjectDocument"/> for which completions will be offered.
        /// </param>
        /// <param name="replaceRange">
        ///     The range of text to be replaced by the completions.
        /// </param>
        /// <returns>
        ///     A sequence of <see cref="CompletionItem"/>s.
        /// </returns>
        public IEnumerable <CompletionItem> GetCompletionItems(ProjectDocument projectDocument, Range replaceRange)
        {
            if (replaceRange == null)
            {
                throw new ArgumentNullException(nameof(replaceRange));
            }

            LspModels.Range replaceRangeLsp = replaceRange.ToLsp();

            HashSet <string> offeredPropertyNames = new HashSet <string>();

            // Well-known properties.
            foreach (string wellKnownPropertyName in MSBuildSchemaHelp.WellKnownPropertyNames)
            {
                if (!offeredPropertyNames.Add(wellKnownPropertyName))
                {
                    continue;
                }

                yield return(PropertyCompletionItem(wellKnownPropertyName, replaceRangeLsp,
                                                    description: MSBuildSchemaHelp.ForProperty(wellKnownPropertyName)
                                                    ));
            }

            if (!projectDocument.HasMSBuildProject)
            {
                yield break; // Without a valid MSBuild project (even a cached one will do), we can't inspect existing MSBuild properties.
            }
            if (!projectDocument.Workspace.Configuration.Language.CompletionsFromProject.Contains(CompletionSource.Property))
            {
                yield break;
            }

            int otherPropertyPriority = Priority + 10;

            string[] otherPropertyNames =
                projectDocument.MSBuildProject.Properties
                .Select(property => property.Name)
                .Where(propertyName => !propertyName.StartsWith("_"))     // Ignore private properties.
                .ToArray();
            foreach (string propertyName in otherPropertyNames)
            {
                if (!offeredPropertyNames.Add(propertyName))
                {
                    continue;
                }

                yield return(PropertyCompletionItem(propertyName, replaceRangeLsp, otherPropertyPriority,
                                                    description: "Property defined in this project (or a project it imports)."
                                                    ));
            }
        }
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildImport"/>.
        /// </summary>
        /// <param name="unresolvedImport">
        ///     The <see cref="MSBuildImport"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer UnresolvedImport(MSBuildUnresolvedImport unresolvedImport)
        {
            if (unresolvedImport == null)
            {
                throw new ArgumentNullException(nameof(unresolvedImport));
            }

            string condition          = unresolvedImport.Condition;
            string evaluatedCondition = _projectDocument.MSBuildProject.ExpandString(condition);

            string project          = unresolvedImport.Project;
            string evaluatedProject = _projectDocument.MSBuildProject.ExpandString(project);

            StringBuilder descriptionContent = new StringBuilder();

            descriptionContent.AppendLine(
                $"Project: `{project}`"
                );
            descriptionContent.AppendLine();
            descriptionContent.AppendLine(
                $"Evaluated Project: `{evaluatedProject}`"
                );
            descriptionContent.AppendLine();
            descriptionContent.AppendLine(
                $"Condition: `{condition}`"
                );
            descriptionContent.AppendLine();
            descriptionContent.AppendLine(
                $"Evaluated Condition: `{evaluatedCondition}`"
                );

            List <MarkedString> content = new List <MarkedString>
            {
                "Unresolved Import (condition is false)",
                descriptionContent.ToString()
            };

            string helpLink = MSBuildSchemaHelp.HelpLinkForElement(unresolvedImport.Element.Name);

            if (!String.IsNullOrWhiteSpace(helpLink))
            {
                content.Add(
                    $"[Help]({helpLink})"
                    );
            }

            return(new MarkedStringContainer(content));
        }
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildUnusedProperty"/>.
        /// </summary>
        /// <param name="unusedProperty">
        ///     The <see cref="MSBuildUnusedProperty"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer UnusedProperty(MSBuildUnusedProperty unusedProperty)
        {
            if (unusedProperty == null)
            {
                throw new ArgumentNullException(nameof(unusedProperty));
            }

            List <MarkedString> content = new List <MarkedString>();

            if (unusedProperty.Element.HasParentPath(WellKnownElementPaths.DynamicPropertyGroup))
            {
                content.Add(
                    $"Dynamic Property: `{unusedProperty.Name}`"
                    );
                content.Add(
                    "(properties declared in targets are only evaluated when building the project)"
                    );
            }
            else
            {
                content.Add(
                    $"Unused Property: `{unusedProperty.Name}` (condition is false)"
                    );
            }

            string propertyHelp = MSBuildSchemaHelp.ForProperty(unusedProperty.Name);

            if (propertyHelp != null)
            {
                content.Add(propertyHelp);
            }

            content.Add(
                $"Value would have been: `{unusedProperty.Value}`"
                );

            string helpLink = MSBuildSchemaHelp.HelpLinkForProperty(unusedProperty.Name);

            if (!String.IsNullOrWhiteSpace(helpLink))
            {
                content.Add(
                    $"[Help]({helpLink})"
                    );
            }

            return(new MarkedStringContainer(content));
        }
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildTarget"/>.
        /// </summary>
        /// <param name="target">
        ///     The <see cref="MSBuildTarget"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer Target(MSBuildTarget target)
        {
            if (target == null)
            {
                throw new ArgumentNullException(nameof(target));
            }

            List <MarkedString> content = new List <MarkedString>
            {
                $"Target: `{target.Name}`"
            };

            string helpLink = MSBuildSchemaHelp.HelpLinkForElement(target.Element.Name);

            if (!String.IsNullOrWhiteSpace(helpLink))
            {
                content.Add(
                    $"[Help]({helpLink})"
                    );
            }

            return(new MarkedStringContainer(content));
        }
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildItemGroup"/>.
        /// </summary>
        /// <param name="itemGroup">
        ///     The <see cref="MSBuildItemGroup"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer ItemGroup(MSBuildItemGroup itemGroup)
        {
            if (itemGroup == null)
            {
                throw new ArgumentNullException(nameof(itemGroup));
            }

            if (itemGroup.Name == "PackageReference")
            {
                string packageVersion = itemGroup.GetFirstMetadataValue("Version");

                // TODO: Verify package is from NuGet (later, we can also recognise MyGet)

                return(new MarkedStringContainer(
                           $"NuGet Package: [{itemGroup.FirstInclude}](https://nuget.org/packages/{itemGroup.FirstInclude}/{packageVersion})",
                           $"Version: {packageVersion}"
                           ));
            }

            List <MarkedString> content = new List <MarkedString>
            {
                $"Item Group: `{itemGroup.OriginatingElement.ItemType}`"
            };

            string itemTypeHelp = MSBuildSchemaHelp.ForItemType(itemGroup.Name);

            if (itemTypeHelp != null)
            {
                content.Add(itemTypeHelp);
            }

            string[]      includes           = itemGroup.Includes.ToArray();
            StringBuilder itemIncludeContent = new StringBuilder();

            itemIncludeContent.AppendLine(
                $"Include: `{itemGroup.OriginatingElement.Include}`  "
                );
            itemIncludeContent.AppendLine();
            itemIncludeContent.Append(
                $"Evaluates to {itemGroup.Items.Count} item"
                );
            if (!itemGroup.HasSingleItem)
            {
                itemIncludeContent.Append("s");
            }
            itemIncludeContent.AppendLine(".");

            foreach (string include in includes.Take(5))
            {
                // TODO: Consider making hyperlinks for includes that map to files which exist.
                itemIncludeContent.AppendLine(
                    $"* `{include}`"
                    );
            }
            if (includes.Length > 5)
            {
                itemIncludeContent.AppendLine("* ...");
            }

            content.Add(
                itemIncludeContent.ToString()
                );

            string helpLink = MSBuildSchemaHelp.HelpLinkForItem(itemGroup.Name);

            if (!String.IsNullOrWhiteSpace(helpLink))
            {
                content.Add(
                    $"[Help]({helpLink})"
                    );
            }

            return(new MarkedStringContainer(content));
        }
        /// <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
                });
            }
        }
        /// <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>
        ///     Get top-level element completions.
        /// </summary>
        /// <param name="replaceRange">
        ///     The range of text to be replaced by the completions.
        /// </param>
        /// <returns>
        ///     A sequence of <see cref="CompletionItem"/>s.
        /// </returns>
        public IEnumerable <CompletionItem> GetCompletionItems(Range replaceRange)
        {
            if (replaceRange == null)
            {
                throw new ArgumentNullException(nameof(replaceRange));
            }

            LspModels.Range completionRange = replaceRange.ToLsp();

            // <PropertyGroup>
            //     $0
            // </PropertyGroup>
            yield return(new CompletionItem
            {
                Label = "<PropertyGroup>",
                Detail = "Element",
                Documentation = MSBuildSchemaHelp.ForElement("PropertyGroup"),
                SortText = Priority + "<PropertyGroup>",
                TextEdit = new TextEdit
                {
                    NewText = "<PropertyGroup>\n\t$0\n</PropertyGroup>",
                    Range = completionRange
                },
                InsertTextFormat = InsertTextFormat.Snippet
            });

            // <ItemGroup>
            //     $0
            // </ItemGroup>
            yield return(new CompletionItem
            {
                Label = "<ItemGroup>",
                Detail = "Element",
                Documentation = MSBuildSchemaHelp.ForElement("ItemGroup"),
                SortText = Priority + "<ItemGroup>",
                TextEdit = new TextEdit
                {
                    NewText = "<ItemGroup>\n\t$0\n</ItemGroup>",
                    Range = completionRange
                },
                InsertTextFormat = InsertTextFormat.Snippet
            });

            // <Target Name="TargetName">
            //     $0
            // </Target>
            yield return(new CompletionItem
            {
                Label = "<Target>",
                Detail = "Element",
                Documentation = MSBuildSchemaHelp.ForElement("Target"),
                SortText = Priority + "<Target>",
                TextEdit = new TextEdit
                {
                    NewText = "<Target Name=\"${1:TargetName}\">\n\t$0\n</Target>",
                    Range = completionRange
                },
                InsertTextFormat = InsertTextFormat.Snippet
            });

            // <Import Project="ProjectFile" />
            yield return(new CompletionItem
            {
                Label = "<Import>",
                Detail = "Element",
                Documentation = MSBuildSchemaHelp.ForElement("Import"),
                SortText = Priority + "<Import>",
                TextEdit = new TextEdit
                {
                    NewText = "<Import Project=\"${1:ProjectFile}\" />$0",
                    Range = completionRange
                },
                InsertTextFormat = InsertTextFormat.Snippet
            });
        }
        /// <summary>
        ///     Get hover content for an <see cref="MSBuildProperty"/>.
        /// </summary>
        /// <param name="property">
        ///     The <see cref="MSBuildProperty"/>.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer Property(MSBuildProperty property)
        {
            if (property == null)
            {
                throw new ArgumentNullException(nameof(property));
            }

            List <MarkedString> content = new List <MarkedString>
            {
                $"Property: `{property.Name}`"
            };

            string propertyHelp = MSBuildSchemaHelp.ForProperty(property.Name);

            if (propertyHelp != null)
            {
                content.Add(propertyHelp);
            }

            if (property.IsOverridden)
            {
                // BUG: This is the location of the *overridden* property, not the *overriding* property.
                //      We'll need to build a lookup by recursively following ProjectProperty.Predecessor.
                Position overridingDeclarationPosition = property.DeclaringXml.Location.ToNative();

                StringBuilder overrideDescription = new StringBuilder();
                string        declarationFile     = property.DeclaringXml.Location.File;
                if (declarationFile != property.Property.Xml.Location.File)
                {
                    Uri declarationDocumentUri = VSCodeDocumentUri.FromFileSystemPath(declarationFile);
                    overrideDescription.AppendLine(
                        $"Value overridden at {overridingDeclarationPosition} in [{Path.GetFileName(declarationFile)}]({declarationDocumentUri})."
                        );
                }
                else
                {
                    overrideDescription.AppendLine($"Value overridden at {overridingDeclarationPosition} in this file.");
                }

                overrideDescription.AppendLine();
                overrideDescription.AppendLine();
                overrideDescription.AppendLine(
                    $"Unused value: `{property.DeclaringXml.Value}`"
                    );
                overrideDescription.AppendLine();
                overrideDescription.AppendLine(
                    $"Actual value: `{property.Value}`"
                    );

                content.Add(overrideDescription.ToString());
            }
            else
            {
                content.Add($"Value: `{property.Value}`");
            }

            string helpLink = MSBuildSchemaHelp.HelpLinkForProperty(property.Name);

            if (!String.IsNullOrWhiteSpace(helpLink))
            {
                content.Add(
                    $"[Help]({helpLink})"
                    );
            }

            return(new MarkedStringContainer(content));
        }
        /// <summary>
        ///     Get hover content for a metadata attribute of an <see cref="MSBuildUnusedItemGroup"/>.
        /// </summary>
        /// <param name="itemGroup">
        ///     The <see cref="MSBuildUnusedItemGroup"/>.
        /// </param>
        /// <param name="metadataName">
        ///     The name of the metadata attribute.
        /// </param>
        /// <returns>
        ///     The content, or <c>null</c> if no content is provided.
        /// </returns>
        public MarkedStringContainer UnusedItemGroupMetadata(MSBuildUnusedItemGroup itemGroup, string metadataName)
        {
            if (itemGroup == null)
            {
                throw new ArgumentNullException(nameof(itemGroup));
            }

            if (String.IsNullOrWhiteSpace(metadataName))
            {
                throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'metadataName'.", nameof(metadataName));
            }

            if (itemGroup.Name == "PackageReference")
            {
                return(UnusedItemGroup(itemGroup));
            }

            if (metadataName == "Condition")
            {
                return(Condition(itemGroup.Name, itemGroup.FirstItem.Xml.Condition));
            }

            if (metadataName == "Include")
            {
                metadataName = "Identity";
            }

            List <MarkedString> content = new List <MarkedString>
            {
                $"Unused Item Metadata: `{itemGroup.Name}.{metadataName}` (item condition is false)"
            };

            string metadataHelp = MSBuildSchemaHelp.ForItemMetadata(itemGroup.Name, metadataName);

            if (metadataHelp != null)
            {
                content.Add(metadataHelp);
            }

            string[] metadataValues =
                itemGroup.GetMetadataValues(metadataName).Where(
                    value => !String.IsNullOrWhiteSpace(value)
                    )
                .Distinct()
                .ToArray();

            StringBuilder metadataContent = new StringBuilder();

            if (metadataValues.Length > 0)
            {
                metadataContent.AppendLine("Values:");
                foreach (string metadataValue in metadataValues)
                {
                    metadataContent.AppendLine(
                        $"* `{metadataValue}`"
                        );
                }
            }
            else
            {
                metadataContent.AppendLine("No values are present for this metadata.");
            }

            content.Add(
                metadataContent.ToString()
                );

            return(new MarkedStringContainer(content));
        }
Example #22
0
        /// <summary>
        ///     Get property element completions.
        /// </summary>
        /// <param name="projectDocument">
        ///     The <see cref="ProjectDocument"/> for which completions will be offered.
        /// </param>
        /// <param name="replaceRange">
        ///     The range of text to be replaced by the completions.
        /// </param>
        /// <returns>
        ///     A sequence of <see cref="CompletionItem"/>s.
        /// </returns>
        public IEnumerable <CompletionItem> GetCompletionItems(ProjectDocument projectDocument, Range replaceRange)
        {
            if (replaceRange == null)
            {
                throw new ArgumentNullException(nameof(replaceRange));
            }

            LspModels.Range replaceRangeLsp = replaceRange.ToLsp();

            HashSet <string> offeredPropertyNames = new HashSet <string>();

            // Special-case properties

            // Output type
            yield return(new CompletionItem
            {
                Label = "<OutputType>",
                Detail = "Property",
                Kind = CompletionItemKind.Property,
                Documentation = MSBuildSchemaHelp.ForProperty("OutputType"),
                SortText = Priority + "<OutputType>",
                TextEdit = new TextEdit
                {
                    NewText = "<OutputType>${1|Library,Exe|}</OutputType>",
                    Range = replaceRangeLsp
                },
                InsertTextFormat = InsertTextFormat.Snippet
            });

            offeredPropertyNames.Add("OutputType");

            // Target framework
            yield return(new CompletionItem
            {
                Label = "<TargetFramework>",
                Detail = "Property",
                Kind = CompletionItemKind.Property,
                Documentation = MSBuildSchemaHelp.ForProperty("TargetFramework"),
                SortText = Priority + "<TargetFramework>",
                TextEdit = new TextEdit
                {
                    NewText = "<TargetFramework>${1|netstandard1.0,netstandard1.1,netstandard1.2,netstandard1.3,netstandard1.4,netstandard1.5,netstandard1.6,netstandard2.0,netcoreapp1.0,netcoreapp1.1,netcoreapp2.0,net4,net451,net452,net46,net461,net462,net47|}</TargetFramework>",
                    Range = replaceRangeLsp
                },
                InsertTextFormat = InsertTextFormat.Snippet
            });

            offeredPropertyNames.Add("TargetFramework");

            // Well-known (but standard-format) properties.

            foreach (string wellKnownPropertyName in MSBuildSchemaHelp.WellKnownPropertyNames)
            {
                if (!offeredPropertyNames.Add(wellKnownPropertyName))
                {
                    continue;
                }

                var propertyDefaults = MSBuildSchemaHelp.DefaultsForProperty(wellKnownPropertyName);

                yield return(PropertyCompletionItem(wellKnownPropertyName, replaceRangeLsp,
                                                    description: MSBuildSchemaHelp.ForProperty(wellKnownPropertyName),
                                                    defaultValue: propertyDefaults.defaultValue,
                                                    defaultValues: propertyDefaults.defaultValues
                                                    ));
            }

            if (!projectDocument.HasMSBuildProject)
            {
                yield break; // Without a valid MSBuild project (even a cached one will do), we can't inspect existing MSBuild properties.
            }
            if (!projectDocument.Workspace.Configuration.Language.CompletionsFromProject.Contains(CompletionSource.Property))
            {
                yield break;
            }

            int otherPropertyPriority = Priority + 10;

            string[] otherPropertyNames =
                projectDocument.MSBuildProject.Properties
                .Select(property => property.Name)
                .Where(propertyName => !propertyName.StartsWith("_"))     // Ignore private properties.
                .ToArray();
            foreach (string propertyName in otherPropertyNames)
            {
                if (!offeredPropertyNames.Add(propertyName))
                {
                    continue;
                }

                yield return(PropertyCompletionItem(propertyName, replaceRangeLsp, otherPropertyPriority,
                                                    description: $"I don't know anything about the '{propertyName}' property, but it's defined in this project (or a project that it imports); you can override its value by specifying it here."
                                                    ));
            }
        }