-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Changes from 8 commits
9726ba1
f91c23b
0b3ec24
0ef5498
4971e31
562d988
fb462c9
442bc56
509d347
c3f2648
7781245
3dfeb2d
484bd20
e5c43cd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe rename to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed to "defaultLabel" |
||
const regionStart = new RegExp("^\\s*//\\s*#region(\\s+.*)?$", "gm"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
What does this do? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed the flags :) they weren't necessary anymore. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Case sensitive? #Closed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. C# is case sensitive, so we're matching them. #Closed |
||
const regionEnd = new RegExp("^\\s*//\\s*#endregion(\\s|$)", "gm"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Exactly one? #Closed There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is there any way to associate the region's identifier with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you can put anything you want there. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed. |
||
|
||
walk(sourceFile); | ||
return elements; | ||
gatherRegions(); | ||
return elements.sort((span1, span2) => span1.textSpan.start - span2.textSpan.start); | ||
|
||
/** If useFullStart is true, then the collapsing span includes leading whitespace, including linebreaks. */ | ||
function addOutliningSpan(hintSpanNode: Node, startElement: Node, endElement: Node, autoCollapse: boolean, useFullStart: boolean) { | ||
|
@@ -35,6 +40,18 @@ namespace ts.OutliningElementsCollector { | |
} | ||
} | ||
|
||
function addOutliningSpanRegions(regionSpan: RegionRange) { | ||
if (regionSpan) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. |
||
const span: OutliningSpan = { | ||
textSpan: createTextSpanFromBounds(regionSpan.pos, regionSpan.end), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Extract common sub-expression? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
hintSpan: createTextSpanFromBounds(regionSpan.pos, regionSpan.end), | ||
bannerText: regionSpan.name, | ||
autoCollapse: false, | ||
}; | ||
elements.push(span); | ||
} | ||
} | ||
|
||
function addOutliningForLeadingCommentsForNode(n: Node) { | ||
const comments = ts.getLeadingCommentRangesOfNode(n, sourceFile); | ||
|
||
|
@@ -89,6 +106,69 @@ namespace ts.OutliningElementsCollector { | |
return isFunctionBlock(node) && node.parent.kind !== SyntaxKind.ArrowFunction; | ||
} | ||
|
||
function isRegionStart(start: number, end: number) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It would appear to be a precondition that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See new approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is a confusing name for a method that returns the name of the region. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed. |
||
if (!ts.formatting.getRangeOfEnclosingComment(sourceFile, start, /*onlyMultiLine*/ true)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is also an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Used isInComment. |
||
const comment = sourceFile.text.substring(start, end); | ||
const result = comment.match(regionStart); | ||
|
||
if (result && result.length > 0) { | ||
const sections = result[0].split(" ").filter(function (s) { return s !== ""; }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why space? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like you want to split on any whitespace. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See new capture group approach. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Arrow expressions are nice in cases like this. |
||
|
||
if (sections[0] === "//") { | ||
if (sections.length > 2) { | ||
return result[0].substring(result[0].indexOf(sections[2])); | ||
} | ||
else { | ||
return regionText; | ||
} | ||
} | ||
else { | ||
if (sections.length > 1) { | ||
return result[0].substring(result[0].indexOf(sections[1])); | ||
} | ||
else { | ||
return regionText; | ||
} | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed to use capture group. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now I've actually pushed the change. :) |
||
} | ||
return ""; | ||
} | ||
|
||
function isRegionEnd(start: number, end: number) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this return a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Coerced. |
||
if (!ts.formatting.getRangeOfEnclosingComment(sourceFile, start, /*onlyMultiLine*/ true)) { | ||
const comment = sourceFile.text.substring(start, end); | ||
return comment.match(regionEnd); | ||
} | ||
return undefined; | ||
} | ||
|
||
function gatherRegions(): void { | ||
const lineStarts = sourceFile.getLineStarts(); | ||
|
||
for (const currentLineStart of lineStarts) { | ||
const lineEnd = sourceFile.getLineEndOfPosition(currentLineStart); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Couldn't you use the start position of the next line? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using start position of next line now. |
||
|
||
const name = isRegionStart(currentLineStart, lineEnd); | ||
if (name) { | ||
const region: RegionRange = { | ||
pos: currentLineStart, | ||
end: lineEnd, | ||
name, | ||
}; | ||
regions.push(region); | ||
} | ||
else if (isRegionEnd(currentLineStart, lineEnd)) { | ||
const region = regions.pop(); | ||
|
||
if (region) { | ||
region.end = lineEnd; | ||
addOutliningSpanRegions(region); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function walk(n: Node): void { | ||
cancellationToken.throwIfCancellationRequested(); | ||
if (depth > maxDepth) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
/// <reference path="fourslash.ts"/> | ||
|
||
////// basic region | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. un-named/anonymous region? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed "region without label" |
||
////[|// #region | ||
//// | ||
////// #endregion|] | ||
//// | ||
////// region with label | ||
////[|// #region label1 | ||
//// | ||
////// #endregion|] | ||
//// | ||
////// region with extra whitespace in all valid locations | ||
////[| // #region label2 label3 | ||
//// | ||
//// // #endregion|] | ||
//// | ||
////// No space before directive | ||
////[|//#region label4 | ||
//// | ||
//////#endregion|] | ||
//// | ||
////// Nested regions | ||
////[|// #region outer | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
//// | ||
////[|// #region inner | ||
//// | ||
////// #endregion inner|] | ||
//// | ||
////// #endregion outer|] | ||
//// | ||
////// region delimiters not valid when preceding text on line | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "when there is"? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. |
||
//// test // #region invalid1 | ||
//// | ||
////test // #endregion | ||
//// | ||
////// region delimiters not valid when in multiline comment | ||
/////* | ||
////// #region invalid2 | ||
////*/ | ||
//// | ||
/////* | ||
////// #endregion | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added unbalanced tests. |
||
////*/ | ||
|
||
verify.outliningSpansInCurrentFile(test.ranges()); |
There was a problem hiding this comment.
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?There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved.