diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 356eb54165e2b..80b32deebb501 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -842,6 +842,7 @@ namespace ts { let tempFlags: TempFlags; // TempFlags for the current name generation scope. let reservedNamesStack: Map[]; // Stack of TempFlags reserved in enclosing name generation scopes. let reservedNames: Map; // TempFlags to reserve in nested name generation scopes. + let preserveSourceNewlines = printerOptions.preserveSourceNewlines; // Can be overridden inside nodes with the `IgnoreSourceNewlines` emit flag. let writer: EmitTextWriter; let ownWriter: EmitTextWriter; // Reusable `EmitTextWriter` for basic printing. @@ -1164,8 +1165,12 @@ namespace ts { function pipelineEmit(emitHint: EmitHint, node: Node) { const savedLastNode = lastNode; const savedLastSubstitution = lastSubstitution; + const savedPreserveSourceNewlines = preserveSourceNewlines; lastNode = node; lastSubstitution = undefined; + if (preserveSourceNewlines && !!(getEmitFlags(node) & EmitFlags.IgnoreSourceNewlines)) { + preserveSourceNewlines = false; + } const pipelinePhase = getPipelinePhase(PipelinePhase.Notification, emitHint, node); pipelinePhase(emitHint, node); @@ -1175,6 +1180,7 @@ namespace ts { const substitute = lastSubstitution; lastNode = savedLastNode; lastSubstitution = savedLastSubstitution; + preserveSourceNewlines = savedPreserveSourceNewlines; return substitute || node; } @@ -2274,10 +2280,10 @@ namespace ts { function emitPropertyAccessExpression(node: PropertyAccessExpression) { const expression = cast(emitExpression(node.expression), isExpression); const token = node.questionDotToken || createNode(SyntaxKind.DotToken, node.expression.end, node.name.pos) as DotToken; - const indentBeforeDot = needsIndentation(node, node.expression, token); - const indentAfterDot = needsIndentation(node, token, node.name); + const linesBeforeDot = getLinesBetweenNodes(node, node.expression, token); + const linesAfterDot = getLinesBetweenNodes(node, token, node.name); - increaseIndentIf(indentBeforeDot, /*writeSpaceIfNotIndenting*/ false); + writeLinesAndIndent(linesBeforeDot, /*writeSpaceIfNotIndenting*/ false); const shouldEmitDotDot = token.kind !== SyntaxKind.QuestionDotToken && @@ -2295,9 +2301,9 @@ namespace ts { else { emitTokenWithComment(token.kind, node.expression.end, writePunctuation, node); } - increaseIndentIf(indentAfterDot, /*writeSpaceIfNotIndenting*/ false); + writeLinesAndIndent(linesAfterDot, /*writeSpaceIfNotIndenting*/ false); emit(node.name); - decreaseIndentIf(indentBeforeDot, indentAfterDot); + decreaseIndentIf(linesBeforeDot, linesAfterDot); } // 1..toString is a valid property access, emit a dot after the literal @@ -2359,7 +2365,16 @@ namespace ts { function emitParenthesizedExpression(node: ParenthesizedExpression) { const openParenPos = emitTokenWithComment(SyntaxKind.OpenParenToken, node.pos, writePunctuation, node); + const leadingNewlines = preserveSourceNewlines && getLeadingLineTerminatorCount(node, [node.expression], ListFormat.None); + if (leadingNewlines) { + writeLinesAndIndent(leadingNewlines, /*writeLinesIfNotIndenting*/ false); + } emitExpression(node.expression); + const trailingNewlines = preserveSourceNewlines && getClosingLineTerminatorCount(node, [node.expression], ListFormat.None); + if (trailingNewlines) { + writeLine(trailingNewlines); + } + decreaseIndentIf(leadingNewlines); emitTokenWithComment(SyntaxKind.CloseParenToken, node.expression ? node.expression.end : openParenPos, writePunctuation, node); } @@ -2462,20 +2477,20 @@ namespace ts { } case EmitBinaryExpressionState.EmitRight: { const isCommaOperator = node.operatorToken.kind !== SyntaxKind.CommaToken; - const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken); - const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right); - increaseIndentIf(indentBeforeOperator, isCommaOperator); + const linesBeforeOperator = getLinesBetweenNodes(node, node.left, node.operatorToken); + const linesAfterOperator = getLinesBetweenNodes(node, node.operatorToken, node.right); + writeLinesAndIndent(linesBeforeOperator, isCommaOperator); emitLeadingCommentsOfPosition(node.operatorToken.pos); writeTokenNode(node.operatorToken, node.operatorToken.kind === SyntaxKind.InKeyword ? writeKeyword : writeOperator); emitTrailingCommentsOfPosition(node.operatorToken.end, /*prefixSpace*/ true); // Binary operators should have a space before the comment starts - increaseIndentIf(indentAfterOperator, /*writeSpaceIfNotIndenting*/ true); + writeLinesAndIndent(linesAfterOperator, /*writeSpaceIfNotIndenting*/ true); maybePipelineEmitExpression(node.right); break; } case EmitBinaryExpressionState.FinishEmit: { - const indentBeforeOperator = needsIndentation(node, node.left, node.operatorToken); - const indentAfterOperator = needsIndentation(node, node.operatorToken, node.right); - decreaseIndentIf(indentBeforeOperator, indentAfterOperator); + const linesBeforeOperator = getLinesBetweenNodes(node, node.left, node.operatorToken); + const linesAfterOperator = getLinesBetweenNodes(node, node.operatorToken, node.right); + decreaseIndentIf(linesBeforeOperator, linesAfterOperator); stackIndex--; break; } @@ -2519,23 +2534,23 @@ namespace ts { } function emitConditionalExpression(node: ConditionalExpression) { - const indentBeforeQuestion = needsIndentation(node, node.condition, node.questionToken); - const indentAfterQuestion = needsIndentation(node, node.questionToken, node.whenTrue); - const indentBeforeColon = needsIndentation(node, node.whenTrue, node.colonToken); - const indentAfterColon = needsIndentation(node, node.colonToken, node.whenFalse); + const linesBeforeQuestion = getLinesBetweenNodes(node, node.condition, node.questionToken); + const linesAfterQuestion = getLinesBetweenNodes(node, node.questionToken, node.whenTrue); + const linesBeforeColon = getLinesBetweenNodes(node, node.whenTrue, node.colonToken); + const linesAfterColon = getLinesBetweenNodes(node, node.colonToken, node.whenFalse); emitExpression(node.condition); - increaseIndentIf(indentBeforeQuestion, /*writeSpaceIfNotIndenting*/ true); + writeLinesAndIndent(linesBeforeQuestion, /*writeSpaceIfNotIndenting*/ true); emit(node.questionToken); - increaseIndentIf(indentAfterQuestion, /*writeSpaceIfNotIndenting*/ true); + writeLinesAndIndent(linesAfterQuestion, /*writeSpaceIfNotIndenting*/ true); emitExpression(node.whenTrue); - decreaseIndentIf(indentBeforeQuestion, indentAfterQuestion); + decreaseIndentIf(linesBeforeQuestion, linesAfterQuestion); - increaseIndentIf(indentBeforeColon, /*writeSpaceIfNotIndenting*/ true); + writeLinesAndIndent(linesBeforeColon, /*writeSpaceIfNotIndenting*/ true); emit(node.colonToken); - increaseIndentIf(indentAfterColon, /*writeSpaceIfNotIndenting*/ true); + writeLinesAndIndent(linesAfterColon, /*writeSpaceIfNotIndenting*/ true); emitExpression(node.whenFalse); - decreaseIndentIf(indentBeforeColon, indentAfterColon); + decreaseIndentIf(linesBeforeColon, linesAfterColon); } function emitTemplateExpression(node: TemplateExpression) { @@ -2930,14 +2945,14 @@ namespace ts { return false; } - if (shouldWriteLeadingLineTerminator(body, body.statements, ListFormat.PreserveLines) - || shouldWriteClosingLineTerminator(body, body.statements, ListFormat.PreserveLines)) { + if (getLeadingLineTerminatorCount(body, body.statements, ListFormat.PreserveLines) + || getClosingLineTerminatorCount(body, body.statements, ListFormat.PreserveLines)) { return false; } let previousStatement: Statement | undefined; for (const statement of body.statements) { - if (shouldWriteSeparatingLineTerminator(previousStatement, statement, ListFormat.PreserveLines)) { + if (getSeparatingLineTerminatorCount(previousStatement, statement, ListFormat.PreserveLines) > 0) { return false; } @@ -3997,7 +4012,7 @@ namespace ts { if (isEmpty) { // Write a line terminator if the parent node was multi-line - if (format & ListFormat.MultiLine) { + if (format & ListFormat.MultiLine && !(preserveSourceNewlines && rangeIsOnSingleLine(parentNode, currentSourceFile!))) { writeLine(); } else if (format & ListFormat.SpaceBetweenBraces && !(format & ListFormat.NoSpaceIfEmpty)) { @@ -4008,8 +4023,9 @@ namespace ts { // Write the opening line terminator or leading whitespace. const mayEmitInterveningComments = (format & ListFormat.NoInterveningComments) === 0; let shouldEmitInterveningComments = mayEmitInterveningComments; - if (shouldWriteLeadingLineTerminator(parentNode, children!, format)) { // TODO: GH#18217 - writeLine(); + const leadingLineTerminatorCount = getLeadingLineTerminatorCount(parentNode, children!, format); // TODO: GH#18217 + if (leadingLineTerminatorCount) { + writeLine(leadingLineTerminatorCount); shouldEmitInterveningComments = false; } else if (format & ListFormat.SpaceBetweenBraces) { @@ -4048,7 +4064,8 @@ namespace ts { recordBundleFileInternalSectionEnd(previousSourceFileTextKind); // Write either a line terminator or whitespace to separate the elements. - if (shouldWriteSeparatingLineTerminator(previousSibling, child, format)) { + const separatingLineTerminatorCount = getSeparatingLineTerminatorCount(previousSibling, child, format); + if (separatingLineTerminatorCount > 0) { // If a synthesized node in a single-line list starts on a new // line, we should increase the indent. if ((format & (ListFormat.LinesMask | ListFormat.Indented)) === ListFormat.SingleLine) { @@ -4056,7 +4073,7 @@ namespace ts { shouldDecreaseIndentAfterEmit = true; } - writeLine(); + writeLine(separatingLineTerminatorCount); shouldEmitInterveningComments = false; } else if (previousSibling && format & ListFormat.SpaceBetweenSiblings) { @@ -4111,8 +4128,9 @@ namespace ts { recordBundleFileInternalSectionEnd(previousSourceFileTextKind); // Write the closing line terminator or closing whitespace. - if (shouldWriteClosingLineTerminator(parentNode, children!, format)) { - writeLine(); + const closingLineTerminatorCount = getClosingLineTerminatorCount(parentNode, children!, format); + if (closingLineTerminatorCount) { + writeLine(closingLineTerminatorCount); } else if (format & ListFormat.SpaceBetweenBraces) { writeSpace(); @@ -4182,8 +4200,10 @@ namespace ts { writer.writeProperty(s); } - function writeLine() { - writer.writeLine(); + function writeLine(count = 1) { + for (let i = 0; i < count; i++) { + writer.writeLine(i > 0); + } } function increaseIndent() { @@ -4239,10 +4259,10 @@ namespace ts { } } - function increaseIndentIf(value: boolean, writeSpaceIfNotIndenting: boolean) { - if (value) { + function writeLinesAndIndent(lineCount: number, writeSpaceIfNotIndenting: boolean) { + if (lineCount) { increaseIndent(); - writeLine(); + writeLine(lineCount); } else if (writeSpaceIfNotIndenting) { writeSpace(); @@ -4253,7 +4273,7 @@ namespace ts { // previous indent values to be considered at a time. This also allows caller to just // call this once, passing in all their appropriate indent values, instead of needing // to call this helper function multiple times. - function decreaseIndentIf(value1: boolean, value2: boolean) { + function decreaseIndentIf(value1: boolean | number | undefined, value2?: boolean | number) { if (value1) { decreaseIndent(); } @@ -4262,75 +4282,119 @@ namespace ts { } } - function shouldWriteLeadingLineTerminator(parentNode: TextRange, children: NodeArray, format: ListFormat) { - if (format & ListFormat.MultiLine) { - return true; - } - - if (format & ListFormat.PreserveLines) { + function getLeadingLineTerminatorCount(parentNode: TextRange, children: readonly Node[], format: ListFormat): number { + if (format & ListFormat.PreserveLines || preserveSourceNewlines) { if (format & ListFormat.PreferNewLine) { - return true; + return 1; } const firstChild = children[0]; if (firstChild === undefined) { - return !rangeIsOnSingleLine(parentNode, currentSourceFile!); + return rangeIsOnSingleLine(parentNode, currentSourceFile!) ? 0 : 1; } - else if (positionIsSynthesized(parentNode.pos) || nodeIsSynthesized(firstChild)) { - return synthesizedNodeStartsOnNewLine(firstChild, format); + if (firstChild.kind === SyntaxKind.JsxText) { + // JsxText will be written with its leading whitespace, so don't add more manually. + return 0; } - else { - return !rangeStartPositionsAreOnSameLine(parentNode, firstChild, currentSourceFile!); + if (!positionIsSynthesized(parentNode.pos) && !nodeIsSynthesized(firstChild) && firstChild.parent === parentNode) { + if (preserveSourceNewlines) { + return getEffectiveLines( + includeComments => getLinesBetweenPositionAndPrecedingNonWhitespaceCharacter( + firstChild.pos, + currentSourceFile!, + includeComments)); + } + return rangeStartPositionsAreOnSameLine(parentNode, firstChild, currentSourceFile!) ? 0 : 1; + } + if (synthesizedNodeStartsOnNewLine(firstChild, format)) { + return 1; } } - else { - return false; - } + return format & ListFormat.MultiLine ? 1 : 0; } - function shouldWriteSeparatingLineTerminator(previousNode: Node | undefined, nextNode: Node, format: ListFormat) { - if (format & ListFormat.MultiLine) { - return true; - } - else if (format & ListFormat.PreserveLines) { + function getSeparatingLineTerminatorCount(previousNode: Node | undefined, nextNode: Node, format: ListFormat): number { + if (format & ListFormat.PreserveLines || preserveSourceNewlines) { if (previousNode === undefined || nextNode === undefined) { - return false; + return 0; } - else if (nodeIsSynthesized(previousNode) || nodeIsSynthesized(nextNode)) { - return synthesizedNodeStartsOnNewLine(previousNode, format) || synthesizedNodeStartsOnNewLine(nextNode, format); + if (nextNode.kind === SyntaxKind.JsxText) { + // JsxText will be written with its leading whitespace, so don't add more manually. + return 0; } - else { - return !rangeEndIsOnSameLineAsRangeStart(previousNode, nextNode, currentSourceFile!); + else if (!nodeIsSynthesized(previousNode) && !nodeIsSynthesized(nextNode) && previousNode.parent === nextNode.parent) { + if (preserveSourceNewlines) { + return getEffectiveLines( + includeComments => getLinesBetweenRangeEndAndRangeStart( + previousNode, + nextNode, + currentSourceFile!, + includeComments)); + } + return rangeEndIsOnSameLineAsRangeStart(previousNode, nextNode, currentSourceFile!) ? 0 : 1; + } + else if (synthesizedNodeStartsOnNewLine(previousNode, format) || synthesizedNodeStartsOnNewLine(nextNode, format)) { + return 1; } } - else { - return getStartsOnNewLine(nextNode); + else if (getStartsOnNewLine(nextNode)) { + return 1; } + return format & ListFormat.MultiLine ? 1 : 0; } - function shouldWriteClosingLineTerminator(parentNode: TextRange, children: NodeArray, format: ListFormat) { - if (format & ListFormat.MultiLine) { - return (format & ListFormat.NoTrailingNewLine) === 0; - } - else if (format & ListFormat.PreserveLines) { + function getClosingLineTerminatorCount(parentNode: TextRange, children: readonly Node[], format: ListFormat): number { + if (format & ListFormat.PreserveLines || preserveSourceNewlines) { if (format & ListFormat.PreferNewLine) { - return true; + return 1; } const lastChild = lastOrUndefined(children); if (lastChild === undefined) { - return !rangeIsOnSingleLine(parentNode, currentSourceFile!); + return rangeIsOnSingleLine(parentNode, currentSourceFile!) ? 0 : 1; } - else if (positionIsSynthesized(parentNode.pos) || nodeIsSynthesized(lastChild)) { - return synthesizedNodeStartsOnNewLine(lastChild, format); + if (!positionIsSynthesized(parentNode.pos) && !nodeIsSynthesized(lastChild) && lastChild.parent === parentNode) { + if (preserveSourceNewlines) { + return getEffectiveLines( + includeComments => getLinesBetweenPositionAndNextNonWhitespaceCharacter( + lastChild.end, + currentSourceFile!, + includeComments)); + } + return rangeEndPositionsAreOnSameLine(parentNode, lastChild, currentSourceFile!) ? 0 : 1; } - else { - return !rangeEndPositionsAreOnSameLine(parentNode, lastChild, currentSourceFile!); + if (synthesizedNodeStartsOnNewLine(lastChild, format)) { + return 1; } } - else { - return false; + if (format & ListFormat.MultiLine && !(format & ListFormat.NoTrailingNewLine)) { + return 1; } + return 0; + } + + function getEffectiveLines(getLineDifference: (includeComments: boolean) => number) { + // If 'preserveSourceNewlines' is disabled, we should never call this function + // because it could be more expensive than alternative approximations. + Debug.assert(!!preserveSourceNewlines); + // We start by measuring the line difference from a position to its adjacent comments, + // so that this is counted as a one-line difference, not two: + // + // node1; + // // NODE2 COMMENT + // node2; + const lines = getLineDifference(/*includeComments*/ true); + if (lines === 0) { + // However, if the line difference considering comments was 0, we might have this: + // + // node1; // NODE2 COMMENT + // node2; + // + // in which case we should be ignoring node2's comment, so this too is counted as + // a one-line difference, not zero. + return getLineDifference(/*includeComments*/ false); + } + return lines; } function synthesizedNodeStartsOnNewLine(node: Node, format: ListFormat) { @@ -4346,9 +4410,9 @@ namespace ts { return (format & ListFormat.PreferNewLine) !== 0; } - function needsIndentation(parent: Node, node1: Node, node2: Node): boolean { + function getLinesBetweenNodes(parent: Node, node1: Node, node2: Node): number { if (getEmitFlags(parent) & EmitFlags.NoIndentation) { - return false; + return 0; } parent = skipSynthesizedParentheses(parent); @@ -4357,13 +4421,22 @@ namespace ts { // Always use a newline for synthesized code if the synthesizer desires it. if (getStartsOnNewLine(node2)) { - return true; + return 1; + } + + if (!nodeIsSynthesized(parent) && !nodeIsSynthesized(node1) && !nodeIsSynthesized(node2)) { + if (preserveSourceNewlines) { + return getEffectiveLines( + includeComments => getLinesBetweenRangeEndAndRangeStart( + node1, + node2, + currentSourceFile!, + includeComments)); + } + return rangeEndIsOnSameLineAsRangeStart(node1, node2, currentSourceFile!) ? 0 : 1; } - return !nodeIsSynthesized(parent) - && !nodeIsSynthesized(node1) - && !nodeIsSynthesized(node2) - && !rangeEndIsOnSameLineAsRangeStart(node1, node2, currentSourceFile!); + return 0; } function isEmptyBlock(block: BlockLike) { diff --git a/src/compiler/factoryPublic.ts b/src/compiler/factoryPublic.ts index 04ec83a622231..b20653fd92926 100644 --- a/src/compiler/factoryPublic.ts +++ b/src/compiler/factoryPublic.ts @@ -3561,6 +3561,12 @@ namespace ts { return node; } + /** @internal */ + export function ignoreSourceNewlines(node: T): T { + getOrCreateEmitNode(node).flags |= EmitFlags.IgnoreSourceNewlines; + return node; + } + /** * Gets the constant value to emit for an expression. */ diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index ab08af376d1bb..856c6dff3b686 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -409,11 +409,20 @@ namespace ts { } /* @internal */ + export function computeLineAndCharacterOfPosition(lineStarts: readonly number[], position: number): LineAndCharacter { + const lineNumber = computeLineOfPosition(lineStarts, position); + return { + line: lineNumber, + character: position - lineStarts[lineNumber] + }; + } + /** + * @internal * We assume the first line starts at position 0 and 'position' is non-negative. */ - export function computeLineAndCharacterOfPosition(lineStarts: readonly number[], position: number): LineAndCharacter { - let lineNumber = binarySearch(lineStarts, position, identity, compareValues); + export function computeLineOfPosition(lineStarts: readonly number[], position: number, lowerBound?: number) { + let lineNumber = binarySearch(lineStarts, position, identity, compareValues, lowerBound); if (lineNumber < 0) { // If the actual position was not found, // the binary search returns the 2's-complement of the next line start @@ -425,10 +434,19 @@ namespace ts { lineNumber = ~lineNumber - 1; Debug.assert(lineNumber !== -1, "position cannot precede the beginning of the file"); } - return { - line: lineNumber, - character: position - lineStarts[lineNumber] - }; + return lineNumber; + } + + /** @internal */ + export function getLinesBetweenPositions(sourceFile: SourceFileLike, pos1: number, pos2: number) { + if (pos1 === pos2) return 0; + const lineStarts = getLineStarts(sourceFile); + const lower = Math.min(pos1, pos2); + const isNegative = lower === pos2; + const upper = isNegative ? pos1 : pos2; + const lowerLine = computeLineOfPosition(lineStarts, lower); + const upperLine = computeLineOfPosition(lineStarts, upper, lowerLine); + return isNegative ? lowerLine - upperLine : upperLine - lowerLine; } export function getLineAndCharacterOfPosition(sourceFile: SourceFileLike, position: number): LineAndCharacter { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 6543d8904f604..d7c67db0d777a 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3781,7 +3781,7 @@ namespace ts { writeParameter(text: string): void; writeProperty(text: string): void; writeSymbol(text: string, symbol: Symbol): void; - writeLine(): void; + writeLine(force?: boolean): void; increaseIndent(): void; decreaseIndent(): void; clear(): void; @@ -5857,6 +5857,7 @@ namespace ts { NoAsciiEscaping = 1 << 24, // When synthesizing nodes that lack an original node or textSourceNode, we want to write the text on the node with ASCII escaping substitutions. /*@internal*/ TypeScriptClassWrapper = 1 << 25, // The node is an IIFE class wrapper created by the ts transform. /*@internal*/ NeverApplyImportHelper = 1 << 26, // Indicates the node should never be wrapped with an import star helper (because, for example, it imports tslib itself) + /*@internal*/ IgnoreSourceNewlines = 1 << 27, // Overrides `printerOptions.preserveSourceNewlines` to print this node (and all descendants) with default whitespace. } export interface EmitHelper { @@ -6321,6 +6322,7 @@ namespace ts { /*@internal*/ writeBundleFileInfo?: boolean; /*@internal*/ recordInternalSection?: boolean; /*@internal*/ stripInternal?: boolean; + /*@internal*/ preserveSourceNewlines?: boolean; /*@internal*/ relativeToBuildInfo?: (path: string) => string; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 65c2b4d4407bd..34f80556d9c8d 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -3633,8 +3633,8 @@ namespace ts { } } - function writeLine() { - if (!lineStart) { + function writeLine(force?: boolean) { + if (!lineStart || force) { output += newLine; lineCount++; linePos = output.length; @@ -3905,12 +3905,13 @@ namespace ts { } } - export function getLineOfLocalPosition(currentSourceFile: SourceFile, pos: number) { - return getLineAndCharacterOfPosition(currentSourceFile, pos).line; + export function getLineOfLocalPosition(sourceFile: SourceFile, pos: number) { + const lineStarts = getLineStarts(sourceFile); + return computeLineOfPosition(lineStarts, pos); } export function getLineOfLocalPositionFromLineMap(lineMap: readonly number[], pos: number) { - return computeLineAndCharacterOfPosition(lineMap, pos).line; + return computeLineOfPosition(lineMap, pos); } export function getFirstConstructorWithBody(node: ClassLikeDeclaration): ConstructorDeclaration & { body: FunctionBody } | undefined { @@ -4735,7 +4736,10 @@ namespace ts { } export function rangeStartPositionsAreOnSameLine(range1: TextRange, range2: TextRange, sourceFile: SourceFile) { - return positionsAreOnSameLine(getStartPositionOfRange(range1, sourceFile), getStartPositionOfRange(range2, sourceFile), sourceFile); + return positionsAreOnSameLine( + getStartPositionOfRange(range1, sourceFile, /*includeComments*/ false), + getStartPositionOfRange(range2, sourceFile, /*includeComments*/ false), + sourceFile); } export function rangeEndPositionsAreOnSameLine(range1: TextRange, range2: TextRange, sourceFile: SourceFile) { @@ -4743,11 +4747,20 @@ namespace ts { } export function rangeStartIsOnSameLineAsRangeEnd(range1: TextRange, range2: TextRange, sourceFile: SourceFile) { - return positionsAreOnSameLine(getStartPositionOfRange(range1, sourceFile), range2.end, sourceFile); + return positionsAreOnSameLine(getStartPositionOfRange(range1, sourceFile, /*includeComments*/ false), range2.end, sourceFile); } export function rangeEndIsOnSameLineAsRangeStart(range1: TextRange, range2: TextRange, sourceFile: SourceFile) { - return positionsAreOnSameLine(range1.end, getStartPositionOfRange(range2, sourceFile), sourceFile); + return positionsAreOnSameLine(range1.end, getStartPositionOfRange(range2, sourceFile, /*includeComments*/ false), sourceFile); + } + + export function getLinesBetweenRangeEndAndRangeStart(range1: TextRange, range2: TextRange, sourceFile: SourceFile, includeSecondRangeComments: boolean) { + const range2Start = getStartPositionOfRange(range2, sourceFile, includeSecondRangeComments); + return getLinesBetweenPositions(sourceFile, range1.end, range2Start); + } + + export function getLinesBetweenRangeEndPositions(range1: TextRange, range2: TextRange, sourceFile: SourceFile) { + return getLinesBetweenPositions(sourceFile, range1.end, range2.end); } export function isNodeArrayMultiLine(list: NodeArray, sourceFile: SourceFile): boolean { @@ -4755,12 +4768,30 @@ namespace ts { } export function positionsAreOnSameLine(pos1: number, pos2: number, sourceFile: SourceFile) { - return pos1 === pos2 || - getLineOfLocalPosition(sourceFile, pos1) === getLineOfLocalPosition(sourceFile, pos2); + return getLinesBetweenPositions(sourceFile, pos1, pos2) === 0; + } + + export function getStartPositionOfRange(range: TextRange, sourceFile: SourceFile, includeComments: boolean) { + return positionIsSynthesized(range.pos) ? -1 : skipTrivia(sourceFile.text, range.pos, /*stopAfterLineBreak*/ false, includeComments); } - export function getStartPositionOfRange(range: TextRange, sourceFile: SourceFile) { - return positionIsSynthesized(range.pos) ? -1 : skipTrivia(sourceFile.text, range.pos); + export function getLinesBetweenPositionAndPrecedingNonWhitespaceCharacter(pos: number, sourceFile: SourceFile, includeComments?: boolean) { + const startPos = skipTrivia(sourceFile.text, pos, /*stopAfterLineBreak*/ false, includeComments); + const prevPos = getPreviousNonWhitespacePosition(startPos, sourceFile); + return getLinesBetweenPositions(sourceFile, prevPos || 0, startPos); + } + + export function getLinesBetweenPositionAndNextNonWhitespaceCharacter(pos: number, sourceFile: SourceFile, includeComments?: boolean) { + const nextPos = skipTrivia(sourceFile.text, pos, /*stopAfterLineBreak*/ false, includeComments); + return getLinesBetweenPositions(sourceFile, pos, nextPos); + } + + function getPreviousNonWhitespacePosition(pos: number, sourceFile: SourceFile) { + while (pos-- > 0) { + if (!isWhiteSpaceLike(sourceFile.text.charCodeAt(pos))) { + return pos; + } + } } /** diff --git a/src/harness/fourslashImpl.ts b/src/harness/fourslashImpl.ts index 33f911ecd8f64..97283a6848785 100644 --- a/src/harness/fourslashImpl.ts +++ b/src/harness/fourslashImpl.ts @@ -3306,7 +3306,7 @@ namespace FourSlash { } public moveToNewFile(options: FourSlashInterface.MoveToNewFileOptions): void { - assert(this.getRanges().length === 1); + assert(this.getRanges().length === 1, "Must have exactly one fourslash range (source enclosed between '[|' and '|]' delimiters) in the source file"); const range = this.getRanges()[0]; const refactor = ts.find(this.getApplicableRefactors(range, { allowTextChangesInNewFiles: true }), r => r.name === "Move to a new file")!; assert(refactor.actions.length === 1); diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index 761391af6bc7f..31aff6b210a5b 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -70,7 +70,7 @@ namespace ts.formatting { * Formatter calls this function when rule adds or deletes new lines from the text * so indentation scope can adjust values of indentation and delta. */ - recomputeIndentation(lineAddedByFormatting: boolean): void; + recomputeIndentation(lineAddedByFormatting: boolean, parent: Node): void; } export function formatOnEnter(position: number, sourceFile: SourceFile, formatContext: FormatContext): TextChange[] { @@ -567,8 +567,8 @@ namespace ts.formatting { !suppressDelta && shouldAddDelta(line, kind, container) ? indentation + getDelta(container) : indentation, getIndentation: () => indentation, getDelta, - recomputeIndentation: lineAdded => { - if (node.parent && SmartIndenter.shouldIndentChildNode(options, node.parent, node, sourceFile)) { + recomputeIndentation: (lineAdded, parent) => { + if (SmartIndenter.shouldIndentChildNode(options, parent, node, sourceFile)) { indentation += lineAdded ? options.indentSize! : -options.indentSize!; delta = SmartIndenter.shouldIndentChildNode(options, node) ? options.indentSize! : 0; } @@ -996,7 +996,7 @@ namespace ts.formatting { // Handle the case where the next line is moved to be the end of this line. // In this case we don't indent the next line in the next pass. if (currentParent.getStart(sourceFile) === currentItem.pos) { - dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false); + dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ false, contextNode); } break; case LineAction.LineAdded: @@ -1004,7 +1004,7 @@ namespace ts.formatting { // In this case we indent token2 in the next pass but we set // sameLineIndent flag to notify the indenter that the indentation is within the line. if (currentParent.getStart(sourceFile) === currentItem.pos) { - dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true); + dynamicIndentation.recomputeIndentation(/*lineAddedByFormatting*/ true, contextNode); } break; default: diff --git a/src/services/refactors/extractType.ts b/src/services/refactors/extractType.ts index 51049f6b7cdd1..813ee186f0faa 100644 --- a/src/services/refactors/extractType.ts +++ b/src/services/refactors/extractType.ts @@ -159,7 +159,7 @@ namespace ts.refactor { typeParameters.map(id => updateTypeParameterDeclaration(id, id.name, id.constraint, /* defaultType */ undefined)), selection ); - changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true); + changes.insertNodeBefore(file, firstStatement, ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true); changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); } @@ -174,7 +174,7 @@ namespace ts.refactor { /* heritageClauses */ undefined, typeElements ); - changes.insertNodeBefore(file, firstStatement, newTypeNode, /* blankLineBetween */ true); + changes.insertNodeBefore(file, firstStatement, ignoreSourceNewlines(newTypeNode), /* blankLineBetween */ true); changes.replaceNode(file, selection, createTypeReferenceNode(name, typeParameters.map(id => createTypeReferenceNode(id.name, /* typeArguments */ undefined)))); } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index 748fe0ca8e2d4..365eeec4cd0f8 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -925,7 +925,7 @@ namespace ts.textChanges { export function getNonformattedText(node: Node, sourceFile: SourceFile | undefined, newLineCharacter: string): { text: string, node: Node } { const writer = createWriter(newLineCharacter); const newLine = newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed; - createPrinter({ newLine, neverAsciiEscape: true }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer); + createPrinter({ newLine, neverAsciiEscape: true, preserveSourceNewlines: true }, writer).writeNode(EmitHint.Unspecified, node, sourceFile, writer); return { text: writer.getText(), node: assignPositionsToNode(node) }; } } @@ -1054,8 +1054,8 @@ namespace ts.textChanges { writer.writeSymbol(s, sym); setLastNonTriviaPosition(s, /*force*/ false); } - function writeLine(): void { - writer.writeLine(); + function writeLine(force?: boolean): void { + writer.writeLine(force); } function increaseIndent(): void { writer.increaseIndent(); diff --git a/tests/baselines/reference/objectLiteralShorthandPropertiesErrorFromNotUsingIdentifier.js b/tests/baselines/reference/objectLiteralShorthandPropertiesErrorFromNotUsingIdentifier.js index e0aecb5f18814..33ae1f6c8f0ee 100644 --- a/tests/baselines/reference/objectLiteralShorthandPropertiesErrorFromNotUsingIdentifier.js +++ b/tests/baselines/reference/objectLiteralShorthandPropertiesErrorFromNotUsingIdentifier.js @@ -35,7 +35,8 @@ var y = { "typeof": }; var x = (_a = { - a: a, : .b, + a: a, + : .b, a: a }, _a["ss"] = , diff --git a/tests/baselines/reference/objectLiteralShorthandPropertiesErrorWithModule.js b/tests/baselines/reference/objectLiteralShorthandPropertiesErrorWithModule.js index ee46d4741dd15..f48a6c3a4e5d6 100644 --- a/tests/baselines/reference/objectLiteralShorthandPropertiesErrorWithModule.js +++ b/tests/baselines/reference/objectLiteralShorthandPropertiesErrorWithModule.js @@ -25,7 +25,8 @@ var n; (function (n) { var z = 10000; n.y = { - m: m, : .x // error + m: m, + : .x // error }; })(n || (n = {})); m.y.x; diff --git a/tests/baselines/reference/objectTypesWithOptionalProperties2.js b/tests/baselines/reference/objectTypesWithOptionalProperties2.js index 03d2b162847ea..284ecdea92ca7 100644 --- a/tests/baselines/reference/objectTypesWithOptionalProperties2.js +++ b/tests/baselines/reference/objectTypesWithOptionalProperties2.js @@ -42,5 +42,6 @@ var C2 = /** @class */ (function () { return C2; }()); var b = { - x: function () { }, 1: // error + x: function () { }, + 1: // error }; diff --git a/tests/baselines/reference/parserErrorRecovery_ObjectLiteral2.js b/tests/baselines/reference/parserErrorRecovery_ObjectLiteral2.js index 1cdf73ddadcb3..6d723a00d4a9d 100644 --- a/tests/baselines/reference/parserErrorRecovery_ObjectLiteral2.js +++ b/tests/baselines/reference/parserErrorRecovery_ObjectLiteral2.js @@ -3,5 +3,4 @@ var v = { a return; //// [parserErrorRecovery_ObjectLiteral2.js] -var v = { a: a, - "return": }; +var v = { a: a, "return": }; diff --git a/tests/cases/fourslash/extract-const-callback-function-this3.ts b/tests/cases/fourslash/extract-const-callback-function-this3.ts index 1d9d8ba59e767..099229a11ba60 100644 --- a/tests/cases/fourslash/extract-const-callback-function-this3.ts +++ b/tests/cases/fourslash/extract-const-callback-function-this3.ts @@ -10,8 +10,6 @@ edit.applyRefactor({ actionDescription: "Extract to constant in enclosing scope", newContent: `declare function fWithThis(fn: (this: { a: string }, a: string) => string): void; -const newLocal = function(this: { - a: string; -}, a: string): string { return this.a; }; +const newLocal = function(this: { a: string; }, a: string): string { return this.a; }; fWithThis(/*RENAME*/newLocal);` }); diff --git a/tests/cases/fourslash/moveToNewFile_declarationKinds.ts b/tests/cases/fourslash/moveToNewFile_declarationKinds.ts index 90c5eb6973a6a..4e9a051108665 100644 --- a/tests/cases/fourslash/moveToNewFile_declarationKinds.ts +++ b/tests/cases/fourslash/moveToNewFile_declarationKinds.ts @@ -24,16 +24,11 @@ type U = T; type V = I;`, "/x.ts": `export const x = 0; export function f() { } -export class C { -} -export enum E { -} -export namespace N { - export const x = 0; -} +export class C { } +export enum E { } +export namespace N { export const x = 0; } export type T = number; -export interface I { -} +export interface I { } `, }, }); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_export_object.ts b/tests/cases/fourslash/refactorConvertToEs6Module_export_object.ts index 97056ceefdad4..664796b117275 100644 --- a/tests/cases/fourslash/refactorConvertToEs6Module_export_object.ts +++ b/tests/cases/fourslash/refactorConvertToEs6Module_export_object.ts @@ -22,6 +22,5 @@ verify.codeFix({ export function f() { } export function g() { } export function h() { } -export class C { -}`, +export class C { }`, }); diff --git a/tests/cases/fourslash/refactorConvertToEs6Module_expressionToDeclaration.ts b/tests/cases/fourslash/refactorConvertToEs6Module_expressionToDeclaration.ts index c91c726dbfd76..73dcff282854a 100644 --- a/tests/cases/fourslash/refactorConvertToEs6Module_expressionToDeclaration.ts +++ b/tests/cases/fourslash/refactorConvertToEs6Module_expressionToDeclaration.ts @@ -15,8 +15,6 @@ verify.codeFix({ `var C = {}; console.log(C); export async function* f(p) { p; } -const _C = class C extends D { - m() { } -}; +const _C = class C extends D { m() { } }; export { _C as C };`, }); diff --git a/tests/cases/fourslash/textChangesPreserveNewlines1.ts b/tests/cases/fourslash/textChangesPreserveNewlines1.ts new file mode 100644 index 0000000000000..7e9a11a738d23 --- /dev/null +++ b/tests/cases/fourslash/textChangesPreserveNewlines1.ts @@ -0,0 +1,25 @@ +/// + +//// /*1*/console.log(1); +//// +//// console.log(2); +//// +//// console.log(3);/*2*/ + +goTo.select("1", "2"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "function_scope_0", + actionDescription: "Extract to function in global scope", + newContent: +`/*RENAME*/newFunction(); + +function newFunction() { + console.log(1); + + console.log(2); + + console.log(3); +} +` +}); diff --git a/tests/cases/fourslash/textChangesPreserveNewlines2.ts b/tests/cases/fourslash/textChangesPreserveNewlines2.ts new file mode 100644 index 0000000000000..b5d343b037689 --- /dev/null +++ b/tests/cases/fourslash/textChangesPreserveNewlines2.ts @@ -0,0 +1,29 @@ +/// + +//// /*1*/1 + +//// 2 + +//// +//// 3 + +//// +//// +//// 4;/*2*/ + +goTo.select("1", "2"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "function_scope_0", + actionDescription: "Extract to function in global scope", + newContent: +`/*RENAME*/newFunction(); + +function newFunction() { + 1 + + 2 + + + 3 + + + + 4; +} +` +}); diff --git a/tests/cases/fourslash/textChangesPreserveNewlines3.ts b/tests/cases/fourslash/textChangesPreserveNewlines3.ts new file mode 100644 index 0000000000000..d01a8c083b985 --- /dev/null +++ b/tests/cases/fourslash/textChangesPreserveNewlines3.ts @@ -0,0 +1,35 @@ +/// + +//// /*1*/app +//// .use(foo) +//// +//// .use(bar) +//// +//// +//// .use( +//// baz, +//// +//// blob);/*2*/ + +goTo.select("1", "2"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "function_scope_0", + actionDescription: "Extract to function in global scope", + newContent: +`/*RENAME*/newFunction(); + +function newFunction() { + app + .use(foo) + + .use(bar) + + + .use( + baz, + + blob); +} +` +}); diff --git a/tests/cases/fourslash/textChangesPreserveNewlines4.ts b/tests/cases/fourslash/textChangesPreserveNewlines4.ts new file mode 100644 index 0000000000000..77892b893c7a0 --- /dev/null +++ b/tests/cases/fourslash/textChangesPreserveNewlines4.ts @@ -0,0 +1,24 @@ +/// + +//// const x = /*1*/function f() +//// { +//// +//// console.log(); +//// }/*2*/; + +goTo.select("1", "2"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "function_scope_0", + actionDescription: "Extract to function in global scope", + newContent: +`const x = /*RENAME*/newFunction(); + +function newFunction() { + return function f() { + + console.log(); + }; +} +` +}); diff --git a/tests/cases/fourslash/textChangesPreserveNewlines5.ts b/tests/cases/fourslash/textChangesPreserveNewlines5.ts new file mode 100644 index 0000000000000..d630fb47e0f48 --- /dev/null +++ b/tests/cases/fourslash/textChangesPreserveNewlines5.ts @@ -0,0 +1,31 @@ +/// + +//// /*1*/f // call expression +//// (arg)( +//// /** @type {number} */ +//// blah, +//// +//// blah +//// +//// );/*2*/ + +goTo.select("1", "2"); +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "function_scope_0", + actionDescription: "Extract to function in global scope", + newContent: +`/*RENAME*/newFunction(); + +function newFunction() { + f // call expression + (arg)( + /** @type {number} */ + blah, + + blah + + ); +} +` +}); diff --git a/tests/cases/fourslash/textChangesPreserveNewlines6.ts b/tests/cases/fourslash/textChangesPreserveNewlines6.ts new file mode 100644 index 0000000000000..d3f5032047292 --- /dev/null +++ b/tests/cases/fourslash/textChangesPreserveNewlines6.ts @@ -0,0 +1,30 @@ +/// + +//// /*1*/f // call expression +//// (arg)( +//// /** @type {number} */ +//// blah, /* another param */ blah // TODO: name variable not 'blah' +//// +//// );/*2*/ + +goTo.select("1", "2"); + +// Note: the loss of `// TODO: name variable not 'blah'` +// is not desirable, but not related to this test. +edit.applyRefactor({ + refactorName: "Extract Symbol", + actionName: "function_scope_0", + actionDescription: "Extract to function in global scope", + newContent: +`/*RENAME*/newFunction(); + +function newFunction() { + f // call expression + (arg)( + /** @type {number} */ + blah, /* another param */ blah + + ); +} +` +}); diff --git a/tests/cases/fourslash/textChangesPreserveNewlines7.ts b/tests/cases/fourslash/textChangesPreserveNewlines7.ts new file mode 100644 index 0000000000000..9c04d29968ed5 --- /dev/null +++ b/tests/cases/fourslash/textChangesPreserveNewlines7.ts @@ -0,0 +1,44 @@ +/// + +// @Filename: /index.tsx + +////[|function Foo({ label }: { label: string }) { +//// return ( +////
+////
+////
{label}
+////
+////
+//// ); +////}|] + +verify.moveToNewFile({ + newFileContents: { + "/index.tsx": "", + "/Foo.tsx": +`function Foo({ label }: { label: string; }) { + return ( +
+
+
{label}
+
+
+ ); +} +` + } +});