Skip to content

Commit bb2d37c

Browse files
authored
WIP: Add support for folding of docstrings and comments (#894)
Add support for folding of docstrings and comments
1 parent 1180941 commit bb2d37c

14 files changed

+3332
-196
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"theme": "dark"
4141
},
4242
"engines": {
43-
"vscode": "^1.18.0"
43+
"vscode": "^1.23.0"
4444
},
4545
"recommendations": [
4646
"donjayamanne.jupyter"

src/client/extension.ts

Lines changed: 197 additions & 195 deletions
Large diffs are not rendered by default.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { ITextRange, ITextRangeCollection } from './types';
7+
8+
export class IterableTextRange<T extends ITextRange> implements Iterable<T>{
9+
constructor(private textRangeCollection: ITextRangeCollection<T>) {
10+
}
11+
public [Symbol.iterator](): Iterator<T> {
12+
let index = -1;
13+
14+
return {
15+
next: (): IteratorResult<T> => {
16+
if (index < this.textRangeCollection.count - 1) {
17+
return {
18+
done: false,
19+
value: this.textRangeCollection.getItemAt(index += 1)
20+
};
21+
} else {
22+
return {
23+
done: true,
24+
// tslint:disable-next-line:no-any
25+
value: undefined as any
26+
};
27+
}
28+
}
29+
};
30+
}
31+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { CancellationToken, FoldingContext, FoldingRange, FoldingRangeKind, FoldingRangeProvider, ProviderResult, Range, TextDocument } from 'vscode';
7+
import { IterableTextRange } from '../language/iterableTextRange';
8+
import { IToken, TokenizerMode, TokenType } from '../language/types';
9+
import { getDocumentTokens } from './providerUtilities';
10+
11+
export class DocStringFoldingProvider implements FoldingRangeProvider {
12+
public provideFoldingRanges(document: TextDocument, _context: FoldingContext, token: CancellationToken): ProviderResult<FoldingRange[]> {
13+
return this.getFoldingRanges(document);
14+
}
15+
16+
private getFoldingRanges(document: TextDocument) {
17+
const tokenCollection = getDocumentTokens(document, document.lineAt(document.lineCount - 1).range.end, TokenizerMode.CommentsAndStrings);
18+
const tokens = new IterableTextRange(tokenCollection);
19+
20+
const docStringRanges: FoldingRange[] = [];
21+
const commentRanges: FoldingRange[] = [];
22+
23+
for (const token of tokens) {
24+
const docstringRange = this.getDocStringFoldingRange(document, token);
25+
if (docstringRange) {
26+
docStringRanges.push(docstringRange);
27+
continue;
28+
}
29+
30+
const commentRange = this.getSingleLineCommentRange(document, token);
31+
if (commentRange) {
32+
this.buildMultiLineCommentRange(commentRange, commentRanges);
33+
}
34+
}
35+
36+
this.removeLastSingleLineComment(commentRanges);
37+
return docStringRanges.concat(commentRanges);
38+
}
39+
private buildMultiLineCommentRange(commentRange: FoldingRange, commentRanges: FoldingRange[]) {
40+
if (commentRanges.length === 0) {
41+
commentRanges.push(commentRange);
42+
return;
43+
}
44+
const previousComment = commentRanges[commentRanges.length - 1];
45+
if (previousComment.end + 1 === commentRange.start) {
46+
previousComment.end = commentRange.end;
47+
return;
48+
}
49+
if (previousComment.start === previousComment.end) {
50+
commentRanges[commentRanges.length - 1] = commentRange;
51+
return;
52+
}
53+
commentRanges.push(commentRange);
54+
}
55+
private removeLastSingleLineComment(commentRanges: FoldingRange[]) {
56+
// Remove last comment folding range if its a single line entry.
57+
if (commentRanges.length === 0) {
58+
return;
59+
}
60+
const lastComment = commentRanges[commentRanges.length - 1];
61+
if (lastComment.start === lastComment.end) {
62+
commentRanges.pop();
63+
}
64+
}
65+
private getDocStringFoldingRange(document: TextDocument, token: IToken) {
66+
if (token.type !== TokenType.String) {
67+
return;
68+
}
69+
70+
const startPosition = document.positionAt(token.start);
71+
const endPosition = document.positionAt(token.end);
72+
if (startPosition.line === endPosition.line) {
73+
return;
74+
}
75+
76+
const startLine = document.lineAt(startPosition);
77+
if (startLine.firstNonWhitespaceCharacterIndex !== startPosition.character) {
78+
return;
79+
}
80+
const startIndex1 = startLine.text.indexOf('\'\'\'');
81+
const startIndex2 = startLine.text.indexOf('"""');
82+
if (startIndex1 !== startPosition.character && startIndex2 !== startPosition.character) {
83+
return;
84+
}
85+
86+
const range = new Range(startPosition, endPosition);
87+
88+
return new FoldingRange(range.start.line, range.end.line);
89+
}
90+
private getSingleLineCommentRange(document: TextDocument, token: IToken) {
91+
if (token.type !== TokenType.Comment) {
92+
return;
93+
}
94+
95+
const startPosition = document.positionAt(token.start);
96+
const endPosition = document.positionAt(token.end);
97+
if (startPosition.line !== endPosition.line) {
98+
return;
99+
}
100+
if (document.lineAt(startPosition).firstNonWhitespaceCharacterIndex !== startPosition.character) {
101+
return;
102+
}
103+
104+
const range = new Range(startPosition, endPosition);
105+
return new FoldingRange(range.start.line, range.end.line, FoldingRangeKind.Comment);
106+
}
107+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { expect } from 'chai';
5+
import * as path from 'path';
6+
import { CancellationTokenSource, FoldingRange, FoldingRangeKind, workspace } from 'vscode';
7+
import { DocStringFoldingProvider } from '../../client/providers/docStringFoldingProvider';
8+
9+
type FileFoldingRanges = { file: string; ranges: FoldingRange[] };
10+
const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'folding');
11+
12+
// tslint:disable-next-line:max-func-body-length
13+
suite('Provider - Folding Provider', () => {
14+
const docStringFileAndExpectedFoldingRanges: FileFoldingRanges[] = [
15+
{
16+
file: path.join(pythonFilesPath, 'attach_server.py'), ranges: [
17+
new FoldingRange(0, 14), new FoldingRange(44, 73, FoldingRangeKind.Comment),
18+
new FoldingRange(95, 143), new FoldingRange(149, 150, FoldingRangeKind.Comment),
19+
new FoldingRange(305, 313), new FoldingRange(320, 322)
20+
]
21+
},
22+
{
23+
file: path.join(pythonFilesPath, 'visualstudio_ipython_repl.py'), ranges: [
24+
new FoldingRange(0, 14), new FoldingRange(78, 79, FoldingRangeKind.Comment),
25+
new FoldingRange(81, 82, FoldingRangeKind.Comment), new FoldingRange(92, 93, FoldingRangeKind.Comment),
26+
new FoldingRange(108, 109, FoldingRangeKind.Comment), new FoldingRange(139, 140, FoldingRangeKind.Comment),
27+
new FoldingRange(169, 170, FoldingRangeKind.Comment), new FoldingRange(275, 277, FoldingRangeKind.Comment),
28+
new FoldingRange(319, 320, FoldingRangeKind.Comment)
29+
]
30+
},
31+
{
32+
file: path.join(pythonFilesPath, 'visualstudio_py_debugger.py'), ranges: [
33+
new FoldingRange(0, 15, FoldingRangeKind.Comment), new FoldingRange(22, 25, FoldingRangeKind.Comment),
34+
new FoldingRange(47, 48, FoldingRangeKind.Comment), new FoldingRange(69, 70, FoldingRangeKind.Comment),
35+
new FoldingRange(96, 97, FoldingRangeKind.Comment), new FoldingRange(105, 106, FoldingRangeKind.Comment),
36+
new FoldingRange(141, 142, FoldingRangeKind.Comment), new FoldingRange(149, 162, FoldingRangeKind.Comment),
37+
new FoldingRange(165, 166, FoldingRangeKind.Comment), new FoldingRange(207, 208, FoldingRangeKind.Comment),
38+
new FoldingRange(235, 237, FoldingRangeKind.Comment), new FoldingRange(240, 241, FoldingRangeKind.Comment),
39+
new FoldingRange(300, 301, FoldingRangeKind.Comment), new FoldingRange(334, 335, FoldingRangeKind.Comment),
40+
new FoldingRange(346, 348, FoldingRangeKind.Comment), new FoldingRange(499, 500, FoldingRangeKind.Comment),
41+
new FoldingRange(558, 559, FoldingRangeKind.Comment), new FoldingRange(602, 604, FoldingRangeKind.Comment),
42+
new FoldingRange(608, 609, FoldingRangeKind.Comment), new FoldingRange(612, 614, FoldingRangeKind.Comment),
43+
new FoldingRange(637, 638, FoldingRangeKind.Comment)
44+
]
45+
},
46+
{
47+
file: path.join(pythonFilesPath, 'visualstudio_py_repl.py'), ranges: []
48+
}
49+
];
50+
51+
docStringFileAndExpectedFoldingRanges.forEach(item => {
52+
test(`Test Docstring folding regions '${path.basename(item.file)}'`, async () => {
53+
const document = await workspace.openTextDocument(item.file);
54+
const provider = new DocStringFoldingProvider();
55+
const ranges = await provider.provideFoldingRanges(document, {}, new CancellationTokenSource().token);
56+
expect(ranges).to.be.lengthOf(item.ranges.length);
57+
ranges!.forEach(range => {
58+
const index = item.ranges
59+
.findIndex(searchItem => searchItem.start === range.start &&
60+
searchItem.end === range.end);
61+
expect(index).to.be.greaterThan(-1, `${range.start}, ${range.end} not found`);
62+
});
63+
});
64+
});
65+
});

0 commit comments

Comments
 (0)