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