Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
✨ comment and docstring folding provider
  • Loading branch information
DonJayamanne committed May 2, 2018
commit 641f023c35838d204855c4592a185bcf5b17754c
16 changes: 11 additions & 5 deletions src/client/language/iterableTextRange.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
// 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 = 0;
let index = -1;

return {
next(): IteratorResult<T> {
if (index < this.textRangeCollection.length) {
next: (): IteratorResult<T> => {
if (index < this.textRangeCollection.count - 1) {
return {
done: false,
value: this.textRangeCollection[index += 1]
value: this.textRangeCollection.getItemAt(index += 1)
};
} else {
return {
done: true,
value: undefined
// tslint:disable-next-line:no-any
value: undefined as any
};
}
}
Expand Down
82 changes: 68 additions & 14 deletions src/client/providers/docStringFoldingProvider.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,67 @@
import { CancellationToken, FoldingProvider, FoldingRange, FoldingRangeList, Position, Range, TextDocument } from 'vscode';
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { CancellationToken, FoldingProvider, FoldingRange, FoldingRangeList, Range, TextDocument } from 'vscode';
import { IterableTextRange } from '../language/iterableTextRange';
import { IToken, TextRange, TokenizerMode, TokenType } from '../language/types';
import { IToken, TokenizerMode, TokenType } from '../language/types';
import { getDocumentTokens } from './providerUtilities';

export class DocStringFoldingProvider implements FoldingProvider {
public provideFoldingRanges(document: TextDocument, _: CancellationToken): FoldingRangeList {
const ranges = this.getDocstringTokens(document);
const ranges = this.getFoldingRanges(document);
return new FoldingRangeList(ranges);
}

private getDocstringTokens(document: TextDocument) {
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 range = this.getDocStringFoldingRange(document, token);
if (range) {
docStringRanges.push(range);
const docstringRange = this.getDocStringFoldingRange(document, token);
if (docstringRange) {
docStringRanges.push(docstringRange);
continue;
}

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

return docStringRanges;
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.endLine + 1 === commentRange.startLine) {
previousComment.endLine = commentRange.endLine;
return;
}
if (previousComment.startLine === previousComment.endLine) {
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.startLine === lastComment.endLine) {
commentRanges.pop();
}
}
private getDocStringFoldingRange(document: TextDocument, token: IToken) {
if (token.type !== TokenType.String) {
Expand All @@ -34,21 +75,34 @@ export class DocStringFoldingProvider implements FoldingProvider {
}

const startLine = document.lineAt(startPosition);
if (startLine.firstNonWhitespaceCharacterIndex < token.start) {
if (startLine.firstNonWhitespaceCharacterIndex !== startPosition.character) {
return;
}
const endLine = document.lineAt(endPosition);
if (endLine.firstNonWhitespaceCharacterIndex > token.end) {
const startIndex1 = startLine.text.indexOf('\'\'\'');
const startIndex2 = startLine.text.indexOf('"""');
if (startIndex1 !== startPosition.character && startIndex2 !== startPosition.character) {
return;
}

const range = new Range(startPosition, endPosition);
const text = document.getText(range);

if (!text.startsWith('\'\'\'') && !text.startsWith('"""')) {
return new FoldingRange(range.start.line, range.end.line, 'docstring');
}
private getSingleLineCommentRange(document: TextDocument, token: IToken) {
if (token.type !== TokenType.Comment) {
return;
}

return new FoldingRange(range.start.line, range.end.line, text.substring(0, Math.min(text.length, 50)));
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, '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.

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.

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, TextDocument } from 'vscode';
import { DocStringFoldingProvider } from '../../client/providers/docStringFoldingProvider';
import { openTextDocument } from '../common';
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, 'docstring'), new FoldingRange(44, 73, 'comments'),
new FoldingRange(95, 143, 'docstring'), new FoldingRange(149, 150, 'comments'),
new FoldingRange(305, 313, 'docstring'), new FoldingRange(320, 322, 'docstring')
]
},
{
file: path.join(pythonFilesPath, 'visualstudio_ipython_repl.py'), ranges: [
new FoldingRange(0, 14, 'docstring'), new FoldingRange(78, 79, 'comments'),
new FoldingRange(81, 82, 'comments'), new FoldingRange(92, 93, 'comments'),
new FoldingRange(108, 109, 'comments'), new FoldingRange(139, 140, 'comments'),
new FoldingRange(169, 170, 'comments'), new FoldingRange(275, 277, 'comments'),
new FoldingRange(319, 320, 'comments')
]
},
{
file: path.join(pythonFilesPath, 'visualstudio_py_debugger.py'), ranges: [
new FoldingRange(0, 15, 'comments'), new FoldingRange(22, 25, 'comments'),
new FoldingRange(47, 48, 'comments'), new FoldingRange(69, 70, 'comments'),
new FoldingRange(96, 97, 'comments'), new FoldingRange(105, 106, 'comments'),
new FoldingRange(141, 142, 'comments'), new FoldingRange(149, 162, 'comments'),
new FoldingRange(165, 166, 'comments'), new FoldingRange(207, 208, 'comments'),
new FoldingRange(235, 237, 'comments'), new FoldingRange(240, 241, 'comments'),
new FoldingRange(300, 301, 'comments'), new FoldingRange(334, 335, 'comments'),
new FoldingRange(346, 348, 'comments'), new FoldingRange(499, 500, 'comments'),
new FoldingRange(558, 559, 'comments'), new FoldingRange(602, 604, 'comments'),
new FoldingRange(608, 609, 'comments'), new FoldingRange(612, 614, 'comments'),
new FoldingRange(637, 638, 'comments')
]
},
{
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 openTextDocument(item.file);
const provider = new DocStringFoldingProvider();
const result = provider.provideFoldingRanges(document, new CancellationTokenSource().token);
expect(result.ranges).to.be.lengthOf(item.ranges.length);
result.ranges.forEach(range => {
const index = item.ranges
.findIndex(searchItem => searchItem.startLine === range.startLine &&
searchItem.endLine === range.endLine);
expect(index).to.be.greaterThan(-1, `${range.startLine}, ${range.endLine} not found`);
});
});
});
});
Loading