public void CanCompleteAttribute(string testFileName, int line, int column, string expectedElementName, PaddingType expectedPadding) { Position testPosition = new Position(line, column); string testXml = LoadTestFile("TestFiles", testFileName + ".xml"); TextPositions positions = new TextPositions(testXml); XmlDocumentSyntax document = Parser.ParseText(testXml); XmlLocator locator = new XmlLocator(document, positions); XmlLocation location = locator.Inspect(testPosition); Assert.NotNull(location); XSPath elementPath = XSPath.Parse(expectedElementName); XSElement element; XSAttribute replaceAttribute; PaddingType needsPadding; Assert.True( location.CanCompleteAttribute(out element, out replaceAttribute, out needsPadding, onElementWithPath: elementPath), "CanCompleteAttribute" ); Assert.NotNull(element); Assert.Null(replaceAttribute); Assert.Equal(expectedPadding, needsPadding); }
/// <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> /// Provide completions for the specified location. /// </summary> /// <param name="location"> /// The <see cref="XmlLocation"/> where completions are requested. /// </param> /// <param name="projectDocument"> /// The <see cref="ProjectDocument"/> that contains the <paramref name="location"/>. /// </param> /// <param name="triggerCharacters"> /// The character(s), if any, that triggered completion. /// </param> /// <param name="cancellationToken"> /// A <see cref="CancellationToken"/> that can be used to cancel the operation. /// </param> /// <returns> /// A <see cref="Task{TResult}"/> that resolves either a <see cref="CompletionList"/>s, or <c>null</c> if no completions are provided. /// </returns> public override async Task <CompletionList> ProvideCompletions(XmlLocation location, ProjectDocument projectDocument, string triggerCharacters, CancellationToken cancellationToken = default(CancellationToken)) { if (location == null) { throw new ArgumentNullException(nameof(location)); } if (projectDocument == null) { throw new ArgumentNullException(nameof(projectDocument)); } if (!projectDocument.Workspace.Configuration.Language.CompletionsFromProject.Contains(CompletionSource.Task)) { Log.Verbose("Not offering task attribute completions for {XmlLocation:l} (task completions not enabled in extension settings).", location); return(null); } if (!projectDocument.HasMSBuildProject) { Log.Verbose("Not offering task attribute completions for {XmlLocation:l} (underlying MSBuild project is not loaded).", location); return(null); } List <CompletionItem> completions = new List <CompletionItem>(); Log.Verbose("Evaluate completions for {XmlLocation:l}", location); using (await projectDocument.Lock.ReaderLockAsync()) { XSElement taskElement; XSAttribute replaceAttribute; PaddingType needsPadding; if (!location.CanCompleteAttribute(out taskElement, out replaceAttribute, out needsPadding)) { Log.Verbose("Not offering any completions for {XmlLocation:l} (not a location an attribute can be created or replaced by completion).", location); return(null); } if (taskElement.ParentElement?.Name != "Target") { Log.Verbose("Not offering any completions for {XmlLocation:l} (attribute is not on an element that's a direct child of a 'Target' element).", location); return(null); } Dictionary <string, MSBuildTaskMetadata> projectTasks = await GetProjectTasks(projectDocument); MSBuildTaskMetadata taskMetadata; if (!projectTasks.TryGetValue(taskElement.Name, out taskMetadata)) { Log.Verbose("Not offering any completions for {XmlLocation:l} (no metadata available for task {TaskName}).", location, taskElement.Name); return(null); } Range replaceRange = replaceAttribute?.Range ?? location.Position.ToEmptyRange(); if (replaceAttribute != null) { Log.Verbose("Offering completions to replace attribute {AttributeName} @ {ReplaceRange:l}", replaceAttribute.Name, replaceRange ); } else { Log.Verbose("Offering completions to create attribute @ {ReplaceRange:l}", replaceRange ); } HashSet <string> existingAttributeNames = new HashSet <string>( taskElement.AttributeNames ); if (replaceAttribute != null) { existingAttributeNames.Remove(replaceAttribute.Name); } completions.AddRange( GetCompletionItems(projectDocument, taskMetadata, existingAttributeNames, replaceRange, needsPadding) ); } Log.Verbose("Offering {CompletionCount} completion(s) for {XmlLocation:l}", completions.Count, location); if (completions.Count == 0) { return(null); } return(new CompletionList(completions, isIncomplete: false // Consider this list to be exhaustive )); }