@@ -286,6 +286,138 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
286
286
return result ;
287
287
}
288
288
289
+ /**
290
+ * Given a zero based offset, find the line text preceeding it in the document
291
+ * @param offset Zero based offset in the document
292
+ * @param document The source text document
293
+ * @returns The line text preceeding the offset, not including the preceeding Line Feed
294
+ */
295
+ private preceedingText (
296
+ offset : number ,
297
+ document : vscode . TextDocument ,
298
+ ) : string {
299
+ const endPos = document . positionAt ( offset ) ;
300
+ const startPos = endPos . translate ( 0 , - endPos . character ) ;
301
+
302
+ return document . getText ( new vscode . Range ( startPos , endPos ) ) ;
303
+ }
304
+
305
+ /**
306
+ * Given a zero based offset, find the line text after it in the document
307
+ * @param offset Zero based offset in the document
308
+ * @param document The source text document
309
+ * @returns The line text after the offset, not including the subsequent Line Feed
310
+ */
311
+ private subsequentText (
312
+ offset : number ,
313
+ document : vscode . TextDocument ,
314
+ ) : string {
315
+ const startPos : vscode . Position = document . positionAt ( offset ) ;
316
+ const endPos : vscode . Position = document . lineAt ( document . positionAt ( offset ) ) . range . end ;
317
+ return document . getText ( new vscode . Range ( startPos , endPos ) ) ;
318
+ }
319
+
320
+ /**
321
+ * Finding blocks of comment tokens is more complicated as the newline characters are not
322
+ * classed as comments. To workaround this we search for the comment character `#` scope name
323
+ * "punctuation.definition.comment.powershell" and then determine contiguous line numbers from there
324
+ * @param tokens List of grammar tokens to parse
325
+ * @param document The source text document
326
+ * @returns A list of LineNumberRange objects for blocks of comment lines
327
+ */
328
+ private matchBlockCommentScopeElements (
329
+ tokens : ITokenList ,
330
+ document : vscode . TextDocument ,
331
+ ) : ILineNumberRangeList {
332
+ const result = [ ] ;
333
+
334
+ const emptyLine = / ^ [ \s ] + $ / ;
335
+
336
+ let startLine : number = - 1 ;
337
+ let nextLine : number = - 1 ;
338
+
339
+ tokens . forEach ( ( token ) => {
340
+ if ( token . scopes . indexOf ( "punctuation.definition.comment.powershell" ) !== - 1 ) {
341
+ // The punctuation.definition.comment.powershell token matches new-line comments
342
+ // and inline comments e.g. `$x = 'foo' # inline comment`. We are only interested
343
+ // in comments which begin the line i.e. no preceeding text
344
+ if ( emptyLine . test ( this . preceedingText ( token . startIndex , document ) ) ) {
345
+ const lineNum = document . positionAt ( token . startIndex ) . line ;
346
+ // A simple pattern for keeping track of contiguous numbers in a known sorted array
347
+ if ( startLine === - 1 ) {
348
+ startLine = lineNum ;
349
+ } else if ( lineNum !== nextLine ) {
350
+ result . push (
351
+ (
352
+ new LineNumberRange ( vscode . FoldingRangeKind . Comment )
353
+ ) . fromLinePair ( startLine , nextLine - 1 ) ,
354
+ ) ;
355
+ startLine = lineNum ;
356
+ }
357
+ nextLine = lineNum + 1 ;
358
+ }
359
+ }
360
+ } ) ;
361
+
362
+ // If we exit the token array and we're still processing comment lines, then the
363
+ // comment block simply ends at the end of document
364
+ if ( startLine !== - 1 ) {
365
+ result . push ( ( new LineNumberRange ( vscode . FoldingRangeKind . Comment ) ) . fromLinePair ( startLine , nextLine - 1 ) ) ;
366
+ }
367
+
368
+ return result ;
369
+ }
370
+
371
+ /**
372
+ * Create a new token object with an appended scopeName
373
+ * @param token The token to append the scope to
374
+ * @param scopeName The scope name to append
375
+ * @returns A copy of the original token, but with the scope appended
376
+ */
377
+ private addTokenScope (
378
+ token : IToken ,
379
+ scopeName : string ,
380
+ ) : IToken {
381
+ // Only a shallow clone is required
382
+ const tokenClone = Object . assign ( { } , token ) ;
383
+ tokenClone . scopes . push ( scopeName ) ;
384
+ return tokenClone ;
385
+ }
386
+
387
+ /**
388
+ * Given a list of grammar tokens, find the tokens that are comments and
389
+ * the comment text is either `# region` or `# endregion`. Return a new list of tokens
390
+ * with custom scope names added, "custom.start.region" and "custom.end.region" respectively
391
+ * @param tokens List of grammar tokens to parse
392
+ * @param document The source text document
393
+ * @returns A list of LineNumberRange objects of the line comment region blocks
394
+ */
395
+ private extractRegionScopeElements (
396
+ tokens : ITokenList ,
397
+ document : vscode . TextDocument ,
398
+ ) : ITokenList {
399
+ const result = [ ] ;
400
+
401
+ const emptyLine = / ^ [ \s ] + $ / ;
402
+ const startRegionText = / ^ # \s * r e g i o n \b / ;
403
+ const endRegionText = / ^ # \s * e n d r e g i o n \b / ;
404
+
405
+ tokens . forEach ( ( token ) => {
406
+ if ( token . scopes . indexOf ( "punctuation.definition.comment.powershell" ) !== - 1 ) {
407
+ if ( emptyLine . test ( this . preceedingText ( token . startIndex , document ) ) ) {
408
+ const commentText = this . subsequentText ( token . startIndex , document ) ;
409
+ if ( startRegionText . test ( commentText ) ) {
410
+ result . push ( this . addTokenScope ( token , "custom.start.region" ) ) ;
411
+ }
412
+ if ( endRegionText . test ( commentText ) ) {
413
+ result . push ( this . addTokenScope ( token , "custom.end.region" ) ) ;
414
+ }
415
+ }
416
+ }
417
+ } ) ;
418
+ return result ;
419
+ }
420
+
289
421
/**
290
422
* Given a list of tokens, return a list of line number ranges which could be folding regions in the document
291
423
* @param tokens List of grammar tokens to parse
@@ -328,6 +460,25 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
328
460
vscode . FoldingRangeKind . Region , document )
329
461
. forEach ( ( match ) => { matchedTokens . push ( match ) ; } ) ;
330
462
463
+ // Find matching comment regions #region -> #endregion
464
+ this . matchScopeElements (
465
+ this . extractRegionScopeElements ( tokens , document ) ,
466
+ "custom.start.region" ,
467
+ "custom.end.region" ,
468
+ vscode . FoldingRangeKind . Region , document )
469
+ . forEach ( ( match ) => { matchedTokens . push ( match ) ; } ) ;
470
+
471
+ // Find blocks of line comments # comment1\n# comment2\n...
472
+ this . matchBlockCommentScopeElements ( tokens , document ) . forEach ( ( match ) => { matchedTokens . push ( match ) ; } ) ;
473
+
474
+ // Find matching block comments <# -> #>
475
+ this . matchScopeElements (
476
+ tokens ,
477
+ "punctuation.definition.comment.block.begin.powershell" ,
478
+ "punctuation.definition.comment.block.end.powershell" ,
479
+ vscode . FoldingRangeKind . Comment , document )
480
+ . forEach ( ( match ) => { matchedTokens . push ( match ) ; } ) ;
481
+
331
482
return matchedTokens ;
332
483
}
333
484
}
0 commit comments