diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 0a20985e6cc2c..bb3376472df8e 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -910,7 +910,7 @@ namespace ts { let containerEnd = -1; let declarationListContainerEnd = -1; let currentLineMap: readonly number[] | undefined; - let detachedCommentsInfo: { nodePos: number, detachedCommentEndPos: number}[] | undefined; + let detachedCommentsInfo: { nodePos: number, detachedCommentEndPos: number }[] | undefined; let hasWrittenComment = false; let commentsDisabled = !!printerOptions.removeComments; const { enter: enterComment, exit: exitComment } = performance.createTimerIf(extendedDiagnostics, "commentTime", "beforeComment", "afterComment"); @@ -4469,15 +4469,15 @@ namespace ts { // JsxText will be written with its leading whitespace, so don't add more manually. return 0; } - else if (!nodeIsSynthesized(previousNode) && !nodeIsSynthesized(nextNode) && (!previousNode.parent || !nextNode.parent || previousNode.parent === nextNode.parent)) { - if (preserveSourceNewlines && previousNode.parent && nextNode.parent) { - return getEffectiveLines( - includeComments => getLinesBetweenRangeEndAndRangeStart( - previousNode, - nextNode, - currentSourceFile!, - includeComments)); - } + else if (preserveSourceNewlines && siblingNodePositionsAreComparable(previousNode, nextNode)) { + return getEffectiveLines( + includeComments => getLinesBetweenRangeEndAndRangeStart( + previousNode, + nextNode, + currentSourceFile!, + includeComments)); + } + else if (!preserveSourceNewlines && !nodeIsSynthesized(previousNode) && !nodeIsSynthesized(nextNode)) { return rangeEndIsOnSameLineAsRangeStart(previousNode, nextNode, currentSourceFile!) ? 0 : 1; } else if (synthesizedNodeStartsOnNewLine(previousNode, format) || synthesizedNodeStartsOnNewLine(nextNode, format)) { @@ -5173,6 +5173,27 @@ namespace ts { } + function siblingNodePositionsAreComparable(previousNode: Node, nextNode: Node) { + if (nodeIsSynthesized(previousNode) || nodeIsSynthesized(nextNode)) { + return false; + } + + if (nextNode.pos < previousNode.end) { + return false; + } + + previousNode = getOriginalNode(previousNode); + nextNode = getOriginalNode(nextNode); + const parent = previousNode.parent; + if (!parent || parent !== nextNode.parent) { + return false; + } + + const parentNodeArray = getContainingNodeArray(previousNode); + const prevNodeIndex = parentNodeArray?.indexOf(previousNode); + return prevNodeIndex !== undefined && prevNodeIndex > -1 && parentNodeArray!.indexOf(nextNode) === prevNodeIndex + 1; + } + function emitLeadingComments(pos: number, isEmittedNode: boolean) { hasWrittenComment = false; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 6ed8c04803a81..27634c39a2ed4 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7097,4 +7097,71 @@ namespace ts { export function containsIgnoredPath(path: string) { return some(ignoredPaths, p => stringContains(path, p)); } + + export function getContainingNodeArray(node: Node): NodeArray | undefined { + if (!node.parent) return undefined; + switch (node.kind) { + case SyntaxKind.TypeParameter: + const { parent } = node as TypeParameterDeclaration; + return parent.kind === SyntaxKind.InferType ? undefined : parent.typeParameters; + case SyntaxKind.Parameter: + return (node as ParameterDeclaration).parent.parameters; + case SyntaxKind.TemplateLiteralTypeSpan: + return (node as TemplateLiteralTypeSpan).parent.templateSpans; + case SyntaxKind.TemplateSpan: + return (node as TemplateSpan).parent.templateSpans; + case SyntaxKind.Decorator: + return (node as Decorator).parent.decorators; + case SyntaxKind.HeritageClause: + return (node as HeritageClause).parent.heritageClauses; + } + + const { parent } = node; + if (isJSDocTag(node)) { + return isJSDocTypeLiteral(node.parent) ? undefined : node.parent.tags; + } + + switch (parent.kind) { + case SyntaxKind.TypeLiteral: + case SyntaxKind.InterfaceDeclaration: + return isTypeElement(node) ? (parent as TypeLiteralNode | InterfaceDeclaration).members : undefined; + case SyntaxKind.UnionType: + case SyntaxKind.IntersectionType: + return (parent as UnionOrIntersectionTypeNode).types; + case SyntaxKind.TupleType: + case SyntaxKind.ArrayLiteralExpression: + case SyntaxKind.CommaListExpression: + case SyntaxKind.NamedImports: + case SyntaxKind.NamedExports: + return (parent as TupleTypeNode | ArrayLiteralExpression | CommaListExpression | NamedImports | NamedExports).elements; + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.JsxAttributes: + return (parent as ObjectLiteralExpressionBase).properties; + case SyntaxKind.CallExpression: + case SyntaxKind.NewExpression: + return isTypeNode(node) ? (parent as CallExpression | NewExpression).typeArguments : + (parent as CallExpression | NewExpression).expression === node ? undefined : + (parent as CallExpression | NewExpression).arguments; + case SyntaxKind.JsxElement: + case SyntaxKind.JsxFragment: + return isJsxChild(node) ? (parent as JsxElement | JsxFragment).children : undefined; + case SyntaxKind.JsxOpeningElement: + case SyntaxKind.JsxSelfClosingElement: + return isTypeNode(node) ? (parent as JsxOpeningElement | JsxSelfClosingElement).typeArguments : undefined; + case SyntaxKind.Block: + case SyntaxKind.CaseClause: + case SyntaxKind.DefaultClause: + case SyntaxKind.ModuleBlock: + return (parent as Block | CaseOrDefaultClause | ModuleBlock).statements; + case SyntaxKind.CaseBlock: + return (parent as CaseBlock).clauses; + case SyntaxKind.ClassDeclaration: + case SyntaxKind.ClassExpression: + return isClassElement(node) ? (parent as ClassLikeDeclaration).members : undefined; + case SyntaxKind.EnumDeclaration: + return isEnumMember(node) ? (parent as EnumDeclaration).members : undefined; + case SyntaxKind.SourceFile: + return (parent as SourceFile).statements; + } + } } diff --git a/tests/cases/fourslash/organizeImports1.ts b/tests/cases/fourslash/organizeImports1.ts new file mode 100644 index 0000000000000..7e25a4afde7c4 --- /dev/null +++ b/tests/cases/fourslash/organizeImports1.ts @@ -0,0 +1,32 @@ +/// + +// Regression test for bug #41417 + +//// import { +//// d, d as D, +//// c, +//// c as C, b, +//// b as B, a +//// } from './foo'; +//// import { +//// h, h as H, +//// g, +//// g as G, f, +//// f as F, e +//// } from './foo'; +//// +//// console.log(a, B, b, c, C, d, D); +//// console.log(e, f, F, g, G, H, h); + +verify.organizeImports( +`import { + a, b, + b as B, c, + c as C, d, d as D, e, f, + f as F, g, + g as G, h, h as H +} from './foo'; + +console.log(a, B, b, c, C, d, D); +console.log(e, f, F, g, G, H, h);` +); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImports2.ts b/tests/cases/fourslash/organizeImports2.ts new file mode 100644 index 0000000000000..2e26e630bc39a --- /dev/null +++ b/tests/cases/fourslash/organizeImports2.ts @@ -0,0 +1,16 @@ +/// + +// Regression test for bug #41417 + +//// import { +//// Foo +//// , Bar +//// } from "foo" +//// +//// console.log(Foo, Bar); + +verify.organizeImports( +`import { Bar, Foo } from "foo"; + +console.log(Foo, Bar);` +); \ No newline at end of file diff --git a/tests/cases/fourslash/organizeImports3.ts b/tests/cases/fourslash/organizeImports3.ts new file mode 100644 index 0000000000000..afba640a898dd --- /dev/null +++ b/tests/cases/fourslash/organizeImports3.ts @@ -0,0 +1,18 @@ +/// + +// Regression test for bug #41417 + +//// import { +//// Bar +//// , Foo +//// } from "foo" +//// +//// console.log(Foo, Bar); + +verify.organizeImports( +`import { + Bar, + Foo +} from "foo"; + +console.log(Foo, Bar);`); \ No newline at end of file