使用Pavel Torgashov的C# WinForms自动完成菜单建议XML属性名称和值并关闭XML标记

t8e9dugd  于 2022-11-17  发布在  C#
关注(0)|答案(2)|浏览(185)

我正在使用WinForms文本框和Fast Colored TextBox控件的组合开发应用程序(语法突出显示)来编辑用html标记和其他xml标记标记的字符串,我已经在使用Pavel Torgashov的自动完成菜单在某些文本框中提供非xml建议。我希望在键入“〈"时使用自动完成菜单来建议xml标记,并且建议属性名称和值,但仅当carot位于建议适用的标记内时。我还希望自动建议关闭下一个需要关闭的打开标记。
在阅读了CodeProject page for the autocomplete menu上的许多评论后,我看到其他人也问了同样的问题,但没有提供解决方案。
如何做到这一点?

nwsw7zdq

nwsw7zdq1#

自动建议属性名称和值

最后两个类别提供属性名称和值的建议。属性名称建议可以使用string[] AppliesToTag属性指定要套用到哪些标签。TagName扩充方法是用来在Compare方法期间补充文字片段信息,以实作此筛选。

/// <summary>
/// Provides an autocomplete suggestion for the specified attribute name, when the caret is inside one of the specified tags.  After inserting the attribute name, the equals sign and a quotation mark will also be inserted, and then the autocomplete menu will be automatically reopened.  This will allow <see cref="AttributeValueAutocompleteItem"/> suggestions applicable to this attribute to be listed.
/// </summary>
public class AttributeNameAutocompleteItem : AutocompleteItem
{
    public string[] AppliesToTag { get; init; }

    public AttributeNameAutocompleteItem( string attributeName, params string[] appliesToTag )
        : base( attributeName )
    {
        AppliesToTag = appliesToTag;
    }

    public override CompareResult Compare( string fragmentText )
    {
        if( Parent.TargetControlWrapper.TagStart( ) < 0 )
            return CompareResult.Hidden;

        if( !AppliesToTag.IsNullOrEmpty( )
            && !AppliesToTag.Contains( Parent.TargetControlWrapper.TagName( ), StringComparer.InvariantCultureIgnoreCase ) )
            return CompareResult.Hidden;
        
        return base.Compare( fragmentText );
    }

    public override string GetTextForReplace( )
    {
        return base.GetTextForReplace( ) + "=\"";
    }

    public override void OnSelected( SelectedEventArgs e )
    {
        base.OnSelected( e );

        Parent.ShowAutocomplete( false );
    }
}

属性值建议子类别衍生自MethodSnippetAutocompleteItem,使用基底类别上的string[] AppliesTo属性筛选出不适用于目前属性的值建议,并提供string[] AppliesToTag筛选本身。
使用此类,您还可以指定在插入建议后,标记是否应作为一个完整的开始标记后跟一个相应的结束标记来结束,或者标记是否应作为一个完整的单个标记来结束(例如,<tag />)。如果未进行这两种选择,则在插入建议后,标记将保持不完整。

public enum TagStyle
{
    None = 0,
    Close,
    Single
}

/// <summary>
/// Provides an autocomplete suggestion for the specified value, after the specified attribute name (or one of them) is typed or inserted into the specified tag (or one of them).  The specified value will be wrapped in quotes if it is not already in quotes.  After inserting the value, the autocomplete menu will be automatically reopened so that <see cref="XmlAutocompleteOpenTag"/> can close the tag.
/// </summary>
public class AttributeValueAutocompleteItem : MethodSnippetAutocompleteItem
{
    public override char Pivot { get; init; } = '=';
    public TagStyle TagStyle { get; init; }
    public string[] AppliesToTag { get; init; }
    
    public AttributeValueAutocompleteItem( string text, string appliesToAttribute = null, string appliesToTag = null ) : base( text, appliesToAttribute )
    {
        bool alreadyInQuotes = false;
        if( text.StartsWith( '"' ) || text.StartsWith( '\'' ) )
            if( text[^1] == text[0] )
                alreadyInQuotes = true;
        
        if( !alreadyInQuotes )
            Text = $"\"{text}\"";

        if( !string.IsNullOrEmpty( appliesToTag ) )
            AppliesToTag = new [] { appliesToTag };
    }

    protected override bool IsApplicable( )
    {
        if( Parent.TargetControlWrapper.TagStart( ) < 0 )
            return false;

        if( !AppliesToTag.IsNullOrEmpty( )
            && !AppliesToTag.Contains( Parent.TargetControlWrapper.TagName( ), StringComparer.InvariantCultureIgnoreCase ) )
            return false;
        
        return base.IsApplicable( );
    }

    public override string MenuText
    {
        get
        {
            switch( TagStyle )
            {
                case TagStyle.Close:
                    return $"<{Parent.TargetControlWrapper.TagName( )} ... {base.GetTextForReplace()} > </ >";
                case TagStyle.Single:
                    return $"<{Parent.TargetControlWrapper.TagName( )} ... {base.GetTextForReplace( )}  />";
                default:
                    return base.GetTextForReplace( );
            }
        } 
        set => throw new NotSupportedException( );
    }

    public override string ToString( )
    {
        return MenuText;
    }

    public override string GetTextForReplace( )
    {
        switch( TagStyle )
        {
            default:
                return base.GetTextForReplace( );
            case TagStyle.Close:
                return base.GetTextForReplace( ) + $">^</{Parent.TargetControlWrapper.TagName( )}>";
            case TagStyle.Single:
                return base.GetTextForReplace( ) + "/>";
        }
    }

    public override void OnSelected( SelectedEventArgs e )
    {
        this.SnippetOnSelected( e );

        Parent.ShowAutocomplete( false );
    }
}
wj8zmpe1

wj8zmpe12#

PavelTorgashov的Autocomplete Menu的工作原理是识别插入符号周围的文本片段,并使用正则表达式定义片段中包含哪些字符,从而提供建议。但是我能想到的使xml建议很好地工作的最简单的方法需要一个不同的搜索模式,该模式定义了插入符号之前要包含的字符,而不是包括在插入符号之后。这是一个要做的简单修改,也是我发现通过对Autocomplete Item进行子类化来实现所有必需的xml功能所必需的唯一修改。

自动完成功能表.cs修改

以下列两个属性取代AutocompleteMenu.cs中的SearchPattern属性,在建构函式中加入预设值,并相应地调整使用SearchPattern(虽然是间接使用)的方法:

/// <summary>
/// Regex pattern identifying the characters to include in the fragment, given that they occur before the caret.
/// </summary>
[Description("Regex pattern identifying the characters to include in the fragment, given that they occur before the caret.")]
[DefaultValue(@"[\w\.]")]
public string BackwardSearchPattern { get; set; }

/// <summary>
/// Regex pattern identifying the characters to include in the fragment, given that they occur after the caret.
/// </summary>
[Description( "Regex pattern identifying the characters to include in the fragment, given that they occur after the caret." )]
[DefaultValue( @"[\w\.]" )]
public string ForwardSearchPattern { get; set; }

public AutocompleteMenu( )
{
    //Pre-existing content omitted

    ForwardSearchPattern = @"[\w\.\=\""]",
    BackwardSearchPattern = @"[\w\.\=\""\<]",
}

private Range GetFragment( )
{
    //Note that the original version has a "searchPattern" parameter to which the SearchPattern property value was passed.
    var tb = TargetControlWrapper;

    if (tb.SelectionLength > 0) return new Range(tb);

    string text = tb.Text;
    
    var result = new Range(tb);

    int startPos = tb.SelectionStart;
    //go forward
    var forwardRegex = new Regex( ForwardSearchPattern );
    int i = startPos;
    while (i >= 0 && i < text.Length)
    {
        if (!forwardRegex.IsMatch(text[i].ToString()))
            break;
        i++;
    }
    result.End = i;

    //go backward
    var backwardRegex = new Regex( BackwardSearchPattern );
    i = startPos;
    while (i > 0 && (i - 1) < text.Length)
    {
        if (!backwardRegex.IsMatch(text[i - 1].ToString()))
            break;
        i--;
    }
    result.Start = i;

    return result;
}

在此之后,下一个挑战是使自动完成项能够理解插入符号何时位于标记内,以及该标记的名称(如果已键入)。用于理解直接片段之前的文本框中的文本如何与当前适用的建议相关的该功能可以通过使用应用于ITextBoxWrapper接口的扩展方法来添加,Torgashov使用该接口来表示文本框中,而不是对其AutocompleteMenu类本身进行任何进一步的修改。

自动完成扩展代码列表

在这里,我将这些扩展方法与解决方案中使用的其他方法组合在一起。首先,几个非常简单的扩展使代码的其余部分更具可读性:

public static class AutocompleteExtensions
{   
    public static bool BetweenExclusive( this int number, int start, int end )
    {
        if( number > start && number < end ) return true;
        else return false;
    }
    
    public static bool IsNullOrEmpty( this object obj )
    {
        if( obj is null )
            return true;

        if( obj is string str )
        {
            if( string.IsNullOrEmpty( str ) )
                return true;
            else 
                return false;
        }

        if( obj is ICollection col )
        {
            if( col.Count == 0 )
                return true;
            else 
                return false;
        }

        if( obj is IEnumerable enumerable )
        {
            return enumerable.GetEnumerator( ).MoveNext( );
        }

        return false;
    }
    
    /// <summary>
    /// Determines if the char is an alphabet character, as the first character in any tag name should be.
    /// </summary>
    /// <param name="c"></param>
    /// <returns></returns>
    public static bool IsAlphaChar( this char c )
    {
        if( c >= 'a' && c <= 'z' )
            return true;

        if( c >= 'A' && c <= 'Z' )
            return true;

        return false;
    }
    
    /// <summary>
    /// Returns the remaining substring after the last occurence of the specified value.
    /// </summary>
    /// <param name="str">The string from which to use a substring.</param>
    /// <param name="indexOfThis">The string that marks the start of the substring.</param>
    /// <returns>The remaining substring after the specified value, or string.Empty.  If the value was not found, the entire string will be returned.</returns>
    public static string AfterLast( this string str, string indexOfThis )
    {
        var index = str.LastIndexOf( indexOfThis );
        if( index < 0 )
            return str;

        index = index + indexOfThis.Length;
        if( str.Length <= index )
            return string.Empty;

        return str.Substring( index );
    }

    /// <summary>
    /// Excludes regex matches that, based on the second to last character of the match value, appear to be single tags (i.e., those that combine the start tag and end tag into one tag).
    /// </summary>
    /// <param name="source">An <see cref="IEnumerable{Match}"/>, such as the return value of a call to <see cref="Regex.Matches"/>.</param>
    /// <returns>A filteed enumerable.</returns>
    public static IEnumerable<Match> ExcludeSingles( this IEnumerable<Match> source )
    {
        return source.Where( m => m.Value.Length >= 2 && m.Value[^2] != '/' );
    }

上面的最后一个扩展方法在下面的两个扩展方法中与regex结合使用。regex搜索模式区分开始和结束标记,但是当开始标记没有属性时,它会捕获开始标记中“〉”之前的最后一个字符,作为表示标记名的regex组的一部分,这使得使用regex区分开始标记和空标记变得很难甚至不可能。而使用这种扩展方法则非常简单。
这个next扩展方法从Torgashov的snippet autocomplete项中复制代码,以便可以将该功能包含在其他AutocompleteItem子类中,而不必从其snippet autocomplete项中派生。

/// <summary>
    /// Call from overrides of OnSelected in order to provide snippet autocomplete behavior.
    /// </summary>
    /// <param name="item">The autocomplete item.</param>
    /// <param name="e">The event args passed in to the autocomplete item's overriden OnSelected method.</param>
    public static void SnippetOnSelected( this AutocompleteItem item, SelectedEventArgs e )
    {
        var tb = item.Parent.TargetControlWrapper;
        
        if ( !item.GetTextForReplace( ).Contains( '^' ) )
            return;
        var text = tb.Text;
        for ( int i = item.Parent.Fragment.Start; i < text.Length; i++ )
            if ( text[i] == '^' )
            {
                tb.SelectionStart  = i;
                tb.SelectionLength = 1;
                tb.SelectedText    = "";
                return;
            }
    }

接下来的两个扩展方法为ITextBoxWrapper添加了重要的功能,以便了解插入符号是否在标记内,如果是,则了解标记的名称。

/// <summary>
    /// If the caret is currently inside a (possibly not yet completed) tag, this method determines where the tag starts, exclusive of the initial '<'.
    /// </summary>
    /// <param name="textbox"></param>
    /// <returns>
    /// If the caret is deemed not to be inside tag, the minimum integer value, <c>int.MinValue</c> is returned.
    ///
    /// If the caret is inside a tag that is sufficiently well-formed to identify at least the beginning of the tag name (or namespace), the index of the first character in the tag name (or namespace) is returned.
    ///
    /// If the caret is inside an incomplete tag for which there is not yet a first character in the tag name (or namespace), the negative of the index of where the first character should go is returned.
    ///
    /// In other words, the <c>Math.Abs( )</c> of the returned value is the index of where the first character in the tag name should be.  The returned value is positive if there is a first character, and negative otherwise.
    /// </returns>
    public static int TagStart( this ITextBoxWrapper textbox )
    {
        string beforeCaret = textbox.Text.Substring( 0, textbox.SelectionStart );
        int tagCloseIndex = beforeCaret.LastIndexOf( '>' );
        int tagStartIndex = beforeCaret.LastIndexOf( '<' );
        
        if( tagCloseIndex > tagStartIndex || tagStartIndex < 0 )
            return int.MinValue;

        if( textbox.Text.Length <= tagStartIndex + 1
            || !textbox.Text[tagStartIndex + 1].IsAlphaChar( ) )
            return 0 - (tagStartIndex + 1);

        return tagStartIndex + 1;
    }

    /// <summary>
    /// If the caret is currently inside of a tag, this returns the tag name (including namespace, if stated).  This is only meant to be used when inside of a tag and considering an attribute or attribute value autocomplete suggestion.  As such, it assumed that there is a space immediately after the tag name.
    /// </summary>
    /// <param name="textbox"></param>
    /// <returns></returns>
    public static string TagName( this ITextBoxWrapper textbox )
    {
        var startIndex = textbox.TagStart( );
        if( startIndex < 0 )
            return string.Empty;

        var nameLength = textbox.Text.Substring( startIndex ).IndexOf( ' ' );
        if( nameLength > 0 )
            return textbox.Text.Substring( startIndex, nameLength );
        else
            return textbox.Text.Substring( startIndex );
    }

最后两个ITextBoxWrapper扩展方法提供了确定建议结束标记的开始标记的替代方法(即,如果用户键入了"<p>This is a sentence, but not a closed paragraph.<",则建议插入"/p>)。
第一种方法比较简单,它只在本地搜索,无法在插入符号之前的任何结束标记之前生成开始标记的建议。(换句话说,如果单词“句子”在前一示例中加粗,它将在"</b>处停止搜索并返回string.Empty。)第二个将尝试一直搜索到文本框文本的开头。在我开发的应用程序中,有许多文本框,但每个文本框通常只有几个句子或几个段落的内容,所以我没有机会评估第二种方法在其中包含大型xml或html文档的文本框中的性能。(在我的应用程序中,标记大多数是html,因此这里的默认值不区分大小写,但是如果需要更严格的XML标准遵从性,则可以在第二个方法中向该方法传递一个区分大小写的字符串比较器。)

/// <summary>
    /// Returns the start tag closest to the caret that precedes the caret, so long as no end tag occurs between said start tag and the caret.
    /// </summary>
    /// <param name="textbox"></param>
    /// <returns>The tag name of the tag to close if one was found, or string.Empty.</returns>
    public static string MostRecentOpenTag( this ITextBoxWrapper textbox )
    {
        string beforeCaret = textbox.Text.Substring( 0, textbox.SelectionStart );

        string afterClose   = beforeCaret.AfterLast( "</" );
        string regexPattern = @"<(?'tag'\w+)[^>]*>";
        var    matches      = Regex.Matches( afterClose, regexPattern ).ExcludeSingles().ToArray();
        if( matches.Length > 0 )
            return matches[^1].Groups["tag"].Value;
        else
            return string.Empty;
    }

    /// <summary>
    /// Returns the start tag that is the first that needs to be closed after the caret position.  This will search backwards until reaching the beginning of the text in the text box control, if necessary.  If the xml is not well-formed, it will return its best guess.
    /// </summary>
    /// <param name="textbox"></param>
    /// <returns></returns>
    public static string LastOpenedTag( this ITextBoxWrapper textbox )
    {
        string beforeCaret = textbox.Text.Substring( 0, textbox.SelectionStart );
        return beforeCaret.LastOpenTag( );
    }
    
    public static string LastOpenTag( this string str, IEqualityComparer<string> tagNameComparer = null )
    {
        //Arrange for repeatedly comparing the tag name from a regex Match object to a tag name string, using the supplied string comparer if one was provided.
        tagNameComparer ??= StringComparer.InvariantCultureIgnoreCase;
        bool TC( Match match, string tagName ) => tagNameComparer.Equals( match.Groups["tag"].Value, tagName );
        
        //Find all regex matches to start and end tags in the string.
        Match[] startRegex = Regex.Matches( str, @"<(?'tag'\w+)[^>]*>" ).ToArray();
        Match[] endRegex = Regex.Matches( str, @"</(?'tag'\w+)[\s]*>" ).ToArray();

        //Begin search.
        while( startRegex.Length > 0 )
        {
            //Find the line between text after the most recent end tag and everything up to that end tag.
            int searchCaret = endRegex.Length > 0 ? endRegex[^1].Index : 0;

            //If there are start tags after the most recent end tag, return the most recent start tag.
            if( startRegex[^1].Index >= searchCaret )
                return startRegex[^1].Groups["tag"].Value;
            //Otherwise, move the search caret back to before the most recent end tag was opened, positioning it at the the end tag before that, if there is one.
            else
            {
                //If the end tag was never started, return string.Empty.
                if( searchCaret == 0 )
                    return string.Empty;

                //Trim startRegex to before the search caret.
                startRegex = startRegex.Where( m => m.Index < searchCaret ).ToArray( );
                
                //Determine the end tag that we are dealing with, and find the closest start tag that matches.
                var closedTag = endRegex[^1].Groups["tag"].Value;
                var openMatch = startRegex.LastOrDefault( m => TC( m, closedTag ) );
                if( openMatch == default )
                    return string.Empty;

                //Figure out if there are nested tags of the same name that are also closed, ...
                var ends = endRegex.Where( m => m.Index.BetweenExclusive( openMatch.Index, endRegex[^1].Index ) 
                                                && TC( m, closedTag ) ).ToArray( );
                int additionalEnds = ends.Length;
                //... and keep searching in reverse until the number of start tags matches the number of end tags.
                startRegex = startRegex.Where( m => m.Index < openMatch.Index ).ToArray( );
                //Move searchCaret backwards past the portion represented by the end (and presumably matching start) tags that we are currently dealing with.
                searchCaret = openMatch.Index;
                while( ends.Length != 0 )
                {
                    var starts = startRegex.Where( m => TC( m, closedTag ) ).TakeLast( additionalEnds ).ToArray( );
                    //If there aren't enough start tags for all of the end tags of this name, return string.Empty.
                    if( starts.Length == 0 )
                        return string.Empty;
                    //Otherwise, count how many additional end tags we found while search in reverse for start tags, and then adjust our search for start tags accordingly.
                    else
                    {
                        ends = endRegex.Where( m => m.Index.BetweenExclusive( starts[0].Index, ends[0].Index )
                                                    && TC( m, closedTag ) ).ToArray( );
                        additionalEnds = ends.Length;
                        //Keep moving searchCaret:
                        searchCaret = starts[0].Index;
                        //Trim tags that are engulfed by the tag that we are currently dealing with from startRegex.
                        startRegex     = startRegex.Where( m => m.Index < starts[0].Index ).ToArray( );
                    }
                }

                //If there are no more start tags after we skip the tag we are currently dealing with (and potentially nested tags of the same name), then return string.Empty.
                if( startRegex.Length == 0 )
                    return string.Empty;
                //Otherwise, trim endRegex to exclude the end tags we just searched past, and restart the outer loop.
                else
                    endRegex = endRegex.Where( m => m.Index < searchCaret ).ToArray( );
            }
        }

        //After exhaustively searching the string, no un-closed start tag was found.
        return string.Empty;
    }
}

我几乎可以肯定,StackOverflow上的某个人将能够提出一个格式良好的XML文档的示例,对于该文档,如果插入符号位于特定位置,上述算法将产生错误的建议。我很想知道人们提出了哪些产生错误建议的示例,以及他们如何建议改进该算法。

方法代码段自动完成项

我的大多数xml自动完成项都建立在Torgashov开发的自动完成项的基础上,该自动完成项在用户在表示编程语言代码的文本框中键入句点后建议方法名。我将他的MethodAutocompleteItem中的代码复制到我自己的AutocompleteItem子类中,以便进行一些改进。
第一个改进是指定“枢纽”字符,将一个符号(例如类名)与第二个符号(例如方法名)分隔开。对于表示xml标记名的建议,枢纽字符将变为“〈",而对于表示标记内xml属性值的建议,枢纽字符将变为“="。

与xml建议相关的另一个改进是添加了string[] AppliesTo属性。如果设置了该属性,则仅当透视字符之前的部分位于AppliesTo数组中时,才会显示建议。这允许仅在用户键入适用的属性名称后才建议属性值。
Compare覆盖还将当前片段中透视字符之后的部分存储在受保护的字段中,以供覆盖IsAppliable方法的子类使用。

public class MethodSnippetAutocompleteItem : AutocompleteItem
{
    public string[] AppliesTo { get; init; }
    public virtual char Pivot { get; init; } = '.';

    public MethodSnippetAutocompleteItem( string text, params string[] appliesTo )
        : this( text )
    {
        AppliesTo = appliesTo;
    }

    protected string _lastPart;
    #region From MethodAutocompleteItem
    protected string _firstPart;
    string lowercaseText;

    public MethodSnippetAutocompleteItem( string text )
        : base( text )
    {
        lowercaseText = Text.ToLower( );
    }
    
    public override CompareResult Compare(string fragmentText)
    {
        int i = fragmentText.LastIndexOf( Pivot );
        if (i < 0)
            return CompareResult.Hidden;
        _lastPart = fragmentText.Substring(i + 1);
        _firstPart = fragmentText.Substring(0, i);

        string startWithFragment = Parent.TargetControlWrapper.Text.Substring( Parent.TargetControlWrapper.SelectionStart );

        if( !IsApplicable( ) ) return CompareResult.Hidden;

        if (_lastPart == "") return CompareResult.Visible;
        if (Text.StartsWith(_lastPart, StringComparison.InvariantCultureIgnoreCase))
            return CompareResult.VisibleAndSelected;
        if (lowercaseText.Contains(_lastPart.ToLower()))
            return CompareResult.Visible;

        return CompareResult.Hidden;
    }

    public override string GetTextForReplace()
    {
        return _firstPart + Pivot + Text;
    }
    #endregion

    public override void OnSelected( SelectedEventArgs e ) => this.SnippetOnSelected( e );

    protected virtual bool IsApplicable( )
    {
        if( AppliesTo.IsNullOrEmpty( )
           || AppliesTo.Contains( _firstPart, StringComparer.InvariantCultureIgnoreCase ) )
            return true;
        
        return false;
    }
}

基于MethodSnippetAutocomplete和上面的扩展方法,使用相对简单的AutocompleteItem子类就可以很容易地满足xml建议要求。

自动建议完成当前标记,或为任意开始标记插入结束标记

AutocompleteItem的前两个面向XML的子类适用于任意标记,而不是专门适用于开发人员已将标记名称列为要建议的特定标记的标记。
第一种方法建议将当前in-progress标记作为开始标记/结束标记对完成,并通过上述扩展方法使用代码片段行为将插入符号放置在开始标记和结束标记之间。

/// <summary>
/// Provides an autocomplete suggestion that would finish the current tag, placing a matching end tag after it, and positioning the caret between the start and end tags.
/// </summary>
public class XmlAutocompleteOpenTag : AutocompleteItem
{
    private string _fragment = string.Empty;
    public override CompareResult Compare( string fragmentText )
    {
        _fragment = fragmentText;

        var tagStart = Parent.TargetControlWrapper.TagStart( );
        //If we are not inside a tag that has a name, do not list this item.
        if( tagStart < 0 )
            return CompareResult.Hidden;

        //If we are inside a tag, and the current fragment is the tag name, then do list the item.
        if( (Parent.Fragment.Start + 1) == Math.Abs( tagStart ) )
            return CompareResult.Visible;

        //If we are inside a tag, and the current fragment potentially represents a complete attribute value, then do list the item, unless it is probably just the start of a value.
        char[] validEndPoints = new [] {  '"', '\'' };
        if( fragmentText.Length > 0 && validEndPoints.Contains( fragmentText.ToCharArray( )[^1] ) )
            if( fragmentText.Length > 1 && fragmentText[^2] != '=' )
                return CompareResult.VisibleAndSelected;
        
        //If we are at any other location inside of a tag, do not list the item.
        return CompareResult.Hidden;
    }

    public override string MenuText
    {
        get => $"<{Parent.TargetControlWrapper.TagName()} ... > </ >";
        set => throw new NotSupportedException( );
    }

    public override string ToString( )
    {
        return MenuText;
    }

    public override string GetTextForReplace( )
    {
        return _fragment + $">^</{Parent.TargetControlWrapper.TagName()}>";
    }

    public override void OnSelected( SelectedEventArgs e ) => this.SnippetOnSelected( e );
}

第二个方法建议为下一个需要关闭的开始标记添加结束标记。它派生自MethodSnippetAutocompleteItem,以便在用户键入“〈"时出现。正如我上面提到的,我提供了两种不同的方法来确定为哪个标记插入结束标记。下面的类提供了这两种方法的语句:其中一个被注解掉。切换两个扩展方法中的哪一个被调用以使用另一个方法。

/// <summary>
/// Provides an autocomplete suggestion that would close the most recently opened tag, so long as there is no end tag of any kind between the open tag and the current caret.
/// </summary>
public class XmlAutoEndPriorTag : MethodSnippetAutocompleteItem
{
    public override char Pivot { get; init; } = '<';

    public XmlAutoEndPriorTag( ) : base( string.Empty )
    { }

    public override CompareResult Compare( string fragmentText )
    {
        var baseResult = base.Compare( fragmentText );
        if( baseResult == CompareResult.Hidden ) 
            return CompareResult.Hidden;

        //string tagToClose = Parent.TargetControlWrapper.MostRecentOpenTag( );
        string tagToClose = Parent.TargetControlWrapper.LastOpenedTag( );
        
        Text = $"/{tagToClose}>";

        if( tagToClose.IsNullOrEmpty( ) )
            return CompareResult.Hidden;

        if( _lastPart.IsNullOrEmpty( ) )
            return CompareResult.Visible;

        if( Text.StartsWith( _lastPart ) )
            return CompareResult.VisibleAndSelected;

        return CompareResult.Hidden;
    }
    
    public override string MenuText
    {
        get => $"<{Text}";
        set => throw new NotSupportedException( );
    }

    public override string ToString( )
    {
        return MenuText;
    }
}

自动建议标记名称

下一个类派生自MethodSnippetAutocompleteItem以便在用户键入“〈"时建议特定的标记名。它被设计为与其他AutocompleteItem子类一起工作以实现完整标记的构造,而不是处理标记名本身以外的内容。如果标记名在末尾定义有空格,则在用户插入此建议后,将自动建议属性名称并以结束标记结束标记。如果不需要特定标记的属性建议,请省略空格。

/// <summary>
/// Provides an autocomplete suggestion that represents an Xml tag's name.  After inserting the name, it will automatically reopen the autocomplete menu, with a minimum fragment length of 0.  This will allow this suggestion to work in conjunction with <see cref="XmlAutocompleteOpenTag"/> to automatically produce the complete tag in two steps.
///
/// If a space is included at the end of the tag name supplied to the constructor, then <see cref="AttributeNameAutocompleteItem"/> suggestions applicable to this tag will also be listed after the tag name is inserted.
/// </summary>
public class XmlTagAutocompleteItem : MethodSnippetAutocompleteItem
{
    public override char Pivot { get; init; } = '<';
    
    /// <summary>
    /// Creates a suggestion for the specified tag name.
    /// </summary>
    /// <param name="tagName">The name of the tag, followed by a space if <see cref="AttributeNameAutocompleteItem"/> suggestions should be listed after inserting this tag name.</param>
    public XmlTagAutocompleteItem( string tagName ) : base( tagName )
    { }

    protected override bool IsApplicable( )
    {
        //If the complete item has already been inserted, do not list it because inserting this type of autocomplete item automatically reopens the menu.
        if( Text.Equals( _lastPart, StringComparison.InvariantCultureIgnoreCase ) )
            return false;
        
        var tagStart = Parent.TargetControlWrapper.TagStart( );
        var selectionStart = Parent.TargetControlWrapper.SelectionStart;
        
        //If we are not inside a tag at all, do not list this item.
        if( tagStart == int.MinValue )
            return false;

        //If we are inside a tag, but the current fragment does not include the tag name, do not list this item.
        if( (Parent.Fragment.Start + 1) != Math.Abs( tagStart ) )
            return false;

        //If we are at the start of an end tag, do not list this item.
        if( Parent.TargetControlWrapper.Text.Length >= selectionStart + 1
            && Parent.TargetControlWrapper.Text.Substring( selectionStart ).StartsWith( '/' ) )
        {
            return false;
        }
        
        return base.IsApplicable( );
    }

    public override void OnSelected( SelectedEventArgs e )
    {
        base.OnSelected( e );

        if( Text.EndsWith( " " ) )
        {
            var previousMinFragmentLength = Parent.MinFragmentLength;
            Parent.MinFragmentLength = 0;
            Parent.ShowAutocomplete( false );
            Parent.MinFragmentLength = previousMinFragmentLength;
        }
    }
}

相关问题