Skip to content

Commit 4e6e1d1

Browse files
authored
Merge pull request #16657 from aozgaa/formatOnOpenCurly
Format on open curly
2 parents 48f4408 + 25abf8a commit 4e6e1d1

13 files changed

+171
-98
lines changed

src/services/formatting/formatting.ts

Lines changed: 73 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,41 @@ namespace ts.formatting {
9797
}
9898

9999
export function formatOnSemicolon(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] {
100-
return formatOutermostParent(position, SyntaxKind.SemicolonToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon);
100+
const semicolon = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.SemicolonToken, sourceFile);
101+
return formatNodeLines(findOutermostNodeWithinListLevel(semicolon), sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon);
102+
}
103+
104+
export function formatOnOpeningCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] {
105+
const openingCurly = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.OpenBraceToken, sourceFile);
106+
if (!openingCurly) {
107+
return [];
108+
}
109+
const curlyBraceRange = openingCurly.parent;
110+
const outermostNode = findOutermostNodeWithinListLevel(curlyBraceRange);
111+
112+
/**
113+
* We limit the span to end at the opening curly to handle the case where
114+
* the brace matched to that just typed will be incorrect after further edits.
115+
* For example, we could type the opening curly for the following method
116+
* body without brace-matching activated:
117+
* ```
118+
* class C {
119+
* foo()
120+
* }
121+
* ```
122+
* and we wouldn't want to move the closing brace.
123+
*/
124+
const textRange: TextRange = {
125+
pos: getLineStartPositionForPosition(outermostNode.getStart(sourceFile), sourceFile),
126+
end: position
127+
};
128+
129+
return formatSpan(textRange, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnOpeningCurlyBrace);
101130
}
102131

103132
export function formatOnClosingCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] {
104-
return formatOutermostParent(position, SyntaxKind.CloseBraceToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace);
133+
const precedingToken = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.CloseBraceToken, sourceFile);
134+
return formatNodeLines(findOutermostNodeWithinListLevel(precedingToken), sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace);
105135
}
106136

107137
export function formatDocument(sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] {
@@ -121,44 +151,36 @@ namespace ts.formatting {
121151
return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatSelection);
122152
}
123153

124-
function formatOutermostParent(position: number, expectedLastToken: SyntaxKind, sourceFile: SourceFile, options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] {
125-
const parent = findOutermostParent(position, expectedLastToken, sourceFile);
126-
if (!parent) {
127-
return [];
128-
}
129-
const span = {
130-
pos: getLineStartPositionForPosition(parent.getStart(sourceFile), sourceFile),
131-
end: parent.end
132-
};
133-
return formatSpan(span, sourceFile, options, rulesProvider, requestKind);
134-
}
154+
/**
155+
* Validating `expectedTokenKind` ensures the token was typed in the context we expect (eg: not a comment).
156+
* @param expectedTokenKind The kind of the last token constituting the desired parent node.
157+
*/
158+
function findImmediatelyPrecedingTokenOfKind(end: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node | undefined {
159+
const precedingToken = findPrecedingToken(end, sourceFile);
135160

136-
function findOutermostParent(position: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node {
137-
const precedingToken = findPrecedingToken(position, sourceFile);
138-
139-
// when it is claimed that trigger character was typed at given position
140-
// we verify that there is a token with a matching kind whose end is equal to position (because the character was just typed).
141-
// If this condition is not hold - then trigger character was typed in some other context,
142-
// i.e.in comment and thus should not trigger autoformatting
143-
if (!precedingToken ||
144-
precedingToken.kind !== expectedTokenKind ||
145-
position !== precedingToken.getEnd()) {
146-
return undefined;
147-
}
161+
return precedingToken && precedingToken.kind === expectedTokenKind && end === precedingToken.getEnd() ?
162+
precedingToken :
163+
undefined;
164+
}
148165

149-
// walk up and search for the parent node that ends at the same position with precedingToken.
150-
// for cases like this
151-
//
152-
// let x = 1;
153-
// while (true) {
154-
// }
155-
// after typing close curly in while statement we want to reformat just the while statement.
156-
// However if we just walk upwards searching for the parent that has the same end value -
157-
// we'll end up with the whole source file. isListElement allows to stop on the list element level
158-
let current = precedingToken;
166+
/**
167+
* Finds the highest node enclosing `node` at the same list level as `node`
168+
* and whose end does not exceed `node.end`.
169+
*
170+
* Consider typing the following
171+
* ```
172+
* let x = 1;
173+
* while (true) {
174+
* }
175+
* ```
176+
* Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding
177+
* variable declaration.
178+
*/
179+
function findOutermostNodeWithinListLevel(node: Node) {
180+
let current = node;
159181
while (current &&
160182
current.parent &&
161-
current.parent.end === precedingToken.end &&
183+
current.parent.end === node.end &&
162184
!isListElement(current.parent, current)) {
163185
current = current.parent;
164186
}
@@ -315,7 +337,7 @@ namespace ts.formatting {
315337
}
316338

317339
/* @internal */
318-
export function formatNode(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, rulesProvider: RulesProvider): TextChange[] {
340+
export function formatNodeGivenIndentation(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, rulesProvider: RulesProvider): TextChange[] {
319341
const range = { pos: 0, end: sourceFileLike.text.length };
320342
return formatSpanWorker(
321343
range,
@@ -330,6 +352,19 @@ namespace ts.formatting {
330352
sourceFileLike);
331353
}
332354

355+
function formatNodeLines(node: Node, sourceFile: SourceFile, options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] {
356+
if (!node) {
357+
return [];
358+
}
359+
360+
const span = {
361+
pos: getLineStartPositionForPosition(node.getStart(sourceFile), sourceFile),
362+
end: node.end
363+
};
364+
365+
return formatSpan(span, sourceFile, options, rulesProvider, requestKind);
366+
}
367+
333368
function formatSpan(originalRange: TextRange,
334369
sourceFile: SourceFile,
335370
options: FormatCodeSettings,
@@ -1096,7 +1131,7 @@ namespace ts.formatting {
10961131
return;
10971132
}
10981133

1099-
// edit should not be applied only if we have one line feed between elements
1134+
// edit should not be applied if we have one line feed between elements
11001135
const lineDelta = currentStartLine - previousStartLine;
11011136
if (lineDelta !== 1) {
11021137
recordReplace(previousRange.end, currentRange.pos - previousRange.end, options.newLineCharacter);

src/services/formatting/formattingRequestKind.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace ts.formatting {
77
FormatSelection,
88
FormatOnEnter,
99
FormatOnSemicolon,
10+
FormatOnOpeningCurlyBrace,
1011
FormatOnClosingCurlyBrace
1112
}
1213
}

src/services/formatting/rules.ts

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -290,15 +290,15 @@ namespace ts.formatting {
290290

291291
// Place a space before open brace in a function declaration
292292
this.FunctionOpenBraceLeftTokenRange = Shared.TokenRange.AnyIncludingMultilineComments;
293-
this.SpaceBeforeOpenBraceInFunction = new Rule(RuleDescriptor.create2(this.FunctionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsFunctionDeclContext, Rules.IsBeforeBlockContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines);
293+
this.SpaceBeforeOpenBraceInFunction = new Rule(RuleDescriptor.create2(this.FunctionOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), Rules.IsFunctionDeclContext, Rules.IsBeforeBlockContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines);
294294

295295
// Place a space before open brace in a TypeScript declaration that has braces as children (class, module, enum, etc)
296296
this.TypeScriptOpenBraceLeftTokenRange = Shared.TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.ClassKeyword, SyntaxKind.ExportKeyword, SyntaxKind.ImportKeyword]);
297-
this.SpaceBeforeOpenBraceInTypeScriptDeclWithBlock = new Rule(RuleDescriptor.create2(this.TypeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsTypeScriptDeclWithBlockContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines);
297+
this.SpaceBeforeOpenBraceInTypeScriptDeclWithBlock = new Rule(RuleDescriptor.create2(this.TypeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForFunctions"), Rules.IsTypeScriptDeclWithBlockContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines);
298298

299299
// Place a space before open brace in a control flow construct
300300
this.ControlOpenBraceLeftTokenRange = Shared.TokenRange.FromTokens([SyntaxKind.CloseParenToken, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.DoKeyword, SyntaxKind.TryKeyword, SyntaxKind.FinallyKeyword, SyntaxKind.ElseKeyword]);
301-
this.SpaceBeforeOpenBraceInControl = new Rule(RuleDescriptor.create2(this.ControlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsControlDeclContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines);
301+
this.SpaceBeforeOpenBraceInControl = new Rule(RuleDescriptor.create2(this.ControlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.isOptionDisabledOrUndefinedOrTokensOnSameLine("placeOpenBraceOnNewLineForControlBlocks"), Rules.IsControlDeclContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines);
302302

303303
// Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}.
304304
this.SpaceAfterOpenBrace = new Rule(RuleDescriptor.create3(SyntaxKind.OpenBraceToken, Shared.TokenRange.Any), RuleOperation.create2(new RuleOperationContext(Rules.IsOptionEnabledOrUndefined("insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces"), Rules.IsBraceWrappedContext), RuleAction.Space));
@@ -585,6 +585,10 @@ namespace ts.formatting {
585585
return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName];
586586
}
587587

588+
static isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean {
589+
return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName] || context.TokensAreOnSameLine();
590+
}
591+
588592
static IsOptionEnabledOrUndefined(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean {
589593
return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !!context.options[optionName];
590594
}
@@ -644,25 +648,8 @@ namespace ts.formatting {
644648
return context.contextNode.kind === SyntaxKind.ConditionalExpression;
645649
}
646650

647-
static IsSameLineTokenOrBeforeMultilineBlockContext(context: FormattingContext): boolean {
648-
//// This check is mainly used inside SpaceBeforeOpenBraceInControl and SpaceBeforeOpenBraceInFunction.
649-
////
650-
//// Ex:
651-
//// if (1) { ....
652-
//// * ) and { are on the same line so apply the rule. Here we don't care whether it's same or multi block context
653-
////
654-
//// Ex:
655-
//// if (1)
656-
//// { ... }
657-
//// * ) and { are on different lines. We only need to format if the block is multiline context. So in this case we don't format.
658-
////
659-
//// Ex:
660-
//// if (1)
661-
//// { ...
662-
//// }
663-
//// * ) and { are on different lines. We only need to format if the block is multiline context. So in this case we format.
664-
665-
return context.TokensAreOnSameLine() || Rules.IsBeforeMultilineBlockContext(context);
651+
static IsSameLineTokenOrBeforeBlockContext(context: FormattingContext): boolean {
652+
return context.TokensAreOnSameLine() || Rules.IsBeforeBlockContext(context);
666653
}
667654

668655
static IsBraceWrappedContext(context: FormattingContext): boolean {

src/services/services.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1762,8 +1762,10 @@ namespace ts {
17621762
function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
17631763
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
17641764
const settings = toEditorSettings(options);
1765-
1766-
if (key === "}") {
1765+
if (key === "{") {
1766+
return formatting.formatOnOpeningCurly(position, sourceFile, getRuleProvider(settings), settings);
1767+
}
1768+
else if (key === "}") {
17671769
return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(settings), settings);
17681770
}
17691771
else if (key === ";") {

src/services/textChanges.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ namespace ts.textChanges {
511511
lineMap,
512512
getLineAndCharacterOfPosition: pos => computeLineAndCharacterOfPosition(lineMap, pos)
513513
};
514-
const changes = formatting.formatNode(nonFormattedText.node, file, sourceFile.languageVariant, initialIndentation, delta, rulesProvider);
514+
const changes = formatting.formatNodeGivenIndentation(nonFormattedText.node, file, sourceFile.languageVariant, initialIndentation, delta, rulesProvider);
515515
return applyChanges(nonFormattedText.text, changes);
516516
}
517517

tests/cases/fourslash/autoFormattingOnPasting.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
/////**/
55
////}
66
goTo.marker("");
7-
edit.paste(" class TestClass{\r\n\
8-
private foo;\r\n\
9-
public testMethod( )\r\n\
10-
{}\r\n\
11-
}");
7+
edit.paste(` class TestClass{
8+
private foo;
9+
public testMethod( )
10+
{}
11+
}`);
1212
// We're missing scenarios of formatting option settings due to bug 693273 - [TypeScript] Need to improve fourslash support for formatting options.
1313
// Missing scenario ** Uncheck Tools->Options->Text Editor->TypeScript->Formatting->General->Format on paste **
1414
//verify.currentFileContentIs("module TestModule {\r\n\
@@ -19,10 +19,9 @@ public testMethod( )\r\n\
1919
//}\r\n\
2020
//}");
2121
// Missing scenario ** Check Tools->Options->Text Editor->TypeScript->Formatting->General->Format on paste **
22-
verify.currentFileContentIs("module TestModule {\r\n\
23-
class TestClass {\r\n\
24-
private foo;\r\n\
25-
public testMethod()\r\n\
26-
{ }\r\n\
27-
}\r\n\
28-
}");
22+
verify.currentFileContentIs(`module TestModule {
23+
class TestClass {
24+
private foo;
25+
public testMethod() { }
26+
}
27+
}`);
Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
///<reference path="fourslash.ts"/>
22

3-
////function f()
3+
////function f1()
44
////{ return 0; }
5+
////function f2()
6+
////{
7+
////return 0;
8+
////}
59
////function g()
610
////{ function h() {
711
////return 0;
812
////}}
913
format.document();
1014
verify.currentFileContentIs(
11-
"function f()\n" +
12-
"{ return 0; }\n" +
13-
"function g() {\n" +
14-
" function h() {\n" +
15-
" return 0;\n" +
16-
" }\n" +
17-
"}"
18-
);
15+
`function f1() { return 0; }
16+
function f2() {
17+
return 0;
18+
}
19+
function g() {
20+
function h() {
21+
return 0;
22+
}
23+
}`);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
//// if(true) {/*0*/}
4+
//// if(false)/*1*/{
5+
//// }
6+
7+
format.setOption("PlaceOpenBraceOnNewLineForControlBlocks", true);
8+
goTo.marker("0");
9+
edit.insertLine("");
10+
verify.currentFileContentIs(
11+
`if (true)
12+
{
13+
}
14+
if(false){
15+
}`);
16+
goTo.marker("1");
17+
edit.insertLine("");
18+
verify.currentFileContentIs(
19+
`if (true)
20+
{
21+
}
22+
if (false)
23+
{
24+
}`);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
//// if(true)
4+
//// /**/ }
5+
6+
format.setOption("PlaceOpenBraceOnNewLineForControlBlocks", false);
7+
goTo.marker("");
8+
edit.insert("{");
9+
verify.currentFileContentIs(
10+
`if (true) { }`);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path="fourslash.ts"/>
2+
3+
//// function foo()
4+
//// {
5+
//// }
6+
//// if (true)
7+
//// {
8+
//// }
9+
10+
format.document();
11+
verify.currentFileContentIs(
12+
`function foo() {
13+
}
14+
if (true) {
15+
}`);

0 commit comments

Comments
 (0)