10
10
11
11
namespace Microsoft . PowerShell . EditorServices
12
12
{
13
-
14
13
/// <summary>
15
14
/// Provides common operations for the tokens of a parsed script.
16
15
/// </summary>
@@ -20,85 +19,65 @@ internal static class TokenOperations
20
19
private const string RegionKindRegion = "region" ;
21
20
private const string RegionKindNone = null ;
22
21
22
+ // These regular expressions are used to match lines which mark the start and end of region comment in a PowerShell
23
+ // script. They are based on the defaults in the VS Code Language Configuration at;
24
+ // https://github.com/Microsoft/vscode/blob/64186b0a26/extensions/powershell/language-configuration.json#L26-L31
25
+ static private readonly Regex s_startRegionTextRegex = new Regex (
26
+ @"^\s*#region\b" ,
27
+ RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
28
+ static private readonly Regex s_endRegionTextRegex = new Regex (
29
+ @"^\s*#endregion\b" ,
30
+ RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
31
+
23
32
/// <summary>
24
33
/// Extracts all of the unique foldable regions in a script given the list tokens
25
34
/// </summary>
26
35
internal static FoldingReference [ ] FoldableRegions (
27
- Token [ ] tokens ,
28
- bool ShowLastLine )
36
+ Token [ ] tokens )
29
37
{
30
38
List < FoldingReference > foldableRegions = new List < FoldingReference > ( ) ;
39
+ Stack < Token > tokenCommentRegionStack = new Stack < Token > ( ) ;
40
+ Token blockStartToken = null ;
41
+ int blockNextLine = - 1 ;
31
42
32
- // Find matching braces { -> }
33
- // Find matching hashes @{ -> }
34
- foldableRegions . AddRange (
35
- MatchTokenElements ( tokens , new TokenKind [ ] { TokenKind . LCurly , TokenKind . AtCurly } , TokenKind . RCurly , RegionKindNone )
36
- ) ;
37
-
38
- // Find matching parentheses ( -> )
39
- // Find matching array literals @( -> )
40
- // Find matching subexpressions $( -> )
41
- foldableRegions . AddRange (
42
- MatchTokenElements ( tokens , new TokenKind [ ] { TokenKind . LParen , TokenKind . AtParen , TokenKind . DollarParen } , TokenKind . RParen , RegionKindNone )
43
- ) ;
44
-
45
- // Find contiguous here strings @' -> '@
46
- foldableRegions . AddRange (
47
- MatchTokenElement ( tokens , TokenKind . HereStringLiteral , RegionKindNone )
48
- ) ;
49
-
50
- // Find unopinionated variable names ${ \n \n }
51
- foldableRegions . AddRange (
52
- MatchTokenElement ( tokens , TokenKind . Variable , RegionKindNone )
53
- ) ;
54
-
55
- // Find contiguous here strings @" -> "@
56
- foldableRegions . AddRange (
57
- MatchTokenElement ( tokens , TokenKind . HereStringExpandable , RegionKindNone )
58
- ) ;
59
-
60
- // Find matching comment regions #region -> #endregion
61
- foldableRegions . AddRange (
62
- MatchCustomCommentRegionTokenElements ( tokens , RegionKindRegion )
63
- ) ;
64
-
65
- // Find blocks of line comments # comment1\n# comment2\n...
66
- foldableRegions . AddRange (
67
- MatchBlockCommentTokenElement ( tokens , RegionKindComment )
68
- ) ;
69
-
70
- // Find comments regions <# -> #>
71
- foldableRegions . AddRange (
72
- MatchTokenElement ( tokens , TokenKind . Comment , RegionKindComment )
73
- ) ;
74
-
75
- // Remove any null entries. Nulls appear if the folding reference is invalid
76
- // or missing
77
- foldableRegions . RemoveAll ( item => item == null ) ;
78
-
79
- // Sort the FoldingReferences, starting at the top of the document,
80
- // and ensure that, in the case of multiple ranges starting the same line,
81
- // that the largest range (i.e. most number of lines spanned) is sorted
82
- // first. This is needed to detect duplicate regions. The first in the list
83
- // will be used and subsequent duplicates ignored.
84
- foldableRegions . Sort ( ) ;
85
-
86
- // It's possible to have duplicate or overlapping ranges, that is, regions which have the same starting
87
- // line number as the previous region. Therefore only emit ranges which have a different starting line
88
- // than the previous range.
89
- foldableRegions . RemoveAll ( ( FoldingReference item ) => {
90
- // Note - I'm not happy with searching here, but as the RemoveAll
91
- // doesn't expose the index in the List, we need to calculate it. Fortunately the
92
- // list is sorted at this point, so we can use BinarySearch.
93
- int index = foldableRegions . BinarySearch ( item ) ;
94
- if ( index == 0 ) { return false ; }
95
- return ( item . StartLine == foldableRegions [ index - 1 ] . StartLine ) ;
96
- } ) ;
43
+ for ( int index = 0 ; index < tokens . Length ; index ++ )
44
+ {
45
+ Token token = tokens [ index ] ;
46
+ if ( token . Kind == TokenKind . Comment ) {
47
+ // Processing for comment regions <# -> #>
48
+ if ( ( token . Kind == TokenKind . Comment ) && ( token . Extent . StartLineNumber != token . Extent . EndLineNumber ) ) {
49
+ foldableRegions . Add ( CreateFoldingReference ( token , token , RegionKindComment ) ) ;
50
+ }
97
51
98
- // Some editors have different folding UI, sometimes the lastline should be displayed
99
- // If we do want to show the last line, just change the region to be one line less
100
- if ( ShowLastLine ) {
101
- foldableRegions . ForEach ( item => { item . EndLine -- ; } ) ;
52
+ if ( IsBlockComment ( index , tokens ) ) {
53
+ // Regex's are very expensive. Use them sparingly.
54
+ bool isStartRegion = s_startRegionTextRegex . IsMatch ( token . Text ) ;
55
+ bool isEndRegion = s_endRegionTextRegex . IsMatch ( token . Text ) ;
56
+ // Processing for # region -> # endregion
57
+ if ( isStartRegion ) {
58
+ tokenCommentRegionStack . Push ( token ) ;
59
+ }
60
+ if ( ( tokenCommentRegionStack . Count > 0 ) && isEndRegion ) {
61
+ foldableRegions . Add ( CreateFoldingReference ( tokenCommentRegionStack . Pop ( ) , token , RegionKindRegion ) ) ;
62
+ }
63
+ // If it's neither a start or end regsion then it could be block line comment
64
+ // Processing for blocks of line comments # comment1\n# comment2\n...
65
+ if ( ! ( isStartRegion || isEndRegion ) ) {
66
+ int thisLine = token . Extent . StartLineNumber - 1 ;
67
+ if ( ( blockStartToken != null ) && ( thisLine != blockNextLine ) ) {
68
+ foldableRegions . Add ( CreateFoldingReference ( blockStartToken , blockNextLine - 1 , RegionKindComment ) ) ;
69
+ blockStartToken = token ;
70
+ }
71
+ if ( blockStartToken == null ) { blockStartToken = token ; }
72
+ blockNextLine = thisLine + 1 ;
73
+ }
74
+ }
75
+ }
76
+ }
77
+ // If we exit the token array and we're still processing comment lines, then the
78
+ // comment block simply ends at the end of document
79
+ if ( blockStartToken != null ) {
80
+ foldableRegions . Add ( CreateFoldingReference ( blockStartToken , blockNextLine - 1 , RegionKindComment ) ) ;
102
81
}
103
82
104
83
return foldableRegions . ToArray ( ) ;
@@ -144,47 +123,6 @@ static private FoldingReference CreateFoldingReference(
144
123
} ;
145
124
}
146
125
147
- /// <summary>
148
- /// Given an array of tokens, find matching regions which start (array of tokens) and end with a different TokenKind
149
- /// </summary>
150
- static private List < FoldingReference > MatchTokenElements (
151
- Token [ ] tokens ,
152
- TokenKind [ ] startTokenKind ,
153
- TokenKind endTokenKind ,
154
- string matchKind )
155
- {
156
- List < FoldingReference > result = new List < FoldingReference > ( ) ;
157
- Stack < Token > tokenStack = new Stack < Token > ( ) ;
158
- foreach ( Token token in tokens )
159
- {
160
- if ( Array . IndexOf ( startTokenKind , token . Kind ) != - 1 ) {
161
- tokenStack . Push ( token ) ;
162
- }
163
- if ( ( tokenStack . Count > 0 ) && ( token . Kind == endTokenKind ) ) {
164
- result . Add ( CreateFoldingReference ( tokenStack . Pop ( ) , token , matchKind ) ) ;
165
- }
166
- }
167
- return result ;
168
- }
169
-
170
- /// <summary>
171
- /// Given an array of token, finds a specific token
172
- /// </summary>
173
- static private List < FoldingReference > MatchTokenElement (
174
- Token [ ] tokens ,
175
- TokenKind tokenKind ,
176
- string matchKind )
177
- {
178
- List < FoldingReference > result = new List < FoldingReference > ( ) ;
179
- foreach ( Token token in tokens )
180
- {
181
- if ( ( token . Kind == tokenKind ) && ( token . Extent . StartLineNumber != token . Extent . EndLineNumber ) ) {
182
- result . Add ( CreateFoldingReference ( token , token , matchKind ) ) ;
183
- }
184
- }
185
- return result ;
186
- }
187
-
188
126
/// <summary>
189
127
/// Returns true if a Token is a block comment;
190
128
/// - Must be a TokenKind.comment
@@ -199,79 +137,5 @@ static private bool IsBlockComment(int index, Token[] tokens) {
199
137
if ( tokens [ index - 1 ] . Kind != TokenKind . NewLine ) { return false ; }
200
138
return thisToken . Text . StartsWith ( "#" ) ;
201
139
}
202
-
203
- // This regular expressions is used to detect a line comment (as opposed to an inline comment), that is not a region
204
- // block directive i.e.
205
- // - No text between the beginning of the line and `#`
206
- // - Comment does start with region
207
- // - Comment does start with endregion
208
- static private readonly Regex s_nonRegionLineCommentRegex = new Regex (
209
- @"\s*#(?!region\b|endregion\b)" ,
210
- RegexOptions . IgnoreCase | RegexOptions . Compiled ) ;
211
-
212
- /// <summary>
213
- /// Finding blocks of comment tokens is more complicated as the newline characters are not
214
- /// classed as comments. To workaround this we search for valid block comments (See IsBlockCmment)
215
- /// and then determine contiguous line numbers from there
216
- /// </summary>
217
- static private List < FoldingReference > MatchBlockCommentTokenElement (
218
- Token [ ] tokens ,
219
- string matchKind )
220
- {
221
- List < FoldingReference > result = new List < FoldingReference > ( ) ;
222
- Token startToken = null ;
223
- int nextLine = - 1 ;
224
- for ( int index = 0 ; index < tokens . Length ; index ++ )
225
- {
226
- Token thisToken = tokens [ index ] ;
227
- if ( IsBlockComment ( index , tokens ) && s_nonRegionLineCommentRegex . IsMatch ( thisToken . Text ) ) {
228
- int thisLine = thisToken . Extent . StartLineNumber - 1 ;
229
- if ( ( startToken != null ) && ( thisLine != nextLine ) ) {
230
- result . Add ( CreateFoldingReference ( startToken , nextLine - 1 , matchKind ) ) ;
231
- startToken = thisToken ;
232
- }
233
- if ( startToken == null ) { startToken = thisToken ; }
234
- nextLine = thisLine + 1 ;
235
- }
236
- }
237
- // If we exit the token array and we're still processing comment lines, then the
238
- // comment block simply ends at the end of document
239
- if ( startToken != null ) {
240
- result . Add ( CreateFoldingReference ( startToken , nextLine - 1 , matchKind ) ) ;
241
- }
242
- return result ;
243
- }
244
-
245
- /// <summary>
246
- /// Given a list of tokens, find the tokens that are comments and
247
- /// the comment text is either `# region` or `# endregion`, and then use a stack to determine
248
- /// the ranges they span
249
- /// </summary>
250
- static private List < FoldingReference > MatchCustomCommentRegionTokenElements (
251
- Token [ ] tokens ,
252
- string matchKind )
253
- {
254
- // These regular expressions are used to match lines which mark the start and end of region comment in a PowerShell
255
- // script. They are based on the defaults in the VS Code Language Configuration at;
256
- // https://github.com/Microsoft/vscode/blob/64186b0a26/extensions/powershell/language-configuration.json#L26-L31
257
- string startRegionText = @"^\s*#region\b" ;
258
- string endRegionText = @"^\s*#endregion\b" ;
259
-
260
- List < FoldingReference > result = new List < FoldingReference > ( ) ;
261
- Stack < Token > tokenStack = new Stack < Token > ( ) ;
262
- for ( int index = 0 ; index < tokens . Length ; index ++ )
263
- {
264
- if ( IsBlockComment ( index , tokens ) ) {
265
- Token token = tokens [ index ] ;
266
- if ( Regex . IsMatch ( token . Text , startRegionText , RegexOptions . IgnoreCase ) ) {
267
- tokenStack . Push ( token ) ;
268
- }
269
- if ( ( tokenStack . Count > 0 ) && ( Regex . IsMatch ( token . Text , endRegionText , RegexOptions . IgnoreCase ) ) ) {
270
- result . Add ( CreateFoldingReference ( tokenStack . Pop ( ) , token , matchKind ) ) ;
271
- }
272
- }
273
- }
274
- return result ;
275
- }
276
140
}
277
141
}
0 commit comments