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