diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 5fb7e97863e6a..96ecd4991526a 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -919,7 +919,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; let lastNode: Node | undefined; @@ -4488,7 +4488,7 @@ 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) { + else if (siblingNodePositionsAreComparable(previousNode, nextNode)) { if (preserveSourceNewlines) { return getEffectiveLines( includeComments => getLinesBetweenRangeEndAndRangeStart( @@ -4509,6 +4509,33 @@ namespace ts { return format & ListFormat.MultiLine ? 1 : 0; } + function siblingNodePositionsAreComparable(previousNode: Node, nextNode: Node) { + if (nodeIsSynthesized(previousNode) || nodeIsSynthesized(nextNode) || previousNode.parent !== nextNode.parent) { + return false; + } + + if (isImportSpecifier(previousNode)) { + const getNextSpecifier = (node: ImportSpecifier): ImportSpecifier | undefined => { + if (!node.parent) { + return; + } + for (const sibling of node.parent.elements) { + if (node.pos < sibling.pos) { + return sibling; + } + } + }; + + // Get the next specifier and compare against nextNode. If they are not equal, nodes have been rearranged and positions cannot be compared. + const nextSpecifier = getNextSpecifier(previousNode); + if (nextSpecifier && nextSpecifier !== nextNode) { + return false; + } + } + + return true; + } + function getClosingLineTerminatorCount(parentNode: TextRange, children: readonly Node[], format: ListFormat): number { if (format & ListFormat.PreserveLines || preserveSourceNewlines) { if (format & ListFormat.PreferNewLine) { @@ -4659,7 +4686,7 @@ namespace ts { const text = isNumericLiteral(textSourceNode) ? textSourceNode.text : getTextOfNode(textSourceNode); return jsxAttributeEscape ? `"${escapeJsxAttributeString(text)}"` : neverAsciiEscape || (getEmitFlags(node) & EmitFlags.NoAsciiEscaping) ? `"${escapeString(text)}"` : - `"${escapeNonAsciiString(text)}"`; + `"${escapeNonAsciiString(text)}"`; } else { return getLiteralTextOfNode(textSourceNode, neverAsciiEscape, jsxAttributeEscape); diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 7b71f011700c1..6401f4ed1514b 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -2354,22 +2354,6 @@ namespace ts { } } - /** Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files */ - function getNodeAtPosition(sourceFile: SourceFile, position: number): Node { - let current: Node = sourceFile; - const getContainingChild = (child: Node) => { - if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === SyntaxKind.EndOfFileToken)))) { - return child; - } - }; - while (true) { - const child = isJavaScriptFile && hasJSDocNodes(current) && forEach(current.jsDoc, getContainingChild) || forEachChild(current, getContainingChild); - if (!child) { - return current; - } - current = child; - } - } } function getLibFileFromReference(ref: FileReference) { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 0e0c589e8a9fa..f00db59682bdc 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -7081,4 +7081,21 @@ namespace ts { return false; } } + + /** Returns a token if position is in [start-of-leading-trivia, end), includes JSDoc only in JS files */ + export function getNodeAtPosition(sourceFile: SourceFile, position: number): Node { + let current: Node = sourceFile; + const getContainingChild = (child: Node) => { + if (child.pos <= position && (position < child.end || (position === child.end && (child.kind === SyntaxKind.EndOfFileToken)))) { + return child; + } + }; + while (true) { + const child = isSourceFileJS(sourceFile) && hasJSDocNodes(current) && forEach(current.jsDoc, getContainingChild) || forEachChild(current, getContainingChild); + if (!child) { + return current; + } + current = child; + } + } } 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