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:
- there is no selected text
- (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
andUncommentSelection
are in yourSR
resources. You can change their usage to be string literals if you want.- The
CommentLines
function uses Linq to findminindex
. 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 |