Skip to content

Commit 1e15180

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 f5ee6d2 commit 1e15180

File tree

1 file changed

+151
-0
lines changed

1 file changed

+151
-0
lines changed

src/features/Folding.ts

+151
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,138 @@ 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+
} 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*region\b/;
403+
const endRegionText = /^#\s*endregion\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+
289421
/**
290422
* Given a list of tokens, return a list of line number ranges which could be folding regions in the document
291423
* @param tokens List of grammar tokens to parse
@@ -328,6 +460,25 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
328460
vscode.FoldingRangeKind.Region, document)
329461
.forEach((match) => { matchedTokens.push(match); });
330462

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+
331482
return matchedTokens;
332483
}
333484
}

0 commit comments

Comments
 (0)