diff --git a/src/harness/unittests/textChanges.ts b/src/harness/unittests/textChanges.ts index b2fc1ae577..8269b6de6b 100644 --- a/src/harness/unittests/textChanges.ts +++ b/src/harness/unittests/textChanges.ts @@ -709,5 +709,83 @@ class A { changeTracker.deleteNode(sourceFile, findChild("x", sourceFile)); }); } + { + const text = ` +class A { + x = foo +} +` + runSingleFileTest("insertNodeInClassAfterNodeWithoutSeparator1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, + createComputedPropertyName(createLiteral(1)), + /*questionToken*/ undefined, + createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*initializer*/ undefined); + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode, { suffix: newLineCharacter }); + }); + } + { + const text = ` +class A { + x() { + } +} +` + runSingleFileTest("insertNodeInClassAfterNodeWithoutSeparator2", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, + createComputedPropertyName(createLiteral(1)), + /*questionToken*/ undefined, + createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*initializer*/ undefined); + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode, { suffix: newLineCharacter }); + }); + } + { + const text = ` +interface A { + x +} +` + runSingleFileTest("insertNodeInInterfaceAfterNodeWithoutSeparator1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, + createComputedPropertyName(createLiteral(1)), + /*questionToken*/ undefined, + createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*initializer*/ undefined); + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode, { suffix: newLineCharacter }); + }); + } + { + const text = ` +interface A { + x() +} +` + runSingleFileTest("insertNodeInInterfaceAfterNodeWithoutSeparator2", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, + createComputedPropertyName(createLiteral(1)), + /*questionToken*/ undefined, + createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*initializer*/ undefined); + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), newNode, { suffix: newLineCharacter }); + }); + } + { + const text = ` +let x = foo +` + runSingleFileTest("insertNodeInStatementListAfterNodeWithoutSeparator1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createStatement(createParen(createLiteral(1))); + changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("x", sourceFile), newNode, { suffix: newLineCharacter }); + }); + } }); } \ No newline at end of file diff --git a/src/lib/dom.iterable.d.ts b/src/lib/dom.iterable.d.ts index 7f653dc8e0..e4da7a6276 100644 --- a/src/lib/dom.iterable.d.ts +++ b/src/lib/dom.iterable.d.ts @@ -5,9 +5,53 @@ interface DOMTokenList { } interface NodeList { + + + /** + * Returns an array of key, value pairs for every entry in the list + */ + entries(): IterableIterator<[number, Node]>; + /** + * Performs the specified action for each node in an list. + * @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the list. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: Node, index: number, listObj: NodeList) => void, thisArg?: any): void; + /** + * Returns an list of keys in the list + */ + keys(): IterableIterator; + + /** + * Returns an list of values in the list + */ + values(): IterableIterator; + + [Symbol.iterator](): IterableIterator } interface NodeListOf { + + /** + * Returns an array of key, value pairs for every entry in the list + */ + entries(): IterableIterator<[number, TNode]>; + + /** + * Performs the specified action for each node in an list. + * @param callbackfn A function that accepts up to three arguments. forEach calls the callbackfn function one time for each element in the list. + * @param thisArg An object to which the this keyword can refer in the callbackfn function. If thisArg is omitted, undefined is used as the this value. + */ + forEach(callbackfn: (value: TNode, index: number, listObj: NodeListOf) => void, thisArg?: any): void; + /** + * Returns an list of keys in the list + */ + keys(): IterableIterator; + /** + * Returns an list of values in the list + */ + values(): IterableIterator; + [Symbol.iterator](): IterableIterator } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index a5e4fe6f2e..b7a8b101c0 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -253,11 +253,31 @@ namespace ts.textChanges { } public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node, options: InsertNodeOptions & ConfigurableEnd = {}) { + if ((isStatementButNotDeclaration(after)) || + after.kind === SyntaxKind.PropertyDeclaration || + after.kind === SyntaxKind.PropertySignature || + after.kind === SyntaxKind.MethodSignature) { + // check if previous statement ends with semicolon + // if not - insert semicolon to preserve the code from changing the meaning due to ASI + if (sourceFile.text.charCodeAt(after.end - 1) !== CharacterCodes.semicolon) { + this.changes.push({ + sourceFile, + options: {}, + range: { pos: after.end, end: after.end }, + node: createToken(SyntaxKind.SemicolonToken) + }) + } + } const endPosition = getAdjustedEndPosition(sourceFile, after, options); this.changes.push({ sourceFile, options, useIndentationFromFile: true, node: newNode, range: { pos: endPosition, end: endPosition } }); return this; } + /** + * This function should be used to insert nodes in lists when nodes don't carry separators as the part of the node range, + * i.e. arguments in arguments lists, parameters in parameter lists etc. Statements or class elements are different in sense that + * for them separators are treated as the part of the node. + */ public insertNodeInListAfter(sourceFile: SourceFile, after: Node, newNode: Node) { const containingList = formatting.SmartIndenter.getContainingList(after, sourceFile); if (!containingList) { @@ -463,10 +483,7 @@ namespace ts.textChanges { private static normalize(changes: Change[]) { // order changes by start position - const normalized = changes - .map((c, i) => ({ c, i })) - .sort(({ c: a, i: i1 }, { c: b, i: i2 }) => (a.range.pos - b.range.pos) || i1 - i2) - .map(({ c }) => c); + const normalized = stableSort(changes, (a, b) => a.range.pos - b.range.pos); // verify that end position of the change is less than start position of the next change for (let i = 0; i < normalized.length - 2; i++) { Debug.assert(normalized[i].range.end <= normalized[i + 1].range.pos); diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 92b6365657..953a2e7bf5 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -1384,7 +1384,7 @@ namespace ts { // First token is the open curly, this is where we want to put the 'super' call. return constructor.body.getFirstToken(sourceFile); } - + export function getOpenBraceOfClassLike(declaration: ClassLikeDeclaration, sourceFile: SourceFile) { return getTokenAtPosition(sourceFile, declaration.members.pos - 1); } diff --git a/tests/baselines/reference/textChanges/insertNodeAfterInClass1.js b/tests/baselines/reference/textChanges/insertNodeAfterInClass1.js index 87206e588f..841d43fef9 100644 --- a/tests/baselines/reference/textChanges/insertNodeAfterInClass1.js +++ b/tests/baselines/reference/textChanges/insertNodeAfterInClass1.js @@ -7,6 +7,6 @@ class A { ===MODIFIED=== class A { - x + x; a: boolean; } diff --git a/tests/baselines/reference/textChanges/insertNodeInClassAfterNodeWithoutSeparator1.js b/tests/baselines/reference/textChanges/insertNodeInClassAfterNodeWithoutSeparator1.js new file mode 100644 index 0000000000..0c47c26221 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInClassAfterNodeWithoutSeparator1.js @@ -0,0 +1,12 @@ +===ORIGINAL=== + +class A { + x = foo +} + +===MODIFIED=== + +class A { + x = foo; + [1]: any; +} diff --git a/tests/baselines/reference/textChanges/insertNodeInClassAfterNodeWithoutSeparator2.js b/tests/baselines/reference/textChanges/insertNodeInClassAfterNodeWithoutSeparator2.js new file mode 100644 index 0000000000..bd6302b29a --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInClassAfterNodeWithoutSeparator2.js @@ -0,0 +1,14 @@ +===ORIGINAL=== + +class A { + x() { + } +} + +===MODIFIED=== + +class A { + x() { + } + [1]: any; +} diff --git a/tests/baselines/reference/textChanges/insertNodeInInterfaceAfterNodeWithoutSeparator1.js b/tests/baselines/reference/textChanges/insertNodeInInterfaceAfterNodeWithoutSeparator1.js new file mode 100644 index 0000000000..61910e64dd --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInInterfaceAfterNodeWithoutSeparator1.js @@ -0,0 +1,12 @@ +===ORIGINAL=== + +interface A { + x +} + +===MODIFIED=== + +interface A { + x; + [1]: any; +} diff --git a/tests/baselines/reference/textChanges/insertNodeInInterfaceAfterNodeWithoutSeparator2.js b/tests/baselines/reference/textChanges/insertNodeInInterfaceAfterNodeWithoutSeparator2.js new file mode 100644 index 0000000000..6e88a371eb --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInInterfaceAfterNodeWithoutSeparator2.js @@ -0,0 +1,12 @@ +===ORIGINAL=== + +interface A { + x() +} + +===MODIFIED=== + +interface A { + x(); + [1]: any; +} diff --git a/tests/baselines/reference/textChanges/insertNodeInStatementListAfterNodeWithoutSeparator1.js b/tests/baselines/reference/textChanges/insertNodeInStatementListAfterNodeWithoutSeparator1.js new file mode 100644 index 0000000000..b28b1579df --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInStatementListAfterNodeWithoutSeparator1.js @@ -0,0 +1,8 @@ +===ORIGINAL=== + +let x = foo + +===MODIFIED=== + +let x = foo; +(1);