@@ -286,6 +286,143 @@ 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
+ nextLine = lineNum + 1 ;
350
+ } else {
351
+ if ( lineNum === nextLine ) {
352
+ nextLine = lineNum + 1 ;
353
+ } else {
354
+ result . push (
355
+ (
356
+ new LineNumberRange ( vscode . FoldingRangeKind . Comment )
357
+ ) . fromLinePair ( startLine , nextLine - 1 ) ,
358
+ ) ;
359
+ startLine = lineNum ;
360
+ nextLine = lineNum + 1 ;
361
+ }
362
+ }
363
+ }
364
+ }
365
+ } ) ;
366
+
367
+ // If we exit the token array and we're still processing comment lines, then the
368
+ // comment block simply ends at the end of document
369
+ if ( startLine !== - 1 ) {
370
+ result . push ( ( new LineNumberRange ( vscode . FoldingRangeKind . Comment ) ) . fromLinePair ( startLine , nextLine - 1 ) ) ;
371
+ }
372
+
373
+ return result ;
374
+ }
375
+
376
+ /**
377
+ * Create a new token object with an appended scopeName
378
+ * @param token The token to append the scope to
379
+ * @param scopeName The scope name to append
380
+ * @returns A copy of the original token, but with the scope appended
381
+ */
382
+ private addTokenScope (
383
+ token : IToken ,
384
+ scopeName : string ,
385
+ ) : IToken {
386
+ // Only a shallow clone is required
387
+ const tokenClone = Object . assign ( { } , token ) ;
388
+ tokenClone . scopes . push ( scopeName ) ;
389
+ return tokenClone ;
390
+ }
391
+
392
+ /**
393
+ * Given a list of grammar tokens, find the tokens that are comments and
394
+ * the comment text is either `# region` or `# endregion`. Return a new list of tokens
395
+ * with custom scope names added, "custom.start.region" and "custom.end.region" respectively
396
+ * @param tokens List of grammar tokens to parse
397
+ * @param document The source text document
398
+ * @returns A list of LineNumberRange objects of the line comment region blocks
399
+ */
400
+ private extractRegionScopeElements (
401
+ tokens : ITokenList ,
402
+ document : vscode . TextDocument ,
403
+ ) : ITokenList {
404
+ const result = [ ] ;
405
+
406
+ const emptyLine = / ^ [ \s ] + $ / ;
407
+ const startRegionText = / ^ # \s * r e g i o n \b / ;
408
+ const endRegionText = / ^ # \s * e n d r e g i o n \b / ;
409
+
410
+ tokens . forEach ( ( token ) => {
411
+ if ( token . scopes . indexOf ( "punctuation.definition.comment.powershell" ) !== - 1 ) {
412
+ if ( emptyLine . test ( this . preceedingText ( token . startIndex , document ) ) ) {
413
+ const commentText = this . subsequentText ( token . startIndex , document ) ;
414
+ if ( startRegionText . test ( commentText ) ) {
415
+ result . push ( this . addTokenScope ( token , "custom.start.region" ) ) ;
416
+ }
417
+ if ( endRegionText . test ( commentText ) ) {
418
+ result . push ( this . addTokenScope ( token , "custom.end.region" ) ) ;
419
+ }
420
+ }
421
+ }
422
+ } ) ;
423
+ return result ;
424
+ }
425
+
289
426
/**
290
427
* Given a list of tokens, return a list of line number ranges which could be folding regions in the document
291
428
* @param tokens List of grammar tokens to parse
@@ -328,6 +465,25 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
328
465
vscode . FoldingRangeKind . Region , document )
329
466
. forEach ( ( match ) => { matchedTokens . push ( match ) ; } ) ;
330
467
468
+ // Find matching comment regions #region -> #endregion
469
+ this . matchScopeElements (
470
+ this . extractRegionScopeElements ( tokens , document ) ,
471
+ "custom.start.region" ,
472
+ "custom.end.region" ,
473
+ vscode . FoldingRangeKind . Region , document )
474
+ . forEach ( ( match ) => { matchedTokens . push ( match ) ; } ) ;
475
+
476
+ // Find blocks of line comments # comment1\n# comment2\n...
477
+ this . matchBlockCommentScopeElements ( tokens , document ) . forEach ( ( match ) => { matchedTokens . push ( match ) ; } ) ;
478
+
479
+ // Find matching block comments <# -> #>
480
+ this . matchScopeElements (
481
+ tokens ,
482
+ "punctuation.definition.comment.block.begin.powershell" ,
483
+ "punctuation.definition.comment.block.end.powershell" ,
484
+ vscode . FoldingRangeKind . Comment , document )
485
+ . forEach ( ( match ) => { matchedTokens . push ( match ) ; } ) ;
486
+
331
487
return matchedTokens ;
332
488
}
333
489
}
0 commit comments