Skip to content

Commit cd19f84

Browse files
committed
(GH-1336) Add syntax folding for comments
This commit adds syntax aware folding for comment regions * Contiguous blocks of line comments `# ....` * Function defintion blocks `<# ... #>` * Region bound comments `# region ... # endregion`
1 parent 57bd4c7 commit cd19f84

File tree

1 file changed

+125
-0
lines changed

1 file changed

+125
-0
lines changed

src/features/Folding.ts

+125
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,114 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
207207
return result;
208208
}
209209

210+
// Given a zero based offset, find the line text preceeding it in the document
211+
private preceedingText(offset: number,
212+
document: vscode.TextDocument): string {
213+
const endPos = document.positionAt(offset);
214+
const startPos = endPos.translate(0, -endPos.character);
215+
216+
return document.getText(new vscode.Range(startPos, endPos));
217+
}
218+
219+
// Given a zero based offset, return the line number in the document
220+
private lineAtOffest(offset: number,
221+
document: vscode.TextDocument): number {
222+
return document.positionAt(offset).line;
223+
}
224+
225+
// Finding blocks of comment tokens is more complicated as the newline characters are not
226+
// classed as comments. To workaround this we search for the comment character `#` scope name
227+
// "punctuation.definition.comment.powershell" and then determine contiguous line numbers from there
228+
private matchBlockCommentScopeElements(tokens,
229+
document: vscode.TextDocument) {
230+
const result = [];
231+
232+
const emptyLine = new RegExp("^[\\s]+$");
233+
234+
let startLine: number = -1;
235+
let nextLine: number = -1;
236+
237+
tokens.forEach((token, index) => {
238+
if (token.scopes.includes("punctuation.definition.comment.powershell")) {
239+
// The punctuation.definition.comment.powershell token matches new-line comments
240+
// and inline comments e.g. `$x = 'foo' # inline comment`. We are only interested
241+
// in comments which begin the line i.e. no preceeding text
242+
if (emptyLine.test(this.preceedingText(token.startIndex, document))) {
243+
const lineNum = this.lineAtOffest(token.startIndex, document);
244+
// A simple pattern for keeping track of contiguous numbers in a known
245+
// sorted array
246+
if (startLine === -1) {
247+
startLine = lineNum;
248+
nextLine = lineNum + 1;
249+
} else {
250+
if (lineNum === nextLine) {
251+
nextLine = lineNum + 1;
252+
} else {
253+
result.push(new MatchedToken(null,
254+
null,
255+
startLine,
256+
nextLine - 1,
257+
MatchType.Comment,
258+
document));
259+
startLine = lineNum;
260+
nextLine = lineNum + 1;
261+
}
262+
}
263+
}
264+
}
265+
});
266+
267+
// If we exist the token array and we're still processing comment lines, then the
268+
// comment block simply ends at the end of document
269+
if (startLine !== -1) {
270+
result.push(new MatchedToken(null, null, startLine, nextLine - 1, MatchType.Comment, document));
271+
}
272+
273+
return result;
274+
}
275+
276+
// Given a zero based offset, find the line text after it in the document
277+
private subsequentText(offset: number,
278+
document: vscode.TextDocument): string {
279+
const startPos = document.positionAt(offset);
280+
// We don't know how long the line is so just return a really long one.
281+
const endPos = startPos.translate(0, 1000);
282+
return document.getText(new vscode.Range(startPos, endPos));
283+
}
284+
285+
// Create a new token object with an appended scopeName
286+
private addTokenScope(token,
287+
scopeName: string) {
288+
// Only a shallow clone is required
289+
const tokenClone = Object.assign({}, token);
290+
tokenClone.scopes.push(scopeName);
291+
return tokenClone;
292+
}
293+
294+
// Given a list of grammar tokens, find the tokens that are comments and
295+
// the comment text is either `# region` or `# endregion`. Return a new list of tokens
296+
// with custom scope names added, "custom.start.region" and "custom.end.region" respectively
297+
private extractRegionScopeElements(tokens,
298+
document: vscode.TextDocument) {
299+
const result = [];
300+
301+
const emptyLine = new RegExp("^[\\s]+$");
302+
const startRegionText = new RegExp("^#\\s*region\\b");
303+
const endRegionText = new RegExp("^#\\s*endregion\\b");
304+
305+
tokens.forEach( (token, index) => {
306+
if (token.scopes.includes("punctuation.definition.comment.powershell")) {
307+
if (emptyLine.test(this.preceedingText(token.startIndex, document))) {
308+
const commentText = this.subsequentText(token.startIndex, document);
309+
310+
if (startRegionText.test(commentText)) { result.push(this.addTokenScope(token, "custom.start.region")); }
311+
if (endRegionText.test(commentText)) { result.push(this.addTokenScope(token, "custom.end.region")); }
312+
}
313+
}
314+
});
315+
return result;
316+
}
317+
210318
// Given a list of tokens, return a list of matched tokens/line numbers regions
211319
private matchGrammarTokens(tokens, document: vscode.TextDocument): IMatchedTokenList {
212320
const matchedTokens = [];
@@ -237,6 +345,23 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
237345
MatchType.Region, document)
238346
.forEach((x) => { matchedTokens.push(x); });
239347

348+
// Find matching Braces #region -> #endregion
349+
this.matchScopeElements(this.extractRegionScopeElements(tokens, document),
350+
"custom.start.region",
351+
"custom.end.region",
352+
MatchType.Region, document)
353+
.forEach( (x) => { matchedTokens.push(x); });
354+
355+
// Find blocks of line comments # comment1\n# comment2\n...
356+
this.matchBlockCommentScopeElements(tokens, document).forEach((x) => { matchedTokens.push(x); });
357+
358+
// Find matching function definitions <# -> #>
359+
this.matchScopeElements(tokens,
360+
"punctuation.definition.comment.block.begin.powershell",
361+
"punctuation.definition.comment.block.end.powershell",
362+
MatchType.Comment, document)
363+
.forEach( (x) => { matchedTokens.push(x); });
364+
240365
return matchedTokens;
241366
}
242367

0 commit comments

Comments
 (0)