Sam's Blog

07 Oct

Custom Visual Studio language services: Advanced commenting features

The default line/block commenting/uncommenting implementation in the Managed Package Framework is … well, lacking. When line comments are available, it always uses them, and it always inserts the comments at the beginning of the line. I came up with a better (IMO of course) set of rules to determine whether block comments or line comments should be used.

  • Use line comments if:
    • UseLineComments is true
    • AND LineStart is not null or empty
    • AND one of the following is true:
      1. there is no selected text
      2. (on the line where the selection starts, there is only whitespace up to the selection start point) AND ((on the line where the selection ends, there is only whitespace up to the selection end point) OR (there is only whitespace from the selection end point to the end of the line))
  • Use block comments if:
    • We are not using line comments
    • AND some text is selected
    • AND BlockStart is not null or empty
    • AND BlockEnd is not null or empty

Then I came up with a better set of rules should line commenting be used:

  • Make sure line comments are indented as far as possible, skipping empty lines as necessary
  • Don’t comment N+1 lines when only N lines were selected my clicking in the left margin

Implementation is straightforward. Simply add the following to your custom source class derived from Microsoft.VisualStudio.Package.Source. Sorry about the narrow column – there’s a horizontal scroll bar at the bottom of the code block.

Assumptions in this code:

  • You already have GetCommentFormat overridden to return your language’s commenting style (the base class implementation uses C++/C# style comments)
  • CommentSelection and UncommentSelection are in your SR resources. You can change their usage to be string literals if you want.
  • The CommentLines function uses Linq to find minindex. If you aren’t using C# 3 and .NET 3.5, you’ll need to duplicate the functionality another way.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#region Commenting
 
public override TextSpan CommentSpan( TextSpan span )
{
    TextSpan result = span;
    CommentInfo commentInfo = GetCommentFormat();
 
    using ( new CompoundAction( this, SR.CommentSelection ) )
    {
        /*
         * Use line comments if:
         *  UseLineComments is true
         *  AND LineStart is not null or empty
         *  AND one of the following is true:
         *
         *  1. there is no selected text
         *  2. on the line where the selection starts, there is only whitespace up to the selection start point
         *     AND on the line where the selection ends, there is only whitespace up to the selection end point,
         *         OR there is only whitespace from the selection end point to the end of the line
         *
         * Use block comments if:
         *  We are not using line comments
         *  AND some text is selected
         *  AND BlockStart is not null or empty
         *  AND BlockEnd is not null or empty
         */
        if ( commentInfo.UseLineComments
            && !string.IsNullOrEmpty( commentInfo.LineStart )
            && ( TextSpanHelper.IsEmpty( span ) ||
                ( ( GetText( span.iStartLine, 0, span.iStartLine, span.iStartIndex ).Trim().Length == 0 )
                    && ( ( GetText( span.iEndLine, 0, span.iEndLine, span.iEndIndex ).Trim().Length == 0 )
                        || ( GetText( span.iEndLine, span.iEndIndex, span.iEndLine, GetLineLength( span.iEndLine ) ).Trim().Length == 0 ) )
               ) ) )
        {
            result = CommentLines( span, commentInfo.LineStart );
        }
        else if (
            TextSpanHelper.IsPositive( span )
            && !string.IsNullOrEmpty( commentInfo.BlockStart )
            && !string.IsNullOrEmpty( commentInfo.BlockEnd )
            )
        {
            result = CommentBlock( span, commentInfo.BlockStart, commentInfo.BlockEnd );
        }
    }
    return result;
}
 
public override TextSpan CommentLines( TextSpan span, string lineComment )
{
    /*
     * Rules for line comments:
     *  Make sure line comments are indented as far as possible, skipping empty lines as necessary
     *  Don't comment N+1 lines when only N lines were selected my clicking in the left margin
     */
    if ( span.iEndLine > span.iStartLine && span.iEndIndex == 0 )
        span.iEndLine--;
 
    int minindex = ( from i in Enumerable.Range( span.iStartLine, span.iEndLine - span.iStartLine + 1 )
                     where GetLine( i ).Trim().Length > 0
                     select ScanToNonWhitespaceChar( i ) )
                   .Min();
 
    //comment each line
    for ( int line = span.iStartLine; line <= span.iEndLine; line++ )
    {
        if ( GetLine( line ).Trim().Length > 0 )
            SetText( line, minindex, line, minindex, lineComment );
    }
 
    span.iStartIndex = 0;
    span.iEndIndex = GetLineLength( span.iEndLine );
 
    return span;
}
 
public override TextSpan CommentBlock( TextSpan span, string blockStart, string blockEnd )
{
    //sp. case no selection
    if ( span.iStartIndex == span.iEndIndex &&
        span.iStartLine == span.iEndLine )
    {
        span.iStartIndex = ScanToNonWhitespaceChar( span.iStartLine );
        span.iEndIndex = GetLineLength( span.iEndLine );
    }
    //sp. case partial selection on single line
    if ( span.iStartLine == span.iEndLine )
    {
        span.iEndIndex += blockStart.Length;
    }
    //add start comment
    SetText( span.iStartLine, span.iStartIndex, span.iStartLine, span.iStartIndex, blockStart );
    //add end comment
    SetText( span.iEndLine, span.iEndIndex, span.iEndLine, span.iEndIndex, blockEnd );
    span.iEndIndex += blockEnd.Length;
    return span;
}
 
public override TextSpan UncommentSpan( TextSpan span )
{
    CommentInfo commentInfo = GetCommentFormat();
 
    using ( new CompoundAction( this, SR.UncommentSelection ) )
    {
        // special case: empty span
        if ( TextSpanHelper.IsEmpty( span ) )
        {
            if ( commentInfo.UseLineComments )
                span = UncommentLines( span, commentInfo.LineStart );
            return span;
        }
 
        string textblock = GetText( span ).Trim();
 
        if ( !string.IsNullOrEmpty( commentInfo.BlockStart )
            && !string.IsNullOrEmpty( commentInfo.BlockEnd )
            && textblock.Length >= commentInfo.BlockStart.Length + commentInfo.BlockEnd.Length
            && textblock.StartsWith( commentInfo.BlockStart )
            && textblock.EndsWith( commentInfo.BlockEnd ) )
        {
            TrimSpan( ref span );
            span = UncommentBlock( span, commentInfo.BlockStart, commentInfo.BlockEnd );
        }
        else if ( commentInfo.UseLineComments && !string.IsNullOrEmpty( commentInfo.LineStart ) )
        {
            span = UncommentLines( span, commentInfo.LineStart );
        }
    }
    return span;
}
 
public override TextSpan UncommentLines( TextSpan span, string lineComment )
{
    if ( span.iEndLine > span.iStartLine && span.iEndIndex == 0 )
        span.iEndLine--;
 
    // Remove line comments
    int clen = lineComment.Length;
    for ( int line = span.iStartLine; line <= span.iEndLine; line++ )
    {
        int i = ScanToNonWhitespaceChar( line );
        string text = GetLine( line );
        if ( ( text.Length > i + clen ) && text.Substring( i, clen ) == lineComment )
        {
            SetText( line, i, line, i + clen, "" ); // remove line comment.
        }
    }
 
    span.iStartIndex = 0;
    span.iEndIndex = GetLineLength( span.iEndLine );
    return span;
}
 
public override TextSpan UncommentBlock( TextSpan span, string blockStart, string blockEnd )
{
 
    int startLen = GetLineLength( span.iStartLine );
    int endLen = GetLineLength( span.iEndLine );
 
    TextSpan result = span;
 
    //sp. case no selection, try and uncomment the current line.
    if ( span.iStartIndex == span.iEndIndex &&
        span.iStartLine == span.iEndLine )
    {
        span.iStartIndex = ScanToNonWhitespaceChar( span.iStartLine );
        span.iEndIndex = GetLineLength( span.iEndLine );
    }
 
    // Check that comment start and end blocks are possible.
    if ( span.iStartIndex + blockStart.Length <= startLen && span.iEndIndex - blockStart.Length >= 0 )
    {
        string startText = GetText( span.iStartLine, span.iStartIndex, span.iStartLine, span.iStartIndex + blockStart.Length );
 
        if ( startText == blockStart )
        {
            string endText = null;
            TextSpan linespan = span;
            linespan.iStartLine = linespan.iEndLine;
            linespan.iStartIndex = linespan.iEndIndex - blockEnd.Length;
            System.Diagnostics.Debug.Assert( TextSpanHelper.IsPositive( linespan ) );
            endText = GetText( linespan );
            if ( endText == blockEnd )
            {
                //yes, block comment selected; remove it        
                SetText( linespan.iStartLine, linespan.iStartIndex, linespan.iEndLine, linespan.iEndIndex, null );
                SetText( span.iStartLine, span.iStartIndex, span.iStartLine, span.iStartIndex + blockStart.Length, null );
                span.iEndIndex -= blockEnd.Length;
                if ( span.iStartLine == span.iEndLine )
                    span.iEndIndex -= blockStart.Length;
                result = span;
            }
        }
    }
 
    return result;
}
 
#endregion

Leave a Reply

© 2014 Sam's Blog | Entries (RSS) and Comments (RSS)

Your Index Web Directorywordpress logo