Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"theme": "dark"
},
"engines": {
"vscode": "^1.18.0"
"vscode": "^1.23.0"
},
"recommendations": [
"donjayamanne.jupyter"
Expand Down
392 changes: 197 additions & 195 deletions src/client/extension.ts

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions src/client/language/iterableTextRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { ITextRange, ITextRangeCollection } from './types';

export class IterableTextRange<T extends ITextRange> implements Iterable<T>{
constructor(private textRangeCollection: ITextRangeCollection<T>) {
}
public [Symbol.iterator](): Iterator<T> {
let index = -1;

return {
next: (): IteratorResult<T> => {
if (index < this.textRangeCollection.count - 1) {
return {
done: false,
value: this.textRangeCollection.getItemAt(index += 1)
};
} else {
return {
done: true,
// tslint:disable-next-line:no-any
value: undefined as any
};
}
}
};
}
}
107 changes: 107 additions & 0 deletions src/client/providers/docStringFoldingProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { CancellationToken, FoldingContext, FoldingRange, FoldingRangeKind, FoldingRangeProvider, ProviderResult, Range, TextDocument } from 'vscode';
import { IterableTextRange } from '../language/iterableTextRange';
import { IToken, TokenizerMode, TokenType } from '../language/types';
import { getDocumentTokens } from './providerUtilities';

export class DocStringFoldingProvider implements FoldingRangeProvider {
public provideFoldingRanges(document: TextDocument, _context: FoldingContext, token: CancellationToken): ProviderResult<FoldingRange[]> {
return this.getFoldingRanges(document);
}

private getFoldingRanges(document: TextDocument) {
const tokenCollection = getDocumentTokens(document, document.lineAt(document.lineCount - 1).range.end, TokenizerMode.CommentsAndStrings);
const tokens = new IterableTextRange(tokenCollection);

const docStringRanges: FoldingRange[] = [];
const commentRanges: FoldingRange[] = [];

for (const token of tokens) {
const docstringRange = this.getDocStringFoldingRange(document, token);
if (docstringRange) {
docStringRanges.push(docstringRange);
continue;
}

const commentRange = this.getSingleLineCommentRange(document, token);
if (commentRange) {
this.buildMultiLineCommentRange(commentRange, commentRanges);
}
}

this.removeLastSingleLineComment(commentRanges);
return docStringRanges.concat(commentRanges);
}
private buildMultiLineCommentRange(commentRange: FoldingRange, commentRanges: FoldingRange[]) {
if (commentRanges.length === 0) {
commentRanges.push(commentRange);
return;
}
const previousComment = commentRanges[commentRanges.length - 1];
if (previousComment.end + 1 === commentRange.start) {
previousComment.end = commentRange.end;
return;
}
if (previousComment.start === previousComment.end) {
commentRanges[commentRanges.length - 1] = commentRange;
return;
}
commentRanges.push(commentRange);
}
private removeLastSingleLineComment(commentRanges: FoldingRange[]) {
// Remove last comment folding range if its a single line entry.
if (commentRanges.length === 0) {
return;
}
const lastComment = commentRanges[commentRanges.length - 1];
if (lastComment.start === lastComment.end) {
commentRanges.pop();
}
}
private getDocStringFoldingRange(document: TextDocument, token: IToken) {
if (token.type !== TokenType.String) {
return;
}

const startPosition = document.positionAt(token.start);
const endPosition = document.positionAt(token.end);
if (startPosition.line === endPosition.line) {
return;
}

const startLine = document.lineAt(startPosition);
if (startLine.firstNonWhitespaceCharacterIndex !== startPosition.character) {
return;
}
const startIndex1 = startLine.text.indexOf('\'\'\'');
const startIndex2 = startLine.text.indexOf('"""');
if (startIndex1 !== startPosition.character && startIndex2 !== startPosition.character) {
return;
}

const range = new Range(startPosition, endPosition);

return new FoldingRange(range.start.line, range.end.line);
}
private getSingleLineCommentRange(document: TextDocument, token: IToken) {
if (token.type !== TokenType.Comment) {
return;
}

const startPosition = document.positionAt(token.start);
const endPosition = document.positionAt(token.end);
if (startPosition.line !== endPosition.line) {
return;
}
if (document.lineAt(startPosition).firstNonWhitespaceCharacterIndex !== startPosition.character) {
return;
}

const range = new Range(startPosition, endPosition);
return new FoldingRange(range.start.line, range.end.line, FoldingRangeKind.Comment);
}
}
65 changes: 65 additions & 0 deletions src/test/providers/foldingProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also add basic tests - empty file, unclosed string, odd sequences like """ s1 """ """ s2 """ without like breaks

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easy, will do.
I think unclosed strings will be treated as strings by tokenizer, wouldn't it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is more how folding code calculates 'next' line position when there may be none

// Licensed under the MIT License.

import { expect } from 'chai';
import * as path from 'path';
import { CancellationTokenSource, FoldingRange, FoldingRangeKind, workspace } from 'vscode';
import { DocStringFoldingProvider } from '../../client/providers/docStringFoldingProvider';

type FileFoldingRanges = { file: string; ranges: FoldingRange[] };
const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'folding');

// tslint:disable-next-line:max-func-body-length
suite('Provider - Folding Provider', () => {
const docStringFileAndExpectedFoldingRanges: FileFoldingRanges[] = [
{
file: path.join(pythonFilesPath, 'attach_server.py'), ranges: [
new FoldingRange(0, 14), new FoldingRange(44, 73, FoldingRangeKind.Comment),
new FoldingRange(95, 143), new FoldingRange(149, 150, FoldingRangeKind.Comment),
new FoldingRange(305, 313), new FoldingRange(320, 322)
]
},
{
file: path.join(pythonFilesPath, 'visualstudio_ipython_repl.py'), ranges: [
new FoldingRange(0, 14), new FoldingRange(78, 79, FoldingRangeKind.Comment),
new FoldingRange(81, 82, FoldingRangeKind.Comment), new FoldingRange(92, 93, FoldingRangeKind.Comment),
new FoldingRange(108, 109, FoldingRangeKind.Comment), new FoldingRange(139, 140, FoldingRangeKind.Comment),
new FoldingRange(169, 170, FoldingRangeKind.Comment), new FoldingRange(275, 277, FoldingRangeKind.Comment),
new FoldingRange(319, 320, FoldingRangeKind.Comment)
]
},
{
file: path.join(pythonFilesPath, 'visualstudio_py_debugger.py'), ranges: [
new FoldingRange(0, 15, FoldingRangeKind.Comment), new FoldingRange(22, 25, FoldingRangeKind.Comment),
new FoldingRange(47, 48, FoldingRangeKind.Comment), new FoldingRange(69, 70, FoldingRangeKind.Comment),
new FoldingRange(96, 97, FoldingRangeKind.Comment), new FoldingRange(105, 106, FoldingRangeKind.Comment),
new FoldingRange(141, 142, FoldingRangeKind.Comment), new FoldingRange(149, 162, FoldingRangeKind.Comment),
new FoldingRange(165, 166, FoldingRangeKind.Comment), new FoldingRange(207, 208, FoldingRangeKind.Comment),
new FoldingRange(235, 237, FoldingRangeKind.Comment), new FoldingRange(240, 241, FoldingRangeKind.Comment),
new FoldingRange(300, 301, FoldingRangeKind.Comment), new FoldingRange(334, 335, FoldingRangeKind.Comment),
new FoldingRange(346, 348, FoldingRangeKind.Comment), new FoldingRange(499, 500, FoldingRangeKind.Comment),
new FoldingRange(558, 559, FoldingRangeKind.Comment), new FoldingRange(602, 604, FoldingRangeKind.Comment),
new FoldingRange(608, 609, FoldingRangeKind.Comment), new FoldingRange(612, 614, FoldingRangeKind.Comment),
new FoldingRange(637, 638, FoldingRangeKind.Comment)
]
},
{
file: path.join(pythonFilesPath, 'visualstudio_py_repl.py'), ranges: []
}
];

docStringFileAndExpectedFoldingRanges.forEach(item => {
test(`Test Docstring folding regions '${path.basename(item.file)}'`, async () => {
const document = await workspace.openTextDocument(item.file);
const provider = new DocStringFoldingProvider();
const ranges = await provider.provideFoldingRanges(document, {}, new CancellationTokenSource().token);
expect(ranges).to.be.lengthOf(item.ranges.length);
ranges!.forEach(range => {
const index = item.ranges
.findIndex(searchItem => searchItem.start === range.start &&
searchItem.end === range.end);
expect(index).to.be.greaterThan(-1, `${range.start}, ${range.end} not found`);
});
});
});
});
Loading