Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom outlining regions #17954

Merged
merged 14 commits into from
Sep 16, 2017

Conversation

uniqueiniquity
Copy link
Contributor

Fixes #11073.

This update enables support for custom outlining regions of the following form

// #region label

...code...

// #endregion

Region delimiters may not be preceded by any non-whitespace text, and they are not valid if they occur within a multiline comment.

@msftclas
Copy link

msftclas commented Aug 21, 2017

@uniqueiniquity,
Thanks for your contribution as a Microsoft full-time employee or intern. You do not need to sign a CLA.
Thanks,
Microsoft Pull Request Bot
#Closed

return regionText;
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

The logic in this whole 'if' block seems a little cumbersome. You're already using a RegExp to determine if it is a region. You should be able to use a capture group in the RegExp to extract the name also.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to use capture group.

Copy link
Member

Choose a reason for hiding this comment

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

That's not the most efficient way to use capture groups. Simply do:

// Change above regexp to the below
// Not removal of the gm flags and moving the left paren of the capture group past the whitespace
const regionStart = new RegExp("^\\s*//\\s*#region\\s+(.*)?$"); 

// In here just do
const result = comment.match(regionStart);
return result[1] ? result[1] : regionText;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now I've actually pushed the change. :)

////*/
////
/////*
////// #endregion
Copy link
Member

Choose a reason for hiding this comment

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

It would be good to have tests with unbalanced start/end markers too. This is an area where a regression could easily slip through and cause exceptions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added unbalanced tests.

//////#endregion|]
////
////// Nested regions
////[|// #region outer
Copy link
Member

Choose a reason for hiding this comment

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

Is there any way to test the name that appears for the region? (And add some tests for interesting region names, e.g. includes spaces or tabs).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as I can tell, fourslash doesn't currently gather that information.
I can try to add the functionality if you would like me to though.

Copy link
Member

@billti billti left a comment

Choose a reason for hiding this comment

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

Some minor changes, else looks good. Thanks!

const regions: RegionRange[] = [];
const regionText = "#region";
const regionStart = new RegExp("^\\s*//\\s*#region(\\s+.*)?$", "gm");
const regionEnd = new RegExp("^\\s*//\\s*#endregion(\\s|$)", "gm");
Copy link
Member

@amcasey amcasey Aug 21, 2017

Choose a reason for hiding this comment

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

\s [](start = 60, length = 3)

Exactly one? #Closed

Copy link
Contributor Author

@uniqueiniquity uniqueiniquity Aug 22, 2017

Choose a reason for hiding this comment

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

Yes; if the line ends after #endregion, it's fine, and if there's whitespace and then anything else, it's still fine, since we just want to know that the key phrase is actually there. #Closed

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I missed that this was a substring match (i.e. that the end of the line was open-ended).

@@ -6,9 +6,14 @@ namespace ts.OutliningElementsCollector {
export function collectElements(sourceFile: SourceFile, cancellationToken: CancellationToken): OutliningSpan[] {
const elements: OutliningSpan[] = [];
let depth = 0;
const regions: RegionRange[] = [];
const regionText = "#region";
const regionStart = new RegExp("^\\s*//\\s*#region(\\s+.*)?$", "gm");
Copy link
Member

Choose a reason for hiding this comment

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

gm [](start = 72, length = 2)

What does this do?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the flags :) they weren't necessary anymore.

const regions: RegionRange[] = [];
const regionText = "#region";
const regionStart = new RegExp("^\\s*//\\s*#region(\\s+.*)?$", "gm");
const regionEnd = new RegExp("^\\s*//\\s*#endregion(\\s|$)", "gm");
Copy link
Member

@amcasey amcasey Aug 21, 2017

Choose a reason for hiding this comment

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

endregion [](start = 50, length = 9)

Is there any way to associate the region's identifier with the #endregion? #Closed

Copy link
Member

Choose a reason for hiding this comment

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

Yes, you can put anything you want there.

@@ -6,9 +6,14 @@ namespace ts.OutliningElementsCollector {
export function collectElements(sourceFile: SourceFile, cancellationToken: CancellationToken): OutliningSpan[] {
const elements: OutliningSpan[] = [];
let depth = 0;
const regions: RegionRange[] = [];
const regionText = "#region";
const regionStart = new RegExp("^\\s*//\\s*#region(\\s+.*)?$", "gm");
Copy link
Member

@amcasey amcasey Aug 21, 2017

Choose a reason for hiding this comment

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

region [](start = 52, length = 6)

Case sensitive? #Closed

Copy link
Contributor Author

@uniqueiniquity uniqueiniquity Aug 22, 2017

Choose a reason for hiding this comment

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

C# is case sensitive, so we're matching them. #Closed

function addOutliningSpanRegions(regionSpan: RegionRange) {
if (regionSpan) {
const span: OutliningSpan = {
textSpan: createTextSpanFromBounds(regionSpan.pos, regionSpan.end),
Copy link
Member

Choose a reason for hiding this comment

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

createTextSpanFromBounds(regionSpan.pos, regionSpan.end) [](start = 30, length = 56)

Extract common sub-expression?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extracted.

@@ -89,6 +106,69 @@ namespace ts.OutliningElementsCollector {
return isFunctionBlock(node) && node.parent.kind !== SyntaxKind.ArrowFunction;
}

function isRegionStart(start: number, end: number) {
Copy link
Member

Choose a reason for hiding this comment

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

isRegionStart [](start = 17, length = 13)

It would appear to be a precondition that start is within a comment. Consider adding a comment to that effect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See new approach.

const result = comment.match(regionStart);

if (result && result.length > 0) {
const sections = result[0].split(" ").filter(function (s) { return s !== ""; });
Copy link
Member

Choose a reason for hiding this comment

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

" " [](start = 53, length = 3)

Why space?

Copy link
Member

Choose a reason for hiding this comment

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

Seems like you want to split on any whitespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See new capture group approach.

const result = comment.match(regionStart);

if (result && result.length > 0) {
const sections = result[0].split(" ").filter(function (s) { return s !== ""; });
Copy link
Member

Choose a reason for hiding this comment

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

function [](start = 65, length = 8)

Arrow expressions are nice in cases like this.

const lineStarts = sourceFile.getLineStarts();

for (const currentLineStart of lineStarts) {
const lineEnd = sourceFile.getLineEndOfPosition(currentLineStart);
Copy link
Member

Choose a reason for hiding this comment

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

getLineEndOfPosition [](start = 43, length = 20)

Couldn't you use the start position of the next line?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using start position of next line now.

@@ -89,6 +106,69 @@ namespace ts.OutliningElementsCollector {
return isFunctionBlock(node) && node.parent.kind !== SyntaxKind.ArrowFunction;
}

function isRegionStart(start: number, end: number) {
Copy link
Member

Choose a reason for hiding this comment

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

isRegionStart [](start = 17, length = 13)

This is a confusing name for a method that returns the name of the region.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed.

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

🕐

Copy link
Member

@amcasey amcasey left a comment

Choose a reason for hiding this comment

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

LGTM, with minor comments

const regions: RegionRange[] = [];
const regionText = "#region";
const regionStart = new RegExp("^\\s*//\\s*#region(\\s+.*)?$", "gm");
const regionEnd = new RegExp("^\\s*//\\s*#endregion(\\s|$)", "gm");
Copy link
Member

Choose a reason for hiding this comment

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

Seeing as you now search line-by-line, as opposed to over the whole file, there is no value in the 'g' and 'm' flags on the regular expression. You are only looking for one match on one line at a time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.

////
////// #endregion outer|]
////
////// region delimiters not valid when preceding text on line
Copy link
Contributor

Choose a reason for hiding this comment

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

"when there is"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

@@ -0,0 +1,46 @@
/// <reference path="fourslash.ts"/>

////// basic region
Copy link
Contributor

Choose a reason for hiding this comment

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

un-named/anonymous region?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed "region without label"

function addOutliningSpanRegions(regionSpan: RegionRange) {
if (regionSpan) {
const span: OutliningSpan = {
textSpan: createTextSpanFromBounds(regionSpan.pos, regionSpan.end),
Copy link
Contributor

Choose a reason for hiding this comment

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

Use createTextSpanFromRange?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed.

@@ -89,6 +106,69 @@ namespace ts.OutliningElementsCollector {
return isFunctionBlock(node) && node.parent.kind !== SyntaxKind.ArrowFunction;
}

function isRegionStart(start: number, end: number) {
if (!ts.formatting.getRangeOfEnclosingComment(sourceFile, start, /*onlyMultiLine*/ true)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

There is also an isInComment helper which coerces the result into a boolean, but then you would effectively be doing a triple-negation. I'm still inclined to switching to the helper, but this is mostly a matter of taste.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Used isInComment.

@@ -6,9 +6,14 @@ namespace ts.OutliningElementsCollector {
export function collectElements(sourceFile: SourceFile, cancellationToken: CancellationToken): OutliningSpan[] {
const elements: OutliningSpan[] = [];
let depth = 0;
const regions: RegionRange[] = [];
const regionText = "#region";
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe rename to regionDelimiter or regionStartDelimter? It's a bit confusing when reading isRegionStart below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to "defaultLabel"

return "";
}

function isRegionEnd(start: number, end: number) {
Copy link
Contributor

Choose a reason for hiding this comment

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

does this return a boolean? Please rename if not, or coerce the result to a boolean via !!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Coerced.

@@ -2037,6 +2037,10 @@ namespace ts {
end: -1;
}

export interface RegionRange extends TextRange {
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be used in the definition of OutliningSpan now?

Copy link
Contributor

Choose a reason for hiding this comment

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

also please move this to services/types.ts instead.

Copy link
Contributor

Choose a reason for hiding this comment

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

i see now this is only used internally while building the spans. then this should move to src/services/outliningElementsCollector.ts and not be exported.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved.

const currentLineStart = lineStarts[i];
const lineEnd = lineStarts[i + 1] - 1 || sourceFile.getEnd();

const name = getRegionName(currentLineStart, lineEnd);
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like you are calling isInComment a few times every line. I would say this is more expensive than the other checks, which is basically does the line start with // or not.

consider making this check first. then after that run your regexp, and if that all passes, then call isInComment before adding the region.

Copy link
Contributor

@mhegazy mhegazy Sep 15, 2017

Choose a reason for hiding this comment

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

Also consider combining your regexps into one. this way you run the regexp and gets you name + end/start marker.

In other words, combine isRegionEnd and getRegionName and inline them in this loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Combined the regexp and moved the isInComment after we know it's a relevant line.

@@ -35,6 +44,19 @@ namespace ts.OutliningElementsCollector {
}
}

function addOutliningSpanRegions(regionSpan: RegionRange) {
if (regionSpan) {
Copy link
Contributor

Choose a reason for hiding this comment

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

why not just create OutliningSpans for every region when we are building them instead of creating the extra object. less garbage to collect later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@@ -2,13 +2,17 @@
namespace ts.OutliningElementsCollector {
const collapseText = "...";
const maxDepth = 20;
const defaultLabel = "#region";
const regionMatch = new RegExp("^\\s*//\\s*(#region|#endregion)(?:\\s+(.*))?$");
Copy link
Contributor

Choose a reason for hiding this comment

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

consider making end optional, this way you can just check on the truthiness of result[1]
new RegExp("^\\s*//\\s*#(end)?region(?:\\s+(.*))?$");

else {
const region = regions.pop();
if (region) {
const newTextSpan = createTextSpanFromBounds(region.textSpan.start, lineEnd);
Copy link
Contributor

Choose a reason for hiding this comment

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

why not just region.textSpan.length = linEnd -region.textSpan.start; and avoid creating the new object.

@uniqueiniquity uniqueiniquity merged commit 83f2401 into microsoft:master Sep 16, 2017
@HolgerJeromin
Copy link
Contributor

Is this a fix for VS Code, VS 2015 and/or VS2017?

@uniqueiniquity
Copy link
Contributor Author

This is a fix for VS2017 and other editors that gather collapsing regions using the TS language service.
I believe VS Code just released their own version: https://code.visualstudio.com/updates/v1_17#_folding-regions.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants