Skip to content

Commit 5a1fbb6

Browse files
committed
(PowerShellGH-1336) Add syntax folding for comments
This commit adds syntax aware folding for comment regions * Contiguous blocks of line comments `# ....` * Block comments `<# ... #>` * Region bound comments `# region ... # endregion`
1 parent 6a7923c commit 5a1fbb6

File tree

1 file changed

+156
-0
lines changed

1 file changed

+156
-0
lines changed

src/features/Folding.ts

+156
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,143 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
286286
return result;
287287
}
288288

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*region\b/;
408+
const endRegionText = /^#\s*endregion\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+
289426
/**
290427
* Given a list of tokens, return a list of line number ranges which could be folding regions in the document
291428
* @param tokens List of grammar tokens to parse
@@ -328,6 +465,25 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
328465
vscode.FoldingRangeKind.Region, document)
329466
.forEach((match) => { matchedTokens.push(match); });
330467

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+
331487
return matchedTokens;
332488
}
333489
}

0 commit comments

Comments
 (0)