mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-24 20:44:53 -05:00
Merge pull request #16657 from aozgaa/formatOnOpenCurly
Format on open curly
This commit is contained in:
@@ -97,11 +97,41 @@ namespace ts.formatting {
|
||||
}
|
||||
|
||||
export function formatOnSemicolon(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] {
|
||||
return formatOutermostParent(position, SyntaxKind.SemicolonToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon);
|
||||
const semicolon = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.SemicolonToken, sourceFile);
|
||||
return formatNodeLines(findOutermostNodeWithinListLevel(semicolon), sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnSemicolon);
|
||||
}
|
||||
|
||||
export function formatOnOpeningCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] {
|
||||
const openingCurly = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.OpenBraceToken, sourceFile);
|
||||
if (!openingCurly) {
|
||||
return [];
|
||||
}
|
||||
const curlyBraceRange = openingCurly.parent;
|
||||
const outermostNode = findOutermostNodeWithinListLevel(curlyBraceRange);
|
||||
|
||||
/**
|
||||
* We limit the span to end at the opening curly to handle the case where
|
||||
* the brace matched to that just typed will be incorrect after further edits.
|
||||
* For example, we could type the opening curly for the following method
|
||||
* body without brace-matching activated:
|
||||
* ```
|
||||
* class C {
|
||||
* foo()
|
||||
* }
|
||||
* ```
|
||||
* and we wouldn't want to move the closing brace.
|
||||
*/
|
||||
const textRange: TextRange = {
|
||||
pos: getLineStartPositionForPosition(outermostNode.getStart(sourceFile), sourceFile),
|
||||
end: position
|
||||
};
|
||||
|
||||
return formatSpan(textRange, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnOpeningCurlyBrace);
|
||||
}
|
||||
|
||||
export function formatOnClosingCurly(position: number, sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] {
|
||||
return formatOutermostParent(position, SyntaxKind.CloseBraceToken, sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace);
|
||||
const precedingToken = findImmediatelyPrecedingTokenOfKind(position, SyntaxKind.CloseBraceToken, sourceFile);
|
||||
return formatNodeLines(findOutermostNodeWithinListLevel(precedingToken), sourceFile, options, rulesProvider, FormattingRequestKind.FormatOnClosingCurlyBrace);
|
||||
}
|
||||
|
||||
export function formatDocument(sourceFile: SourceFile, rulesProvider: RulesProvider, options: FormatCodeSettings): TextChange[] {
|
||||
@@ -121,44 +151,36 @@ namespace ts.formatting {
|
||||
return formatSpan(span, sourceFile, options, rulesProvider, FormattingRequestKind.FormatSelection);
|
||||
}
|
||||
|
||||
function formatOutermostParent(position: number, expectedLastToken: SyntaxKind, sourceFile: SourceFile, options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] {
|
||||
const parent = findOutermostParent(position, expectedLastToken, sourceFile);
|
||||
if (!parent) {
|
||||
return [];
|
||||
}
|
||||
const span = {
|
||||
pos: getLineStartPositionForPosition(parent.getStart(sourceFile), sourceFile),
|
||||
end: parent.end
|
||||
};
|
||||
return formatSpan(span, sourceFile, options, rulesProvider, requestKind);
|
||||
/**
|
||||
* Validating `expectedTokenKind` ensures the token was typed in the context we expect (eg: not a comment).
|
||||
* @param expectedTokenKind The kind of the last token constituting the desired parent node.
|
||||
*/
|
||||
function findImmediatelyPrecedingTokenOfKind(end: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node | undefined {
|
||||
const precedingToken = findPrecedingToken(end, sourceFile);
|
||||
|
||||
return precedingToken && precedingToken.kind === expectedTokenKind && end === precedingToken.getEnd() ?
|
||||
precedingToken :
|
||||
undefined;
|
||||
}
|
||||
|
||||
function findOutermostParent(position: number, expectedTokenKind: SyntaxKind, sourceFile: SourceFile): Node {
|
||||
const precedingToken = findPrecedingToken(position, sourceFile);
|
||||
|
||||
// when it is claimed that trigger character was typed at given position
|
||||
// we verify that there is a token with a matching kind whose end is equal to position (because the character was just typed).
|
||||
// If this condition is not hold - then trigger character was typed in some other context,
|
||||
// i.e.in comment and thus should not trigger autoformatting
|
||||
if (!precedingToken ||
|
||||
precedingToken.kind !== expectedTokenKind ||
|
||||
position !== precedingToken.getEnd()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// walk up and search for the parent node that ends at the same position with precedingToken.
|
||||
// for cases like this
|
||||
//
|
||||
// let x = 1;
|
||||
// while (true) {
|
||||
// }
|
||||
// after typing close curly in while statement we want to reformat just the while statement.
|
||||
// However if we just walk upwards searching for the parent that has the same end value -
|
||||
// we'll end up with the whole source file. isListElement allows to stop on the list element level
|
||||
let current = precedingToken;
|
||||
/**
|
||||
* Finds the highest node enclosing `node` at the same list level as `node`
|
||||
* and whose end does not exceed `node.end`.
|
||||
*
|
||||
* Consider typing the following
|
||||
* ```
|
||||
* let x = 1;
|
||||
* while (true) {
|
||||
* }
|
||||
* ```
|
||||
* Upon typing the closing curly, we want to format the entire `while`-statement, but not the preceding
|
||||
* variable declaration.
|
||||
*/
|
||||
function findOutermostNodeWithinListLevel(node: Node) {
|
||||
let current = node;
|
||||
while (current &&
|
||||
current.parent &&
|
||||
current.parent.end === precedingToken.end &&
|
||||
current.parent.end === node.end &&
|
||||
!isListElement(current.parent, current)) {
|
||||
current = current.parent;
|
||||
}
|
||||
@@ -315,7 +337,7 @@ namespace ts.formatting {
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function formatNode(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, rulesProvider: RulesProvider): TextChange[] {
|
||||
export function formatNodeGivenIndentation(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, rulesProvider: RulesProvider): TextChange[] {
|
||||
const range = { pos: 0, end: sourceFileLike.text.length };
|
||||
return formatSpanWorker(
|
||||
range,
|
||||
@@ -330,6 +352,19 @@ namespace ts.formatting {
|
||||
sourceFileLike);
|
||||
}
|
||||
|
||||
function formatNodeLines(node: Node, sourceFile: SourceFile, options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] {
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const span = {
|
||||
pos: getLineStartPositionForPosition(node.getStart(sourceFile), sourceFile),
|
||||
end: node.end
|
||||
};
|
||||
|
||||
return formatSpan(span, sourceFile, options, rulesProvider, requestKind);
|
||||
}
|
||||
|
||||
function formatSpan(originalRange: TextRange,
|
||||
sourceFile: SourceFile,
|
||||
options: FormatCodeSettings,
|
||||
@@ -1096,7 +1131,7 @@ namespace ts.formatting {
|
||||
return;
|
||||
}
|
||||
|
||||
// edit should not be applied only if we have one line feed between elements
|
||||
// edit should not be applied if we have one line feed between elements
|
||||
const lineDelta = currentStartLine - previousStartLine;
|
||||
if (lineDelta !== 1) {
|
||||
recordReplace(previousRange.end, currentRange.pos - previousRange.end, options.newLineCharacter);
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace ts.formatting {
|
||||
FormatSelection,
|
||||
FormatOnEnter,
|
||||
FormatOnSemicolon,
|
||||
FormatOnOpeningCurlyBrace,
|
||||
FormatOnClosingCurlyBrace
|
||||
}
|
||||
}
|
||||
@@ -290,15 +290,15 @@ namespace ts.formatting {
|
||||
|
||||
// Place a space before open brace in a function declaration
|
||||
this.FunctionOpenBraceLeftTokenRange = Shared.TokenRange.AnyIncludingMultilineComments;
|
||||
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);
|
||||
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);
|
||||
|
||||
// Place a space before open brace in a TypeScript declaration that has braces as children (class, module, enum, etc)
|
||||
this.TypeScriptOpenBraceLeftTokenRange = Shared.TokenRange.FromTokens([SyntaxKind.Identifier, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.ClassKeyword, SyntaxKind.ExportKeyword, SyntaxKind.ImportKeyword]);
|
||||
this.SpaceBeforeOpenBraceInTypeScriptDeclWithBlock = new Rule(RuleDescriptor.create2(this.TypeScriptOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsTypeScriptDeclWithBlockContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines);
|
||||
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);
|
||||
|
||||
// Place a space before open brace in a control flow construct
|
||||
this.ControlOpenBraceLeftTokenRange = Shared.TokenRange.FromTokens([SyntaxKind.CloseParenToken, SyntaxKind.MultiLineCommentTrivia, SyntaxKind.DoKeyword, SyntaxKind.TryKeyword, SyntaxKind.FinallyKeyword, SyntaxKind.ElseKeyword]);
|
||||
this.SpaceBeforeOpenBraceInControl = new Rule(RuleDescriptor.create2(this.ControlOpenBraceLeftTokenRange, SyntaxKind.OpenBraceToken), RuleOperation.create2(new RuleOperationContext(Rules.IsControlDeclContext, Rules.IsNotFormatOnEnter, Rules.IsSameLineTokenOrBeforeMultilineBlockContext), RuleAction.Space), RuleFlags.CanDeleteNewLines);
|
||||
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);
|
||||
|
||||
// Insert a space after { and before } in single-line contexts, but remove space from empty object literals {}.
|
||||
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 {
|
||||
return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName];
|
||||
}
|
||||
|
||||
static isOptionDisabledOrUndefinedOrTokensOnSameLine(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean {
|
||||
return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !context.options[optionName] || context.TokensAreOnSameLine();
|
||||
}
|
||||
|
||||
static IsOptionEnabledOrUndefined(optionName: keyof FormatCodeSettings): (context: FormattingContext) => boolean {
|
||||
return (context) => !context.options || !context.options.hasOwnProperty(optionName) || !!context.options[optionName];
|
||||
}
|
||||
@@ -644,25 +648,8 @@ namespace ts.formatting {
|
||||
return context.contextNode.kind === SyntaxKind.ConditionalExpression;
|
||||
}
|
||||
|
||||
static IsSameLineTokenOrBeforeMultilineBlockContext(context: FormattingContext): boolean {
|
||||
//// This check is mainly used inside SpaceBeforeOpenBraceInControl and SpaceBeforeOpenBraceInFunction.
|
||||
////
|
||||
//// Ex:
|
||||
//// if (1) { ....
|
||||
//// * ) and { are on the same line so apply the rule. Here we don't care whether it's same or multi block context
|
||||
////
|
||||
//// Ex:
|
||||
//// if (1)
|
||||
//// { ... }
|
||||
//// * ) and { are on different lines. We only need to format if the block is multiline context. So in this case we don't format.
|
||||
////
|
||||
//// Ex:
|
||||
//// if (1)
|
||||
//// { ...
|
||||
//// }
|
||||
//// * ) and { are on different lines. We only need to format if the block is multiline context. So in this case we format.
|
||||
|
||||
return context.TokensAreOnSameLine() || Rules.IsBeforeMultilineBlockContext(context);
|
||||
static IsSameLineTokenOrBeforeBlockContext(context: FormattingContext): boolean {
|
||||
return context.TokensAreOnSameLine() || Rules.IsBeforeBlockContext(context);
|
||||
}
|
||||
|
||||
static IsBraceWrappedContext(context: FormattingContext): boolean {
|
||||
|
||||
@@ -1762,8 +1762,10 @@ namespace ts {
|
||||
function getFormattingEditsAfterKeystroke(fileName: string, position: number, key: string, options: FormatCodeOptions | FormatCodeSettings): TextChange[] {
|
||||
const sourceFile = syntaxTreeCache.getCurrentSourceFile(fileName);
|
||||
const settings = toEditorSettings(options);
|
||||
|
||||
if (key === "}") {
|
||||
if (key === "{") {
|
||||
return formatting.formatOnOpeningCurly(position, sourceFile, getRuleProvider(settings), settings);
|
||||
}
|
||||
else if (key === "}") {
|
||||
return formatting.formatOnClosingCurly(position, sourceFile, getRuleProvider(settings), settings);
|
||||
}
|
||||
else if (key === ";") {
|
||||
|
||||
@@ -511,7 +511,7 @@ namespace ts.textChanges {
|
||||
lineMap,
|
||||
getLineAndCharacterOfPosition: pos => computeLineAndCharacterOfPosition(lineMap, pos)
|
||||
};
|
||||
const changes = formatting.formatNode(nonFormattedText.node, file, sourceFile.languageVariant, initialIndentation, delta, rulesProvider);
|
||||
const changes = formatting.formatNodeGivenIndentation(nonFormattedText.node, file, sourceFile.languageVariant, initialIndentation, delta, rulesProvider);
|
||||
return applyChanges(nonFormattedText.text, changes);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user