繁体   English   中英

建议 XML 属性名称和值并关闭 Xml 标签

[英]Suggest XML Attribute Names and Values and Close Xml Tags Using Pavel Torgashov's Autocomplete Menu for C# WinForms

我正在开发一个应用程序,使用 WinForms 文本框和快速彩色文本框控件(带有语法突出显示)的组合来编辑用 html 标记和其他 xml 标记标记的字符串,并且我已经在使用 Pavel Torgashov 的自动完成菜单来制作非 xml某些文本框中的建议。 我想在键入“<”时使用自动完成菜单来建议 xml 标签,并建议属性名称和值,但仅当胡萝卜位于建议适用的标签内时。 我还想自动建议关闭下一个需要关闭的打开标签。

在阅读了 CodeProject 页面上关于自动完成菜单的许多评论后,我看到其他人提出了同样的问题,但没有提供解决方案。

如何才能做到这一点?

Pavel Torgasov 的自动完成菜单的工作原理是识别插入符号周围的文本片段,作为建议的基础,使用正则表达式来定义片段中包含哪些字符。 这适用于许多场景,但我能想出的唯一方法是让 xml 建议正常工作,需要使用不同的搜索模式来定义插入符号之前包含的字符,而不是插入符号之后包含的字符。 这是一个简单的修改,也是我认为通过继承 Autocomplete Item 来实现所有必需的 xml 功能所必需的唯一修改。

AutocompleteMenu.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 接口的扩展方法来添加此功能,用于了解直接片段之前的文本框中的文本如何与当前适用的建议相关联,而不是进一步修改他的 AutocompleteMenu class 本身。

AutocompleteExtensions 代码清单

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

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 );
    }

下一个扩展方法从 Torgasov 的片段自动完成项复制代码,以便该功能可以包含在其他 AutocompleteItem 子类中,而不必从他的片段自动完成项派生。

    /// <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> )。

第一种是一种更简单的方法,它只会在本地搜索,无法在插入符号之前的任何结束标记之前生成开始标记的建议。 (换句话说,如果在前面的例子中“sentence”这个词是加粗的,它会停止在"</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 );
        if( matches.Count > 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 )
    {
        return textbox.Text.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 文档的示例,鉴于插入符号位于特定的 position 上,上述算法会产生错误的建议。 我很想知道人们提出的哪些示例会产生错误的建议,以及他们如何建议改进该算法。

MethodSnippetAutocompleteItem

我的大多数 xml 自动完成项目都建立在 Torgasov 开发的自动完成项目的基础上,该项目在用户在代表编程语言代码的文本框中键入句点后建议方法名称。 我将他的 MethodAutocompleteItem 中的代码复制到我自己的 AutocompleteItem 子类中,以便进行一些改进。

第一个改进是指定将一个符号(例如,class 名称)与第二个符号(例如,方法名称)分开的“枢轴”字符。 For suggestions that represent xml tag names, the pivot character will become "<", and for suggestions that represent values for xml attributes inside of tags, the pivot character will become "=".

与 xml 建议相关的另一项改进是添加了string[] AppliesTo属性。 如果设置,这将导致仅当 pivot 字符之前的部分在 ApplysTo 数组中时才显示建议。 这允许仅在用户键入它们适用的属性名称后才建议属性值。

Compare 覆盖还将当前片段的 pivot 字符之后的部分存储在受保护的字段中,供覆盖 IsApplicable 方法的子类使用。

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 和上述扩展方法,xml 建议要求可以通过相对简单的 AutocompleteItem 子类轻松满足。

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

AutocompleteItem 的前两个面向 xml 的子类适用于任意标签,而不是专门适用于开发人员已将标签名称列为要建议的特定标签的标签。

第一个建议将当前正在进行的标记作为开始标记/结束标记对完成,并通过上述扩展方法使用片段行为将插入符号放置在开始和结束标记之间。

/// <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,以便在用户键入“<”时出现。 正如我上面提到的,我提供了两种不同的方法来确定为哪个标签插入结束标签。 以下 class 对两者都有声明,其中一个已被注释掉。 切换调用两种扩展方法中的哪一种来使用另一种方法。

/// <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;
    }
}

自动建议标签名称

下一个 class 派生自 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;
        }
    }
}

自动建议属性名称和值

最后两个类为属性名称和值提供建议。 属性名称建议可以使用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,使用基础 class 上的string[] AppliesTo属性过滤掉不适用于当前属性的值建议,并提供string[] AppliesToTag过滤本身。

使用此 class,您还可以指定标签是否应作为完整的开始标签关闭,然后在插入建议后跟随相应的结束标签,或者是否应将标签作为完整的单个标签(例如 )关闭。 如果这些选择均未进行,则插入建议后标签将不完整。

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 );
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM