diff --git a/new-packages/ts-project/__tests__/source-edits/add-transition.test.ts b/new-packages/ts-project/__tests__/source-edits/add-transition.test.ts index c3100d29..8dcd1a5c 100644 --- a/new-packages/ts-project/__tests__/source-edits/add-transition.test.ts +++ b/new-packages/ts-project/__tests__/source-edits/add-transition.test.ts @@ -1279,46 +1279,422 @@ test('should be possible to add a transition in the middle of the existing array `); }); -test.todo( - 'should be possible to add a transition at the end of an upgraded array', - async () => { - const tmpPath = await testdir({ - 'tsconfig.json': JSON.stringify({}), - 'index.ts': ts` - import { createMachine } from "xstate"; +test('should be possible to add a transition at the end of an upgraded array (single line, comma directly after it)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; - createMachine({ - initial: "foo", - states: { - foo: { - on: { - NEXT: "bar", - }, + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: "bar", }, - bar: {}, - baz: {}, }, - }); - `, - }); + bar: {}, + baz: {}, + }, + }); + `, + }); - const project = await createTestProject(tmpPath); + const project = await createTestProject(tmpPath); - const textEdits = project.editDigraph( - { - fileName: 'index.ts', - machineIndex: 0, + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'add_transition', + sourcePath: ['foo'], + targetPath: ['baz'], + transitionPath: ['on', 'NEXT', 1], + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: [ + "bar", + "baz" + ], + }, + }, + bar: {}, + baz: {}, }, - { - type: 'add_transition', - sourcePath: ['foo'], - targetPath: ['baz'], - transitionPath: ['on', 'NEXT', 1], + });", + } + `); +}); + +test('should be possible to add a transition at the end of an upgraded array (single line, comma directly after it, comment before it)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': ts` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: /* comm */ "bar", + }, + }, + bar: {}, + baz: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'add_transition', + sourcePath: ['foo'], + targetPath: ['baz'], + transitionPath: ['on', 'NEXT', 1], + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: [ + /* comm */ "bar", + "baz" + ], + }, + }, + bar: {}, + baz: {}, }, - ); - expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(); - }, -); + });", + } + `); +}); + +test('should be possible to add a transition at the end of an upgraded array (existing value on subsequent line)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': outdent` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: + "bar", + }, + }, + bar: {}, + baz: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'add_transition', + sourcePath: ['foo'], + targetPath: ['baz'], + transitionPath: ['on', 'NEXT', 1], + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: [ + "bar", + "baz" + ], + }, + }, + bar: {}, + baz: {}, + }, + });", + } + `); +}); + +test('should be possible to add a transition at the end of an upgraded array (existing value on subsequent line, trailing comma on yet another line)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': outdent` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: + "bar" + + , + }, + }, + bar: {}, + baz: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'add_transition', + sourcePath: ['foo'], + targetPath: ['baz'], + transitionPath: ['on', 'NEXT', 1], + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: [ + "bar", + "baz" + ] + + , + }, + }, + bar: {}, + baz: {}, + }, + });", + } + `); +}); + +test('should be possible to add a transition at the end of an upgraded array (existing value on subsequent line, with trailing comment)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': outdent` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: + "bar" // comment + }, + }, + bar: {}, + baz: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'add_transition', + sourcePath: ['foo'], + targetPath: ['baz'], + transitionPath: ['on', 'NEXT', 1], + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: [ + "bar", // comment + "baz" + ] + }, + }, + bar: {}, + baz: {}, + }, + });", + } + `); +}); + +test('should be possible to add a transition at the end of an upgraded array (existing value on subsequent line, multi-line comment below it)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': outdent` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: + "bar" + /* comment */ + }, + }, + bar: {}, + baz: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'add_transition', + sourcePath: ['foo'], + targetPath: ['baz'], + transitionPath: ['on', 'NEXT', 1], + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: [ + "bar", + "baz" + ] + /* comment */ + }, + }, + bar: {}, + baz: {}, + }, + });", + } + `); +}); + +test('should be possible to add a transition at the end of an upgraded array (existing value on subsequent line, with trailing comment and comma after it)', async () => { + const tmpPath = await testdir({ + 'tsconfig.json': JSON.stringify({}), + 'index.ts': outdent` + import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: + "bar" /* comment */ , + }, + }, + bar: {}, + baz: {}, + }, + }); + `, + }); + + const project = await createTestProject(tmpPath); + + const textEdits = project.editDigraph( + { + fileName: 'index.ts', + machineIndex: 0, + }, + { + type: 'add_transition', + sourcePath: ['foo'], + targetPath: ['baz'], + transitionPath: ['on', 'NEXT', 1], + }, + ); + expect(await project.applyTextEdits(textEdits)).toMatchInlineSnapshot(` + { + "index.ts": "import { createMachine } from "xstate"; + + createMachine({ + initial: "foo", + states: { + foo: { + on: { + NEXT: [ + "bar", /* comment */ + "baz" + ] , + }, + }, + bar: {}, + baz: {}, + }, + });", + } + `); +}); test.todo( 'should be possible to add a transition at the start of an upgraded array', diff --git a/new-packages/ts-project/src/codeChanges.ts b/new-packages/ts-project/src/codeChanges.ts index 1a3b746d..63e2d22a 100644 --- a/new-packages/ts-project/src/codeChanges.ts +++ b/new-packages/ts-project/src/codeChanges.ts @@ -77,6 +77,13 @@ interface ReplaceWithCodeChange extends BaseCodeChange { replacement: InsertionElement; } +interface WrapIntoArrayWithCodeChange extends BaseCodeChange { + type: 'wrap_into_array_with'; + current: Node; + insertionType: 'append' | 'prepend'; + newElement: InsertionElement; +} + type CodeChange = | InsertAtOptionalObjectPathCodeChange | InsertElementIntoArrayCodeChange @@ -85,7 +92,8 @@ type CodeChange = | RemovePropertyNameChange | ReplacePropertyNameChange | ReplaceRangeCodeChange - | ReplaceWithCodeChange; + | ReplaceWithCodeChange + | WrapIntoArrayWithCodeChange; function toZeroLengthRange(position: number) { return { start: position, end: position }; @@ -123,6 +131,10 @@ function getSingleLineWhitespaceBeforePosition(text: string, position: number) { return text.slice(0, end); } +function getLeadingWhitespaceLength(text: string) { + return text.match(/^\s*/)?.[0].length || 0; +} + function getTrailingCommaPosition({ text }: SourceFile, position: number) { for (let i = position; i < text.length; i++) { const char = text[i]; @@ -290,21 +302,13 @@ export function createCodeChanges(ts: typeof import('typescript')) { if (isNextTheLastSegment) { if (!ts.isArrayLiteralExpression(prop.initializer)) { const existing = prop.initializer; - assert(nextSegment === 0 || nextSegment === 1); - const existingRaw = c.raw(existing.getFullText()); if (nextSegment === 0) { - codeChanges.replaceWith( - existing, - c.array([value, existingRaw]), - ); + codeChanges.wrapIntoArrayWith(existing, 'append', value); return; } if (nextSegment === 1) { - codeChanges.replaceWith( - existing, - c.array([existingRaw, value]), - ); + codeChanges.wrapIntoArrayWith(existing, 'prepend', value); return; } @@ -440,6 +444,23 @@ export function createCodeChanges(ts: typeof import('typescript')) { replacement, }); }, + wrapIntoArrayWith: ( + current: Node, + insertionType: 'append' | 'prepend', + newElement: InsertionElement, + ) => { + changes.push({ + type: 'wrap_into_array_with', + sourceFile: current.getSourceFile(), + range: { + start: current.getStart(), + end: current.getEnd(), + }, + current, + insertionType, + newElement, + }); + }, getTextEdits: (): TextEdit[] => { const edits: TextEdit[] = []; @@ -834,6 +855,78 @@ export function createCodeChanges(ts: typeof import('typescript')) { break; case 'replace_with': throw new Error('Not implemented'); + case 'wrap_into_array_with': { + const currentIdentation = getIndentationBeforePosition( + change.sourceFile.text, + change.current.getStart(), + ); + const hasNewLine = change.current.getFullText().includes('\n'); + const trailingComment = last( + ts.getTrailingCommentRanges( + change.sourceFile.text, + change.current.getEnd(), + ), + ); + + const newElementIndentation = hasNewLine + ? currentIdentation + : currentIdentation + formattingOptions.singleIndentation; + + if (hasNewLine) { + edits.push({ + type: 'insert', + fileName: change.sourceFile.fileName, + position: change.current.getFullStart(), + newText: ` [`, + }); + } else { + const leadingTrivia = change.sourceFile.text.slice( + change.current.getFullStart(), + change.current.getStart(), + ); + edits.push({ + type: 'insert', + fileName: change.sourceFile.fileName, + position: + change.current.getFullStart() + + getLeadingWhitespaceLength(leadingTrivia), + newText: `[\n` + newElementIndentation, + }); + } + + if (trailingComment) { + edits.push({ + type: 'insert', + fileName: change.sourceFile.fileName, + position: change.current.getEnd(), + newText: ',', + }); + } + + edits.push({ + type: 'insert', + fileName: change.sourceFile.fileName, + position: trailingComment?.end ?? change.current.getEnd(), + newText: + (trailingComment ? '' : `,`) + + `\n` + + newElementIndentation + + insertionToText( + ts, + change.sourceFile, + change.newElement, + formattingOptions, + ) + + '\n' + + getIndentationBeforePosition( + change.sourceFile.text, + change.current.getFullStart(), + ) + + `]`, + }); + + break; + } } }