File: DocumentationComments\XmlTagCompletionCommandHandler.cs
Web Access
Project: ..\..\..\src\EditorFeatures\CSharp\Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj (Microsoft.CodeAnalysis.CSharp.EditorFeatures)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System;
using System.ComponentModel.Composition;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.DocumentationComments;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Text.Operations;
using Microsoft.VisualStudio.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.CSharp.DocumentationComments
{
    [Export(typeof(ICommandHandler))]
    [ContentType(ContentTypeNames.CSharpContentType)]
    [Name(nameof(XmlTagCompletionCommandHandler))]
    [Order(Before = PredefinedCompletionNames.CompletionCommandHandler)]
    internal class XmlTagCompletionCommandHandler : AbstractXmlTagCompletionCommandHandler
    {
        [ImportingConstructor]
        [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
        public XmlTagCompletionCommandHandler(ITextUndoHistoryRegistry undoHistory)
            : base(undoHistory)
        {
        }
 
        protected override void TryCompleteTag(ITextView textView, ITextBuffer subjectBuffer, Document document, SnapshotPoint position, CancellationToken cancellationToken)
        {
            var tree = document.GetRequiredSyntaxTreeSynchronously(cancellationToken);
            var token = tree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDocumentationComments: true);
 
            if (token.IsKind(SyntaxKind.GreaterThanToken))
            {
                if (token.Parent is not XmlElementStartTagSyntax parentStartTag)
                {
                    return;
                }
 
                // Slightly special case: <blah><blah$$</blah>
                // If we already have a matching end tag and we're parented by 
                // an xml element with the same start tag and a missing/non-matching end tag, 
                // do completion anyway. Generally, if this is the case, we have to walk
                // up the parent elements until we find an unmatched start tag.
 
                if (parentStartTag.Name.LocalName.ValueText.Length > 0 && HasMatchingEndTag(parentStartTag))
                {
                    if (HasUnmatchedIdenticalParent(parentStartTag))
                    {
                        InsertTextAndMoveCaret(textView, subjectBuffer, position, "</" + parentStartTag.Name.LocalName.ValueText + ">", position);
                        return;
                    }
                }
 
                CheckNameAndInsertText(textView, subjectBuffer, position, parentStartTag, position.Position, "</{0}>");
            }
            else if (token.IsKind(SyntaxKind.LessThanSlashToken))
            {
                // /// <summary>
                // /// </$$
                // /// </summary>
                // We need to check for non-trivia XML text tokens after $$ that match the expected end tag text.
 
                if (token.Parent.IsKind(SyntaxKind.XmlElementEndTag) &&
                    token.Parent?.Parent is XmlElementSyntax parentElement &&
                    !HasFollowingEndTagTrivia(parentElement, token))
                {
                    CheckNameAndInsertText(textView, subjectBuffer, position, parentElement.StartTag, null, "{0}>");
                }
            }
        }
 
        private static bool HasFollowingEndTagTrivia(XmlElementSyntax parentElement, SyntaxToken lessThanSlashToken)
        {
            var expectedEndTagText = "</" + parentElement.StartTag.Name.LocalName.ValueText + ">";
 
            var token = lessThanSlashToken.GetNextToken(includeDocumentationComments: true);
            while (token.Parent.IsKind(SyntaxKind.XmlText))
            {
                if (token.ValueText == expectedEndTagText)
                {
                    return true;
                }
 
                token = token.GetNextToken(includeDocumentationComments: true);
            }
 
            return false;
        }
 
        private bool HasUnmatchedIdenticalParent(XmlElementStartTagSyntax parentStartTag)
        {
            if (parentStartTag.Parent?.Parent is XmlElementSyntax grandParentElement)
            {
                if (grandParentElement.StartTag.Name.LocalName.ValueText == parentStartTag.Name.LocalName.ValueText)
                {
                    if (HasMatchingEndTag(grandParentElement.StartTag))
                    {
                        return HasUnmatchedIdenticalParent(grandParentElement.StartTag);
                    }
 
                    return true;
                }
            }
 
            return false;
        }
 
        private static bool HasMatchingEndTag(XmlElementStartTagSyntax parentStartTag)
        {
            if (parentStartTag == null)
            {
                return false;
            }
 
            if (parentStartTag.Parent is not XmlElementSyntax parentElement)
            {
                return false;
            }
 
            var endTag = parentElement.EndTag;
            return endTag != null && !endTag.IsMissing && endTag.Name.LocalName.ValueText == parentStartTag.Name.LocalName.ValueText;
        }
 
        private void CheckNameAndInsertText(ITextView textView, ITextBuffer subjectBuffer, SnapshotPoint position, XmlElementStartTagSyntax startTag, int? finalCaretPosition, string formatString)
        {
            if (startTag == null)
            {
                return;
            }
 
            var elementName = startTag.Name.LocalName.ValueText;
 
            if (elementName.Length > 0)
            {
                var parentElement = (XmlElementSyntax)startTag.GetRequiredParent();
                if (parentElement.EndTag.Name.LocalName.ValueText != elementName)
                {
                    InsertTextAndMoveCaret(textView, subjectBuffer, position, string.Format(formatString, elementName), finalCaretPosition);
                }
            }
        }
    }
}