Skip to content

Commit 84e4ec6

Browse files
glennsartirjmholt
authored andcommitted
(GH-1417) Fix code folding on CRLF documents (#1418)
* (GH-1417) Tokenize a document per line to avoid line endings Previously the folding provider would tokenize the entire document at a time. While this mostly works, it causes issues with regular expressions which use the `$` anchor. This anchor will interpret CRLF vs LF differently. While it may be possible to munge the document prior to tokenization, the prior art within the VS Code codebase shows that this is not the intended usage, i.e. lines should be tokenized, not an entire document. This commit changes the tokenization process to tokenize per line but still preserves the original folding behaviour. * (GH-1417) Add tests for CRLF documents Previously the test fixtures were failing on some Windows systems due to git changing the line endings. This commit adds a new test fixture, and runs the same tests on the same content document, but each with a different line-ending (CRLF and LF). * (maint) Update folding test fixtures Previously some of the text in the test fixtures was confusing e.g. Text that says 'cannot fold' should read 'should fold'. This commit updates the text in the fixture to describe the expected behaviour for humans. * (GH-1417) Add folding tests for double quote here strings Previously only single quoted here strings were tested for folding. This commit adds a test for the double quoted herestrings as well. * (maint) Refactor folding test fixtures Previously there was some duplication in the folder test fixtures. This commit refactors the expectation to be more DRY. This commit also adds an expectation on the line endings as we're depending on git to clone correctly. Without this we may get false positive results.
1 parent b2eba06 commit 84e4ec6

File tree

6 files changed

+169
-66
lines changed

6 files changed

+169
-66
lines changed

src/features/Folding.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as vscode from "vscode";
88
import {
99
DocumentSelector,
1010
LanguageClient,
11+
Position,
1112
} from "vscode-languageclient";
1213
import { IFeature } from "../feature";
1314
import { ILogger } from "../logging";
@@ -193,8 +194,24 @@ export class FoldingProvider implements vscode.FoldingRangeProvider {
193194
// If the grammar hasn't been setup correctly, return empty result
194195
if (this.powershellGrammar == null) { return []; }
195196

196-
// Convert the document text into a series of grammar tokens
197-
const tokens: ITokenList = this.powershellGrammar.tokenizeLine(document.getText(), null).tokens;
197+
// Tokenize each line and build up an array of document-wide tokens
198+
// Note that line endings (CRLF/LF/CR) have interpolation issues so don't
199+
// tokenize an entire document if the line endings are variable.
200+
const tokens: ITokenList = new Array<IToken>();
201+
let tokenizationState = null;
202+
for (let i = 0; i < document.lineCount; i++) {
203+
const result = this.powershellGrammar.tokenizeLine(document.lineAt(i).text, tokenizationState);
204+
const offset = document.offsetAt(new vscode.Position(i, 0)) ;
205+
206+
for (const item of result.tokens) {
207+
// Add the offset of the line to translate a character offset into
208+
// a document based index
209+
item.startIndex += offset;
210+
item.endIndex += offset;
211+
tokens.push(item);
212+
}
213+
tokenizationState = result.ruleStack;
214+
}
198215

199216
// Parse the token list looking for matching tokens and return
200217
// a list of LineNumberRange objects. Then filter the list and only return matches

test/features/folding.test.ts

+38-17
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import { MockLogger } from "../test_utils";
1212
const fixturePath = path.join(__dirname, "..", "..", "..", "test", "fixtures");
1313

1414
function assertFoldingRegions(result, expected): void {
15-
assert.equal(result.length, expected.length);
16-
1715
for (let i = 0; i < expected.length; i++) {
1816
const failMessage = `expected ${JSON.stringify(expected[i])}, actual ${JSON.stringify(result[i])}`;
1917
assert.equal(result[i].start, expected[i].start, failMessage);
2018
assert.equal(result[i].end, expected[i].end, failMessage);
2119
assert.equal(result[i].kind, expected[i].kind, failMessage);
2220
}
21+
22+
assert.equal(result.length, expected.length);
2323
}
2424

2525
suite("Features", () => {
@@ -36,26 +36,47 @@ suite("Features", () => {
3636
assert.notEqual(psGrammar, null);
3737
});
3838

39-
test("Can detect all of the foldable regions in a document", async () => {
40-
// Integration test against the test fixture 'folding.ps1' that contains
41-
// all of the different types of folding available
42-
const uri = vscode.Uri.file(path.join(fixturePath, "folding.ps1"));
43-
const document = await vscode.workspace.openTextDocument(uri);
44-
const result = await provider.provideFoldingRanges(document, null, null);
45-
46-
const expected = [
39+
suite("For a single document", async () => {
40+
const expectedFoldingRegions = [
4741
{ start: 1, end: 6, kind: 1 },
48-
{ start: 7, end: 46, kind: 3 },
42+
{ start: 7, end: 51, kind: 3 },
4943
{ start: 8, end: 13, kind: 1 },
5044
{ start: 14, end: 17, kind: 3 },
51-
{ start: 21, end: 23, kind: 1 },
52-
{ start: 25, end: 35, kind: 3 },
53-
{ start: 27, end: 31, kind: 3 },
54-
{ start: 37, end: 39, kind: 3 },
55-
{ start: 42, end: 45, kind: 3 },
45+
{ start: 19, end: 22, kind: 3 },
46+
{ start: 26, end: 28, kind: 1 },
47+
{ start: 30, end: 40, kind: 3 },
48+
{ start: 32, end: 36, kind: 3 },
49+
{ start: 42, end: 44, kind: 3 },
50+
{ start: 47, end: 50, kind: 3 },
5651
];
5752

58-
assertFoldingRegions(result, expected);
53+
test("Can detect all of the foldable regions in a document with CRLF line endings", async () => {
54+
// Integration test against the test fixture 'folding-crlf.ps1' that contains
55+
// all of the different types of folding available
56+
const uri = vscode.Uri.file(path.join(fixturePath, "folding-crlf.ps1"));
57+
const document = await vscode.workspace.openTextDocument(uri);
58+
const result = await provider.provideFoldingRanges(document, null, null);
59+
60+
// Ensure we have CRLF line endings as we're depending on git
61+
// to clone the test fixtures correctly
62+
assert.notEqual(document.getText().indexOf("\r\n"), -1);
63+
64+
assertFoldingRegions(result, expectedFoldingRegions);
65+
});
66+
67+
test("Can detect all of the foldable regions in a document with LF line endings", async () => {
68+
// Integration test against the test fixture 'folding-lf.ps1' that contains
69+
// all of the different types of folding available
70+
const uri = vscode.Uri.file(path.join(fixturePath, "folding-lf.ps1"));
71+
const document = await vscode.workspace.openTextDocument(uri);
72+
const result = await provider.provideFoldingRanges(document, null, null);
73+
74+
// Ensure we do not CRLF line endings as we're depending on git
75+
// to clone the test fixtures correctly
76+
assert.equal(document.getText().indexOf("\r\n"), -1);
77+
78+
assertFoldingRegions(result, expectedFoldingRegions);
79+
});
5980
});
6081
});
6182
});

test/fixtures/.gitattributes

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Set the default behavior, in case people don't have core.autocrlf set.
2+
* text=auto
3+
4+
# These test fixtures require crlf
5+
folding-crlf.ps1 text eol=crlf
6+
7+
# These test fixtures require lf
8+
folding-lf.ps1 text eol=lf

test/fixtures/folding-crlf.ps1

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
function short-func-not-fold {};
2+
<#
3+
.SYNOPSIS
4+
This whole comment block should fold, not just the SYNOPSIS
5+
.EXAMPLE
6+
This whole comment block should fold, not just the EXAMPLE
7+
#>
8+
function New-VSCodeShouldFold {
9+
<#
10+
.SYNOPSIS
11+
This whole comment block should fold, not just the SYNOPSIS
12+
.EXAMPLE
13+
This whole comment block should fold, not just the EXAMPLE
14+
#>
15+
$I = @'
16+
herestrings should fold
17+
18+
'@
19+
20+
$I = @"
21+
double quoted herestrings should also fold
22+
23+
"@
24+
25+
# this won't be folded
26+
27+
# This block of comments should be foldable as a single block
28+
# This block of comments should be foldable as a single block
29+
# This block of comments should be foldable as a single block
30+
31+
#region This fools the indentation folding.
32+
Write-Host "Hello"
33+
# region Nested regions should be foldable
34+
Write-Host "Hello"
35+
# comment1
36+
Write-Host "Hello"
37+
#endregion
38+
Write-Host "Hello"
39+
# comment2
40+
Write-Host "Hello"
41+
# endregion
42+
43+
$c = {
44+
Write-Host "Script blocks should be foldable"
45+
}
46+
47+
# Array fools indentation folding
48+
$d = @(
49+
'should fold1',
50+
'should fold2'
51+
)
52+
}

test/fixtures/folding-lf.ps1

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
function short-func-not-fold {};
2+
<#
3+
.SYNOPSIS
4+
This whole comment block should fold, not just the SYNOPSIS
5+
.EXAMPLE
6+
This whole comment block should fold, not just the EXAMPLE
7+
#>
8+
function New-VSCodeShouldFold {
9+
<#
10+
.SYNOPSIS
11+
This whole comment block should fold, not just the SYNOPSIS
12+
.EXAMPLE
13+
This whole comment block should fold, not just the EXAMPLE
14+
#>
15+
$I = @'
16+
herestrings should fold
17+
18+
'@
19+
20+
$I = @"
21+
double quoted herestrings should also fold
22+
23+
"@
24+
25+
# this won't be folded
26+
27+
# This block of comments should be foldable as a single block
28+
# This block of comments should be foldable as a single block
29+
# This block of comments should be foldable as a single block
30+
31+
#region This fools the indentation folding.
32+
Write-Host "Hello"
33+
# region Nested regions should be foldable
34+
Write-Host "Hello"
35+
# comment1
36+
Write-Host "Hello"
37+
#endregion
38+
Write-Host "Hello"
39+
# comment2
40+
Write-Host "Hello"
41+
# endregion
42+
43+
$c = {
44+
Write-Host "Script blocks should be foldable"
45+
}
46+
47+
# Array fools indentation folding
48+
$d = @(
49+
'should fold1',
50+
'should fold2'
51+
)
52+
}

test/fixtures/folding.ps1

-47
This file was deleted.

0 commit comments

Comments
 (0)