diff --git a/Jakefile.js b/Jakefile.js index 595875dfc32..2df29911125 100644 --- a/Jakefile.js +++ b/Jakefile.js @@ -11,11 +11,7 @@ var ts = require("./lib/typescript"); // Variables var compilerDirectory = "src/compiler/"; -var servicesDirectory = "src/services/"; var serverDirectory = "src/server/"; -var typingsInstallerDirectory = "src/server/typingsInstaller"; -var cancellationTokenDirectory = "src/server/cancellationToken"; -var watchGuardDirectory = "src/server/watchGuard"; var harnessDirectory = "src/harness/"; var libraryDirectory = "src/lib/"; var scriptsDirectory = "scripts/"; @@ -131,6 +127,7 @@ var harnessSources = harnessCoreSources.concat([ "matchFiles.ts", "initializeTSConfig.ts", "printer.ts", + "textChanges.ts", "transform.ts", "customTransforms.ts", ].map(function (f) { diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index b10f2e7cb17..616be35270e 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -199,6 +199,8 @@ namespace ts { onEmitHelpers, onSetSourceFile, substituteNode, + onBeforeEmitNodeArray, + onAfterEmitNodeArray } = handlers; const newLine = getNewLineCharacter(printerOptions); @@ -631,6 +633,11 @@ namespace ts { if (isExpression(node)) { return pipelineEmitExpression(trySubstituteNode(EmitHint.Expression, node)); } + + if (isToken(node)) { + writeTokenText(kind); + return; + } } function pipelineEmitExpression(node: Node): void { @@ -1553,6 +1560,10 @@ namespace ts { emitSignatureAndBody(node, emitSignatureHead); } + function emitBlockCallback(_hint: EmitHint, body: Node): void { + emitBlockFunctionBody(body); + } + function emitSignatureAndBody(node: FunctionLikeDeclaration, emitSignatureHead: (node: SignatureDeclaration) => void) { const body = node.body; if (body) { @@ -1564,12 +1575,22 @@ namespace ts { if (getEmitFlags(node) & EmitFlags.ReuseTempVariableScope) { emitSignatureHead(node); - emitBlockFunctionBody(body); + if (onEmitNode) { + onEmitNode(EmitHint.Unspecified, body, emitBlockCallback); + } + else { + emitBlockFunctionBody(body); + } } else { pushNameGenerationScope(); emitSignatureHead(node); - emitBlockFunctionBody(body); + if (onEmitNode) { + onEmitNode(EmitHint.Unspecified, body, emitBlockCallback); + } + else { + emitBlockFunctionBody(body); + } popNameGenerationScope(); } @@ -2200,6 +2221,10 @@ namespace ts { write(getOpeningBracket(format)); } + if (onBeforeEmitNodeArray) { + onBeforeEmitNodeArray(children); + } + if (isEmpty) { // Write a line terminator if the parent node was multi-line if (format & ListFormat.MultiLine) { @@ -2315,6 +2340,10 @@ namespace ts { } } + if (onAfterEmitNodeArray) { + onAfterEmitNodeArray(children); + } + if (format & ListFormat.BracketsMask) { write(getClosingBracket(format)); } diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 3ff3bc2782f..669c66c4337 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -1099,6 +1099,10 @@ namespace ts { : node; } + export function createKeywordTypeNode(kind: KeywordTypeNode["kind"]): KeywordTypeNode { + return createSynthesizedNode(kind); + } + export function createFunctionDeclaration(decorators: Decorator[] | undefined, modifiers: Modifier[] | undefined, asteriskToken: AsteriskToken | undefined, name: string | Identifier | undefined, typeParameters: TypeParameterDeclaration[] | undefined, parameters: ParameterDeclaration[], type: TypeNode | undefined, body: Block | undefined) { const node = createSynthesizedNode(SyntaxKind.FunctionDeclaration); node.decorators = asNodeArray(decorators); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index fb91b0ff5b7..b7a3f35d728 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -333,7 +333,7 @@ namespace ts { } /* @internal */ - export function getLineStarts(sourceFile: SourceFile): number[] { + export function getLineStarts(sourceFile: SourceFileLike): number[] { return sourceFile.lineMap || (sourceFile.lineMap = computeLineStarts(sourceFile.text)); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 97b12aeb712..86895f3db6e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2205,6 +2205,16 @@ namespace ts { name: string; } + /* @internal */ + /** + * Subset of properties from SourceFile that are used in multiple utility functions + */ + export interface SourceFileLike { + readonly text: string; + lineMap: number[]; + } + + // Source files are declarations when they are external modules. export interface SourceFile extends Declaration { kind: SyntaxKind.SourceFile; @@ -4132,6 +4142,8 @@ namespace ts { /*@internal*/ onEmitSourceMapOfPosition?: (pos: number) => void; /*@internal*/ onEmitHelpers?: (node: Node, writeLines: (text: string) => void) => void; /*@internal*/ onSetSourceFile?: (node: SourceFile) => void; + /*@internal*/ onBeforeEmitNodeArray?: (nodes: NodeArray) => void; + /*@internal*/ onAfterEmitNodeArray?: (nodes: NodeArray) => void; } export interface PrinterOptions { diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index a5229b614be..030b66813a5 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -184,7 +184,7 @@ namespace ts { return false; } - export function getStartPositionOfLine(line: number, sourceFile: SourceFile): number { + export function getStartPositionOfLine(line: number, sourceFile: SourceFileLike): number { Debug.assert(line >= 0); return getLineStarts(sourceFile)[line]; } @@ -204,7 +204,7 @@ namespace ts { return value !== undefined; } - export function getEndLinePosition(line: number, sourceFile: SourceFile): number { + export function getEndLinePosition(line: number, sourceFile: SourceFileLike): number { Debug.assert(line >= 0); const lineStarts = getLineStarts(sourceFile); @@ -255,7 +255,11 @@ namespace ts { return !nodeIsMissing(node); } - export function getTokenPosOfNode(node: Node, sourceFile?: SourceFile, includeJsDoc?: boolean): number { + export function isToken(n: Node): boolean { + return n.kind >= SyntaxKind.FirstToken && n.kind <= SyntaxKind.LastToken; + } + + export function getTokenPosOfNode(node: Node, sourceFile?: SourceFileLike, includeJsDoc?: boolean): number { // With nodes that have no width (i.e. 'Missing' nodes), we actually *don't* // want to skip trivia because this will launch us forward to the next token. if (nodeIsMissing(node)) { @@ -289,7 +293,7 @@ namespace ts { return node.kind >= SyntaxKind.FirstJSDocTagNode && node.kind <= SyntaxKind.LastJSDocTagNode; } - export function getNonDecoratorTokenPosOfNode(node: Node, sourceFile?: SourceFile): number { + export function getNonDecoratorTokenPosOfNode(node: Node, sourceFile?: SourceFileLike): number { if (nodeIsMissing(node) || !node.decorators) { return getTokenPosOfNode(node, sourceFile); } @@ -2491,7 +2495,7 @@ namespace ts { return indentStrings[1].length; } - export function createTextWriter(newLine: String): EmitTextWriter { + export function createTextWriter(newLine: string): EmitTextWriter { let output: string; let indent: number; let lineStart: boolean; diff --git a/src/compiler/visitor.ts b/src/compiler/visitor.ts index 0f4a4ed6561..133c62e1926 100644 --- a/src/compiler/visitor.ts +++ b/src/compiler/visitor.ts @@ -154,9 +154,9 @@ namespace ts { * Starts a new lexical environment and visits a parameter list, suspending the lexical * environment upon completion. */ - export function visitParameterList(nodes: NodeArray, visitor: Visitor, context: TransformationContext) { + export function visitParameterList(nodes: NodeArray, visitor: Visitor, context: TransformationContext, nodesVisitor = visitNodes) { context.startLexicalEnvironment(); - const updated = visitNodes(nodes, visitor, isParameterDeclaration); + const updated = nodesVisitor(nodes, visitor, isParameterDeclaration); context.suspendLexicalEnvironment(); return updated; } @@ -204,9 +204,9 @@ namespace ts { * @param visitor The callback used to visit each child. * @param context A lexical environment context for the visitor. */ - export function visitEachChild(node: T | undefined, visitor: Visitor, context: TransformationContext): T | undefined; + export function visitEachChild(node: T | undefined, visitor: Visitor, context: TransformationContext, nodesVisitor?: typeof visitNodes): T | undefined; - export function visitEachChild(node: Node, visitor: Visitor, context: TransformationContext): Node { + export function visitEachChild(node: Node, visitor: Visitor, context: TransformationContext, nodesVisitor = visitNodes): Node { if (node === undefined) { return undefined; } @@ -243,8 +243,8 @@ namespace ts { // Signature elements case SyntaxKind.Parameter: return updateParameter(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), (node).dotDotDotToken, visitNode((node).name, visitor, isBindingName), visitNode((node).type, visitor, isTypeNode), @@ -257,55 +257,55 @@ namespace ts { // Type member case SyntaxKind.PropertyDeclaration: return updateProperty(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).name, visitor, isPropertyName), visitNode((node).type, visitor, isTypeNode), visitNode((node).initializer, visitor, isExpression)); case SyntaxKind.MethodDeclaration: return updateMethod(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), (node).asteriskToken, visitNode((node).name, visitor, isPropertyName), - visitNodes((node).typeParameters, visitor, isTypeParameter), - visitParameterList((node).parameters, visitor, context), + nodesVisitor((node).typeParameters, visitor, isTypeParameter), + visitParameterList((node).parameters, visitor, context, nodesVisitor), visitNode((node).type, visitor, isTypeNode), visitFunctionBody((node).body, visitor, context)); case SyntaxKind.Constructor: return updateConstructor(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), - visitParameterList((node).parameters, visitor, context), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), + visitParameterList((node).parameters, visitor, context, nodesVisitor), visitFunctionBody((node).body, visitor, context)); case SyntaxKind.GetAccessor: return updateGetAccessor(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).name, visitor, isPropertyName), - visitParameterList((node).parameters, visitor, context), + visitParameterList((node).parameters, visitor, context, nodesVisitor), visitNode((node).type, visitor, isTypeNode), visitFunctionBody((node).body, visitor, context)); case SyntaxKind.SetAccessor: return updateSetAccessor(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).name, visitor, isPropertyName), - visitParameterList((node).parameters, visitor, context), + visitParameterList((node).parameters, visitor, context, nodesVisitor), visitFunctionBody((node).body, visitor, context)); // Binding patterns case SyntaxKind.ObjectBindingPattern: return updateObjectBindingPattern(node, - visitNodes((node).elements, visitor, isBindingElement)); + nodesVisitor((node).elements, visitor, isBindingElement)); case SyntaxKind.ArrayBindingPattern: return updateArrayBindingPattern(node, - visitNodes((node).elements, visitor, isArrayBindingElement)); + nodesVisitor((node).elements, visitor, isArrayBindingElement)); case SyntaxKind.BindingElement: return updateBindingElement(node, @@ -317,11 +317,11 @@ namespace ts { // Expression case SyntaxKind.ArrayLiteralExpression: return updateArrayLiteral(node, - visitNodes((node).elements, visitor, isExpression)); + nodesVisitor((node).elements, visitor, isExpression)); case SyntaxKind.ObjectLiteralExpression: return updateObjectLiteral(node, - visitNodes((node).properties, visitor, isObjectLiteralElementLike)); + nodesVisitor((node).properties, visitor, isObjectLiteralElementLike)); case SyntaxKind.PropertyAccessExpression: return updatePropertyAccess(node, @@ -336,14 +336,14 @@ namespace ts { case SyntaxKind.CallExpression: return updateCall(node, visitNode((node).expression, visitor, isExpression), - visitNodes((node).typeArguments, visitor, isTypeNode), - visitNodes((node).arguments, visitor, isExpression)); + nodesVisitor((node).typeArguments, visitor, isTypeNode), + nodesVisitor((node).arguments, visitor, isExpression)); case SyntaxKind.NewExpression: return updateNew(node, visitNode((node).expression, visitor, isExpression), - visitNodes((node).typeArguments, visitor, isTypeNode), - visitNodes((node).arguments, visitor, isExpression)); + nodesVisitor((node).typeArguments, visitor, isTypeNode), + nodesVisitor((node).arguments, visitor, isExpression)); case SyntaxKind.TaggedTemplateExpression: return updateTaggedTemplate(node, @@ -361,19 +361,19 @@ namespace ts { case SyntaxKind.FunctionExpression: return updateFunctionExpression(node, - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).modifiers, visitor, isModifier), (node).asteriskToken, visitNode((node).name, visitor, isIdentifier), - visitNodes((node).typeParameters, visitor, isTypeParameter), - visitParameterList((node).parameters, visitor, context), + nodesVisitor((node).typeParameters, visitor, isTypeParameter), + visitParameterList((node).parameters, visitor, context, nodesVisitor), visitNode((node).type, visitor, isTypeNode), visitFunctionBody((node).body, visitor, context)); case SyntaxKind.ArrowFunction: return updateArrowFunction(node, - visitNodes((node).modifiers, visitor, isModifier), - visitNodes((node).typeParameters, visitor, isTypeParameter), - visitParameterList((node).parameters, visitor, context), + nodesVisitor((node).modifiers, visitor, isModifier), + nodesVisitor((node).typeParameters, visitor, isTypeParameter), + visitParameterList((node).parameters, visitor, context, nodesVisitor), visitNode((node).type, visitor, isTypeNode), visitFunctionBody((node).body, visitor, context)); @@ -415,7 +415,7 @@ namespace ts { case SyntaxKind.TemplateExpression: return updateTemplateExpression(node, visitNode((node).head, visitor, isTemplateHead), - visitNodes((node).templateSpans, visitor, isTemplateSpan)); + nodesVisitor((node).templateSpans, visitor, isTemplateSpan)); case SyntaxKind.YieldExpression: return updateYield(node, @@ -428,15 +428,15 @@ namespace ts { case SyntaxKind.ClassExpression: return updateClassExpression(node, - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).name, visitor, isIdentifier), - visitNodes((node).typeParameters, visitor, isTypeParameter), - visitNodes((node).heritageClauses, visitor, isHeritageClause), - visitNodes((node).members, visitor, isClassElement)); + nodesVisitor((node).typeParameters, visitor, isTypeParameter), + nodesVisitor((node).heritageClauses, visitor, isHeritageClause), + nodesVisitor((node).members, visitor, isClassElement)); case SyntaxKind.ExpressionWithTypeArguments: return updateExpressionWithTypeArguments(node, - visitNodes((node).typeArguments, visitor, isTypeNode), + nodesVisitor((node).typeArguments, visitor, isTypeNode), visitNode((node).expression, visitor, isExpression)); case SyntaxKind.AsExpression: @@ -457,11 +457,11 @@ namespace ts { // Element case SyntaxKind.Block: return updateBlock(node, - visitNodes((node).statements, visitor, isStatement)); + nodesVisitor((node).statements, visitor, isStatement)); case SyntaxKind.VariableStatement: return updateVariableStatement(node, - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).declarationList, visitor, isVariableDeclarationList)); case SyntaxKind.ExpressionStatement: @@ -549,61 +549,61 @@ namespace ts { case SyntaxKind.VariableDeclarationList: return updateVariableDeclarationList(node, - visitNodes((node).declarations, visitor, isVariableDeclaration)); + nodesVisitor((node).declarations, visitor, isVariableDeclaration)); case SyntaxKind.FunctionDeclaration: return updateFunctionDeclaration(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), (node).asteriskToken, visitNode((node).name, visitor, isIdentifier), - visitNodes((node).typeParameters, visitor, isTypeParameter), - visitParameterList((node).parameters, visitor, context), + nodesVisitor((node).typeParameters, visitor, isTypeParameter), + visitParameterList((node).parameters, visitor, context, nodesVisitor), visitNode((node).type, visitor, isTypeNode), visitFunctionBody((node).body, visitor, context)); case SyntaxKind.ClassDeclaration: return updateClassDeclaration(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).name, visitor, isIdentifier), - visitNodes((node).typeParameters, visitor, isTypeParameter), - visitNodes((node).heritageClauses, visitor, isHeritageClause), - visitNodes((node).members, visitor, isClassElement)); + nodesVisitor((node).typeParameters, visitor, isTypeParameter), + nodesVisitor((node).heritageClauses, visitor, isHeritageClause), + nodesVisitor((node).members, visitor, isClassElement)); case SyntaxKind.EnumDeclaration: return updateEnumDeclaration(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).name, visitor, isIdentifier), - visitNodes((node).members, visitor, isEnumMember)); + nodesVisitor((node).members, visitor, isEnumMember)); case SyntaxKind.ModuleDeclaration: return updateModuleDeclaration(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).name, visitor, isIdentifier), visitNode((node).body, visitor, isModuleBody)); case SyntaxKind.ModuleBlock: return updateModuleBlock(node, - visitNodes((node).statements, visitor, isStatement)); + nodesVisitor((node).statements, visitor, isStatement)); case SyntaxKind.CaseBlock: return updateCaseBlock(node, - visitNodes((node).clauses, visitor, isCaseOrDefaultClause)); + nodesVisitor((node).clauses, visitor, isCaseOrDefaultClause)); case SyntaxKind.ImportEqualsDeclaration: return updateImportEqualsDeclaration(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).name, visitor, isIdentifier), visitNode((node).moduleReference, visitor, isModuleReference)); case SyntaxKind.ImportDeclaration: return updateImportDeclaration(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).importClause, visitor, isImportClause), visitNode((node).moduleSpecifier, visitor, isExpression)); @@ -618,7 +618,7 @@ namespace ts { case SyntaxKind.NamedImports: return updateNamedImports(node, - visitNodes((node).elements, visitor, isImportSpecifier)); + nodesVisitor((node).elements, visitor, isImportSpecifier)); case SyntaxKind.ImportSpecifier: return updateImportSpecifier(node, @@ -627,20 +627,20 @@ namespace ts { case SyntaxKind.ExportAssignment: return updateExportAssignment(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).expression, visitor, isExpression)); case SyntaxKind.ExportDeclaration: return updateExportDeclaration(node, - visitNodes((node).decorators, visitor, isDecorator), - visitNodes((node).modifiers, visitor, isModifier), + nodesVisitor((node).decorators, visitor, isDecorator), + nodesVisitor((node).modifiers, visitor, isModifier), visitNode((node).exportClause, visitor, isNamedExports), visitNode((node).moduleSpecifier, visitor, isExpression)); case SyntaxKind.NamedExports: return updateNamedExports(node, - visitNodes((node).elements, visitor, isExportSpecifier)); + nodesVisitor((node).elements, visitor, isExportSpecifier)); case SyntaxKind.ExportSpecifier: return updateExportSpecifier(node, @@ -656,12 +656,12 @@ namespace ts { case SyntaxKind.JsxElement: return updateJsxElement(node, visitNode((node).openingElement, visitor, isJsxOpeningElement), - visitNodes((node).children, visitor, isJsxChild), + nodesVisitor((node).children, visitor, isJsxChild), visitNode((node).closingElement, visitor, isJsxClosingElement)); case SyntaxKind.JsxAttributes: return updateJsxAttributes(node, - visitNodes((node).properties, visitor, isJsxAttributeLike)); + nodesVisitor((node).properties, visitor, isJsxAttributeLike)); case SyntaxKind.JsxSelfClosingElement: return updateJsxSelfClosingElement(node, @@ -694,15 +694,15 @@ namespace ts { case SyntaxKind.CaseClause: return updateCaseClause(node, visitNode((node).expression, visitor, isExpression), - visitNodes((node).statements, visitor, isStatement)); + nodesVisitor((node).statements, visitor, isStatement)); case SyntaxKind.DefaultClause: return updateDefaultClause(node, - visitNodes((node).statements, visitor, isStatement)); + nodesVisitor((node).statements, visitor, isStatement)); case SyntaxKind.HeritageClause: return updateHeritageClause(node, - visitNodes((node).types, visitor, isExpressionWithTypeArguments)); + nodesVisitor((node).types, visitor, isExpressionWithTypeArguments)); case SyntaxKind.CatchClause: return updateCatchClause(node, diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 35788ebca80..d7638ffc033 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -22,6 +22,10 @@ namespace FourSlash { ts.disableIncrementalParsing = false; + function normalizeNewLines(s: string) { + return s.replace(/\r\n/g, "\n"); + } + // Represents a parsed source file with metadata export interface FourSlashFile { // The contents of the file (with markers, etc stripped out) @@ -1958,8 +1962,7 @@ namespace FourSlash { public verifyCurrentFileContent(text: string) { const actual = this.getFileContent(this.activeFile.fileName); - const replaceNewlines = (str: string) => str.replace(/\r\n/g, "\n"); - if (replaceNewlines(actual) !== replaceNewlines(text)) { + if (normalizeNewLines(actual) !== normalizeNewLines(text)) { throw new Error("verifyCurrentFileContent\n" + "\tExpected: \"" + text + "\"\n" + "\t Actual: \"" + actual + "\""); @@ -2135,7 +2138,7 @@ namespace FourSlash { const actualText = this.rangeText(ranges[0]); const result = includeWhiteSpace - ? actualText === expectedText + ? normalizeNewLines(actualText) === normalizeNewLines(expectedText) : this.removeWhitespace(actualText) === this.removeWhitespace(expectedText); if (!result) { @@ -2185,7 +2188,7 @@ namespace FourSlash { continue; } - const newActions = this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.length, [diagnostic.code]); + const newActions = this.languageService.getCodeFixesAtPosition(fileName, diagnostic.start, diagnostic.length, [diagnostic.code], this.formatCodeSettings); if (newActions && newActions.length) { actions = actions ? actions.concat(newActions) : newActions; } diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 21622325368..1bbdfa71280 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -125,6 +125,7 @@ "./unittests/projectErrors.ts", "./unittests/printer.ts", "./unittests/transform.ts", - "./unittests/customTransforms.ts" + "./unittests/customTransforms.ts", + "./unittests/textChanges.ts" ] } diff --git a/src/harness/unittests/textChanges.ts b/src/harness/unittests/textChanges.ts new file mode 100644 index 00000000000..8269b6de6be --- /dev/null +++ b/src/harness/unittests/textChanges.ts @@ -0,0 +1,791 @@ +/// +/// +/// + +namespace ts { + describe("textChanges", () => { + function findChild(name: string, n: Node) { + return find(n); + + function find(node: Node): Node { + if (isDeclaration(node) && node.name && isIdentifier(node.name) && node.name.text === name) { + return node; + } + else { + return forEachChild(node, find); + } + } + } + + const printerOptions = { newLine: NewLineKind.LineFeed }; + const newLineCharacter = getNewLineCharacter(printerOptions); + + function getRuleProvider(action?: (opts: FormatCodeSettings) => void) { + const options = { + indentSize: 4, + tabSize: 4, + newLineCharacter, + convertTabsToSpaces: true, + indentStyle: ts.IndentStyle.Smart, + insertSpaceAfterConstructor: false, + insertSpaceAfterCommaDelimiter: true, + insertSpaceAfterSemicolonInForStatements: true, + insertSpaceBeforeAndAfterBinaryOperators: true, + insertSpaceAfterKeywordsInControlFlowStatements: true, + insertSpaceAfterFunctionKeywordForAnonymousFunctions: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: false, + insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: true, + insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: false, + insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: false, + insertSpaceBeforeFunctionParenthesis: false, + placeOpenBraceOnNewLineForFunctions: false, + placeOpenBraceOnNewLineForControlBlocks: false, + }; + if (action) { + action(options); + } + const rulesProvider = new formatting.RulesProvider(); + rulesProvider.ensureUpToDate(options); + return rulesProvider; + } + + // validate that positions that were recovered from the printed text actually match positions that will be created if the same text is parsed. + function verifyPositions({ text, node }: textChanges.NonFormattedText): void { + const nodeList = flattenNodes(node); + const sourceFile = createSourceFile("f.ts", text, ScriptTarget.ES2015); + const parsedNodeList = flattenNodes(sourceFile.statements[0]); + Debug.assert(nodeList.length === parsedNodeList.length); + for (let i = 0; i < nodeList.length; i++) { + const left = nodeList[i]; + const right = parsedNodeList[i]; + Debug.assert(left.pos === right.pos); + Debug.assert(left.end === right.end); + } + + function flattenNodes(n: Node) { + const data: (Node | NodeArray)[] = []; + walk(n); + return data; + + function walk(n: Node | Node[]): void { + data.push(n); + return isArray(n) ? forEach(n, walk) : forEachChild(n, walk, walk); + } + } + } + + function runSingleFileTest(caption: string, setupFormatOptions: (opts: FormatCodeSettings) => void, text: string, validateNodes: boolean, testBlock: (sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker) => void) { + it(caption, () => { + Harness.Baseline.runBaseline(`textChanges/${caption}.js`, () => { + const sourceFile = createSourceFile("source.ts", text, ScriptTarget.ES2015, /*setParentNodes*/ true); + const rulesProvider = getRuleProvider(setupFormatOptions); + const changeTracker = new textChanges.ChangeTracker(printerOptions.newLine, rulesProvider, validateNodes ? verifyPositions : undefined); + testBlock(sourceFile, changeTracker); + const changes = changeTracker.getChanges(); + assert.equal(changes.length, 1); + assert.equal(changes[0].fileName, sourceFile.fileName); + const modified = textChanges.applyChanges(sourceFile.text, changes[0].textChanges); + return `===ORIGINAL===${newLineCharacter}${text}${newLineCharacter}===MODIFIED===${newLineCharacter}${modified}`; + }); + }); + } + + function setNewLineForOpenBraceInFunctions(opts: FormatCodeSettings) { + opts.placeOpenBraceOnNewLineForFunctions = true; + } + + { + const text = ` +namespace M +{ + namespace M2 + { + function foo() { + // comment 1 + const x = 1; + + /** + * comment 2 line 1 + * comment 2 line 2 + */ + function f() { + return 100; + } + const y = 2; // comment 3 + return 1; + } + } +}`; + runSingleFileTest("extractMethodLike", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + const statements = ((findChild("foo", sourceFile)).body).statements.slice(1); + const newFunction = createFunctionDeclaration( + /*decorators*/ undefined, + /*modifiers*/ undefined, + /*asteriskToken*/ undefined, + /*name*/ "bar", + /*typeParameters*/ undefined, + /*parameters*/ emptyArray, + /*type*/ createKeywordTypeNode(SyntaxKind.AnyKeyword), + /*body */ createBlock(statements) + ); + + changeTracker.insertNodeBefore(sourceFile, /*before*/findChild("M2", sourceFile), newFunction, { suffix: newLineCharacter }); + + // replace statements with return statement + const newStatement = createReturn( + createCall( + /*expression*/ newFunction.name, + /*typeArguments*/ undefined, + /*argumentsArray*/ emptyArray + )); + changeTracker.replaceNodeRange(sourceFile, statements[0], lastOrUndefined(statements), newStatement, { suffix: newLineCharacter }); + }); + } + { + const text = ` +function foo() { + return 1; +} + +function bar() { + return 2; +} +`; + runSingleFileTest("deleteRange1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteRange(sourceFile, { pos: text.indexOf("function foo"), end: text.indexOf("function bar") }); + }); + } + function findVariableStatementContaining(name: string, sourceFile: SourceFile) { + const varDecl = findChild(name, sourceFile); + assert.equal(varDecl.kind, SyntaxKind.VariableDeclaration); + const varStatement = varDecl.parent.parent; + assert.equal(varStatement.kind, SyntaxKind.VariableStatement); + return varStatement; + } + { + const text = ` +var x = 1; // some comment - 1 +/** + * comment 2 + */ +var y = 2; // comment 3 +var z = 3; // comment 4 +`; + runSingleFileTest("deleteNode1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNode(sourceFile, findVariableStatementContaining("y", sourceFile)); + }); + runSingleFileTest("deleteNode2", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNode(sourceFile, findVariableStatementContaining("y", sourceFile), { useNonAdjustedStartPosition: true }); + }); + runSingleFileTest("deleteNode3", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNode(sourceFile, findVariableStatementContaining("y", sourceFile), { useNonAdjustedEndPosition: true }); + }); + runSingleFileTest("deleteNode4", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNode(sourceFile, findVariableStatementContaining("y", sourceFile), { useNonAdjustedStartPosition: true, useNonAdjustedEndPosition: true }); + }); + runSingleFileTest("deleteNode5", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNode(sourceFile, findVariableStatementContaining("x", sourceFile)); + }); + } + { + const text = ` +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +`; + runSingleFileTest("deleteNodeRange1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile)); + }); + runSingleFileTest("deleteNodeRange2", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), + { useNonAdjustedStartPosition: true }); + }); + runSingleFileTest("deleteNodeRange3", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), + { useNonAdjustedEndPosition: true }); + }); + runSingleFileTest("deleteNodeRange4", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), + { useNonAdjustedStartPosition: true, useNonAdjustedEndPosition: true }); + }); + } + function createTestVariableDeclaration(name: string) { + return createVariableDeclaration(name, /*type*/ undefined, createObjectLiteral([createPropertyAssignment("p1", createLiteral(1))], /*multiline*/ true)); + } + function createTestClass() { + return createClassDeclaration( + /*decorators*/ undefined, + [ + createToken(SyntaxKind.PublicKeyword) + ], + "class1", + /*typeParameters*/ undefined, + [ + createHeritageClause( + SyntaxKind.ImplementsKeyword, + [ + createExpressionWithTypeArguments(/*typeArguments*/ undefined, createIdentifier("interface1")) + ] + ) + ], + [ + createProperty( + /*decorators*/ undefined, + /*modifiers*/ undefined, + "property1", + /*questionToken*/ undefined, + createKeywordTypeNode(SyntaxKind.BooleanKeyword), + /*initializer*/ undefined + ) + ] + ); + } + { + const text = ` +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7`; + runSingleFileTest("replaceRange", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceRange(sourceFile, { pos: text.indexOf("var y"), end: text.indexOf("var a") }, createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("replaceRangeWithForcedIndentation", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceRange(sourceFile, { pos: text.indexOf("var y"), end: text.indexOf("var a") }, createTestClass(), { suffix: newLineCharacter, indentation: 8, delta: 0 }); + }); + + runSingleFileTest("replaceRangeNoLineBreakBefore", setNewLineForOpenBraceInFunctions, `const x = 1, y = "2";`, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createTestVariableDeclaration("z1"); + changeTracker.replaceRange(sourceFile, { pos: sourceFile.text.indexOf("y"), end: sourceFile.text.indexOf(";") }, newNode); + }); + } + { + const text = ` +namespace A { + const x = 1, y = "2"; +} +`; + runSingleFileTest("replaceNode1NoLineBreakBefore", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + const newNode = createTestVariableDeclaration("z1"); + changeTracker.replaceNode(sourceFile, findChild("y", sourceFile), newNode); + }); + } + { + const text = ` +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7`; + runSingleFileTest("replaceNode1", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNode2", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { useNonAdjustedStartPosition: true, suffix: newLineCharacter, prefix: newLineCharacter }); + }); + runSingleFileTest("replaceNode3", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { useNonAdjustedEndPosition: true, suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNode4", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { useNonAdjustedStartPosition: true, useNonAdjustedEndPosition: true }); + }); + runSingleFileTest("replaceNode5", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNode(sourceFile, findVariableStatementContaining("x", sourceFile), createTestClass(), { useNonAdjustedStartPosition: true, useNonAdjustedEndPosition: true }); + }); + } + { + const text = ` +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7`; + runSingleFileTest("replaceNodeRange1", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNodeRange2", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { useNonAdjustedStartPosition: true, suffix: newLineCharacter, prefix: newLineCharacter }); + }); + runSingleFileTest("replaceNodeRange3", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { useNonAdjustedEndPosition: true, suffix: newLineCharacter }); + }); + runSingleFileTest("replaceNodeRange4", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.replaceNodeRange(sourceFile, findVariableStatementContaining("y", sourceFile), findVariableStatementContaining("z", sourceFile), createTestClass(), { useNonAdjustedStartPosition: true, useNonAdjustedEndPosition: true }); + }); + } + { + const text = ` +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7`; + runSingleFileTest("insertNodeAt1", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeAt(sourceFile, text.indexOf("var y"), createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("insertNodeAt2", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAt(sourceFile, text.indexOf("; // comment 4"), createTestVariableDeclaration("z1")); + }); + } + { + const text = ` +namespace M { + // comment 1 + var x = 1; // comment 2 + // comment 3 + var y; // comment 4 + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +}`; + runSingleFileTest("insertNodeBefore1", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeBefore(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("insertNodeBefore2", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeBefore(sourceFile, findChild("M", sourceFile), createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("insertNodeAfter1", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("y", sourceFile), createTestClass(), { suffix: newLineCharacter }); + }); + runSingleFileTest("insertNodeAfter2", setNewLineForOpenBraceInFunctions, text, /*validateNodes*/ true, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findChild("M", sourceFile), createTestClass(), { prefix: newLineCharacter }); + }); + } + { + function findOpenBraceForConstructor(sourceFile: SourceFile) { + const classDecl = sourceFile.statements[0]; + const constructorDecl = forEach(classDecl.members, m => m.kind === SyntaxKind.Constructor && (m).body && m); + return constructorDecl.body.getFirstToken(); + } + function createTestSuperCall() { + const superCall = createCall( + createSuper(), + /*typeArguments*/ undefined, + /*argumentsArray*/ emptyArray + ); + return createStatement(superCall); + } + const text1 = ` +class A { + constructor() { + } +} +`; + runSingleFileTest("insertNodeAfter3", noop, text1, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findOpenBraceForConstructor(sourceFile), createTestSuperCall(), { suffix: newLineCharacter }); + }); + const text2 = ` +class A { + constructor() { + var x = 1; + } +} +`; + runSingleFileTest("insertNodeAfter4", noop, text2, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findVariableStatementContaining("x", sourceFile), createTestSuperCall(), { suffix: newLineCharacter }); + }); + const text3 = ` +class A { + constructor() { + + } +} +`; + runSingleFileTest("insertNodeAfter3-block with newline", noop, text3, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findOpenBraceForConstructor(sourceFile), createTestSuperCall(), { suffix: newLineCharacter }); + }); + } + { + const text = `var a = 1, b = 2, c = 3;`; + runSingleFileTest("deleteNodeInList1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList2", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList3", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = `var a = 1,b = 2,c = 3;`; + runSingleFileTest("deleteNodeInList1_1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList2_1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList3_1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` +namespace M { + var a = 1, + b = 2, + c = 3; +}`; + runSingleFileTest("deleteNodeInList4", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList5", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList6", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` +namespace M { + var a = 1, // comment 1 + // comment 2 + b = 2, // comment 3 + // comment 4 + c = 3; // comment 5 +}`; + runSingleFileTest("deleteNodeInList4_1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList5_1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList6_1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` +function foo(a: number, b: string, c = true) { + return 1; +}`; + runSingleFileTest("deleteNodeInList7", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList8", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList9", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` +function foo(a: number,b: string,c = true) { + return 1; +}`; + runSingleFileTest("deleteNodeInList10", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList11", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList12", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` +function foo( + a: number, + b: string, + c = true) { + return 1; +}`; + runSingleFileTest("deleteNodeInList13", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("a", sourceFile)); + }); + runSingleFileTest("deleteNodeInList14", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("b", sourceFile)); + }); + runSingleFileTest("deleteNodeInList15", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNodeInList(sourceFile, findChild("c", sourceFile)); + }); + } + { + const text = ` +const x = 1, y = 2;`; + runSingleFileTest("insertNodeInListAfter1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter2", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` +const /*x*/ x = 1, /*y*/ y = 2;`; + runSingleFileTest("insertNodeInListAfter3", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter4", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` +const x = 1;`; + runSingleFileTest("insertNodeInListAfter5", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` +const x = 1, + y = 2;`; + runSingleFileTest("insertNodeInListAfter6", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter7", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` +const /*x*/ x = 1, + /*y*/ y = 2;`; + runSingleFileTest("insertNodeInListAfter8", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + runSingleFileTest("insertNodeInListAfter9", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("y", sourceFile), createVariableDeclaration("z", /*type*/ undefined, createLiteral(1))); + }); + } + { + const text = ` +import { + x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter10", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); + }) + } + { + const text = ` +import { + x // this is x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter11", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); + }) + } + { + const text = ` +import { + x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter12", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }) + } + { + const text = ` +import { + x // this is x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter13", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }) + } + { + const text = ` +import { + x0, + x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter14", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); + }) + } + { + const text = ` +import { + x0, + x // this is x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter15", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(createIdentifier("b"), createIdentifier("a"))); + }) + } + { + const text = ` +import { + x0, + x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter16", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }) + } + { + const text = ` +import { + x0, + x // this is x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter17", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }) + } + { + const text = ` +import { + x0, x +} from "bar"`; + runSingleFileTest("insertNodeInListAfter18", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeInListAfter(sourceFile, findChild("x", sourceFile), createImportSpecifier(undefined, createIdentifier("a"))); + }) + } + { + const text = ` +class A { + x; +}`; + runSingleFileTest("insertNodeAfterMultipleNodes", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + let newNodes = []; + for (let i = 0; i < 11 /*error doesn't occur with fewer nodes*/; ++i) { + newNodes.push( + createProperty(undefined, undefined, i + "", undefined, undefined, undefined)); + } + const insertAfter = findChild("x", sourceFile); + for (const newNode of newNodes) { + changeTracker.insertNodeAfter(sourceFile, insertAfter, newNode, { suffix: newLineCharacter }); + } + }); + } + { + const text = ` +class A { + x +} +`; + runSingleFileTest("insertNodeAfterInClass1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), createProperty(undefined, undefined, "a", undefined, createKeywordTypeNode(SyntaxKind.BooleanKeyword), undefined), { suffix: newLineCharacter }); + }); + } + { + const text = ` +class A { + x; +} +`; + runSingleFileTest("insertNodeAfterInClass2", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.insertNodeAfter(sourceFile, findChild("x", sourceFile), createProperty(undefined, undefined, "a", undefined, createKeywordTypeNode(SyntaxKind.BooleanKeyword), undefined), { suffix: newLineCharacter }); + }); + } + { + const text = ` +class A { + x; + y = 1; +} +`; + runSingleFileTest("deleteNodeAfterInClass1", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + changeTracker.deleteNode(sourceFile, findChild("x", sourceFile)); + }); + } + { + const text = ` +class A { + x + y = 1; +} +`; + runSingleFileTest("deleteNodeAfterInClass2", noop, text, /*validateNodes*/ false, (sourceFile, changeTracker) => { + 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/server/session.ts b/src/server/session.ts index b5e7e44970d..64fce61a095 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1420,8 +1420,9 @@ namespace ts.server { const scriptInfo = project.getScriptInfoForNormalizedPath(file); const startPosition = getStartPosition(); const endPosition = getEndPosition(); + const formatOptions = this.projectService.getFormatCodeOptions(file); - const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes); + const codeActions = project.getLanguageService().getCodeFixesAtPosition(file, startPosition, endPosition, args.errorCodes, formatOptions); if (!codeActions) { return undefined; } diff --git a/src/services/codeFixProvider.ts b/src/services/codeFixProvider.ts index c22ba779d19..bab5356e99c 100644 --- a/src/services/codeFixProvider.ts +++ b/src/services/codeFixProvider.ts @@ -13,6 +13,7 @@ namespace ts { newLineCharacter: string; host: LanguageServiceHost; cancellationToken: CancellationToken; + rulesProvider: formatting.RulesProvider; } export namespace codefix { diff --git a/src/services/codefixes/fixClassSuperMustPrecedeThisAccess.ts b/src/services/codefixes/fixClassSuperMustPrecedeThisAccess.ts index 4528c48774d..6d7efb64c6f 100644 --- a/src/services/codefixes/fixClassSuperMustPrecedeThisAccess.ts +++ b/src/services/codefixes/fixClassSuperMustPrecedeThisAccess.ts @@ -26,22 +26,13 @@ namespace ts.codefix { } } } - - const newPosition = getOpenBraceEnd(constructor, sourceFile); - const changes = [{ - fileName: sourceFile.fileName, textChanges: [{ - newText: superCall.getText(sourceFile), - span: { start: newPosition, length: 0 } - }, - { - newText: "", - span: { start: superCall.getStart(sourceFile), length: superCall.getWidth(sourceFile) } - }] - }]; + const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); + changeTracker.insertNodeAfter(sourceFile, getOpenBrace(constructor, sourceFile), superCall, { suffix: context.newLineCharacter }); + changeTracker.deleteNode(sourceFile, superCall); return [{ description: getLocaleSpecificMessage(Diagnostics.Make_super_call_the_first_statement_in_the_constructor), - changes + changes: changeTracker.getChanges() }]; function findSuperCall(n: Node): ExpressionStatement { diff --git a/src/services/codefixes/fixConstructorForDerivedNeedSuperCall.ts b/src/services/codefixes/fixConstructorForDerivedNeedSuperCall.ts index 0dede87cf28..822c34842fd 100644 --- a/src/services/codefixes/fixConstructorForDerivedNeedSuperCall.ts +++ b/src/services/codefixes/fixConstructorForDerivedNeedSuperCall.ts @@ -10,10 +10,13 @@ namespace ts.codefix { return undefined; } - const newPosition = getOpenBraceEnd(token.parent, sourceFile); + const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); + const superCall = createStatement(createCall(createSuper(), /*typeArguments*/ undefined, /*argumentsArray*/ emptyArray)); + changeTracker.insertNodeAfter(sourceFile, getOpenBrace(token.parent, sourceFile), superCall, { suffix: context.newLineCharacter }); + return [{ description: getLocaleSpecificMessage(Diagnostics.Add_missing_super_call), - changes: [{ fileName: sourceFile.fileName, textChanges: [{ newText: "super();", span: { start: newPosition, length: 0 } }] }] + changes: changeTracker.getChanges() }]; } }); diff --git a/src/services/codefixes/fixExtendsInterfaceBecomesImplements.ts b/src/services/codefixes/fixExtendsInterfaceBecomesImplements.ts index 1e72700ec01..80d4345a943 100644 --- a/src/services/codefixes/fixExtendsInterfaceBecomesImplements.ts +++ b/src/services/codefixes/fixExtendsInterfaceBecomesImplements.ts @@ -21,26 +21,20 @@ namespace ts.codefix { return undefined; } - let changeStart = extendsToken.getStart(sourceFile); - let changeEnd = extendsToken.getEnd(); - const textChanges: TextChange[] = [{ newText: " implements", span: { start: changeStart, length: changeEnd - changeStart } }]; + const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); + changeTracker.replaceNode(sourceFile, extendsToken, createToken(SyntaxKind.ImplementsKeyword)); // We replace existing keywords with commas. for (let i = 1; i < heritageClauses.length; i++) { const keywordToken = heritageClauses[i].getFirstToken(); if (keywordToken) { - changeStart = keywordToken.getStart(sourceFile); - changeEnd = keywordToken.getEnd(); - textChanges.push({ newText: ",", span: { start: changeStart, length: changeEnd - changeStart } }); + changeTracker.replaceNode(sourceFile, keywordToken, createToken(SyntaxKind.CommaToken)); } } const result = [{ description: getLocaleSpecificMessage(Diagnostics.Change_extends_to_implements), - changes: [{ - fileName: sourceFile.fileName, - textChanges: textChanges - }] + changes: changeTracker.getChanges() }]; return result; diff --git a/src/services/codefixes/fixForgottenThisPropertyAccess.ts b/src/services/codefixes/fixForgottenThisPropertyAccess.ts index c9d59dd8337..711a3289a27 100644 --- a/src/services/codefixes/fixForgottenThisPropertyAccess.ts +++ b/src/services/codefixes/fixForgottenThisPropertyAccess.ts @@ -5,11 +5,15 @@ namespace ts.codefix { getCodeActions: (context: CodeFixContext) => { const sourceFile = context.sourceFile; const token = getTokenAtPosition(sourceFile, context.span.start); - const start = token.getStart(sourceFile); + if (token.kind !== SyntaxKind.Identifier) { + return undefined; + } + const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context); + changeTracker.replaceNode(sourceFile, token, createPropertyAccess(createThis(), token)); return [{ description: getLocaleSpecificMessage(Diagnostics.Add_this_to_unresolved_variable), - changes: [{ fileName: sourceFile.fileName, textChanges: [{ newText: "this.", span: { start, length: 0 } }] }] + changes: changeTracker.getChanges() }]; } }); diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 3e07efbc3de..743c13ccf38 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -130,7 +130,7 @@ namespace ts.codefix { // this is a module id -> module import declaration map const cachedImportDeclarations: (ImportDeclaration | ImportEqualsDeclaration)[][] = []; - let cachedNewImportInsertPosition: number; + let lastImportDeclaration: Node; const currentTokenMeaning = getMeaningFromLocation(token); if (context.errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code) { @@ -138,14 +138,14 @@ namespace ts.codefix { return getCodeActionForImport(symbol, /*isDefault*/ false, /*isNamespaceImport*/ true); } - const allPotentialModules = checker.getAmbientModules(); + const candidateModules = checker.getAmbientModules(); for (const otherSourceFile of allSourceFiles) { if (otherSourceFile !== sourceFile && isExternalOrCommonJsModule(otherSourceFile)) { - allPotentialModules.push(otherSourceFile.symbol); + candidateModules.push(otherSourceFile.symbol); } } - for (const moduleSymbol of allPotentialModules) { + for (const moduleSymbol of candidateModules) { context.cancellationToken.throwIfCancellationRequested(); // check the default export @@ -277,14 +277,12 @@ namespace ts.codefix { * If the existing import declaration already has a named import list, just * insert the identifier into that list. */ - const textChange = getTextChangeForImportClause(namedImportDeclaration.importClause); + const fileTextChanges = getTextChangeForImportClause(namedImportDeclaration.importClause); const moduleSpecifierWithoutQuotes = stripQuotes(namedImportDeclaration.moduleSpecifier.getText()); actions.push(createCodeAction( Diagnostics.Add_0_to_existing_import_declaration_from_1, [name, moduleSpecifierWithoutQuotes], - textChange.newText, - textChange.span, - sourceFile.fileName, + fileTextChanges, "InsertingIntoExistingImport", moduleSpecifierWithoutQuotes )); @@ -302,49 +300,31 @@ namespace ts.codefix { return declaration.moduleReference.getText(); } - function getTextChangeForImportClause(importClause: ImportClause): TextChange { - const newImportText = isDefault ? `default as ${name}` : name; + function getTextChangeForImportClause(importClause: ImportClause): FileTextChanges[] { + //const newImportText = isDefault ? `default as ${name}` : name; const importList = importClause.namedBindings; + const newImportSpecifier = createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name)); // case 1: // original text: import default from "module" // change to: import default, { name } from "module" - if (!importList && importClause.name) { - const start = importClause.name.getEnd(); - return { - newText: `, { ${newImportText} }`, - span: { start, length: 0 } - }; - } - // case 2: // original text: import {} from "module" // change to: import { name } from "module" - if (importList.elements.length === 0) { - const start = importList.getStart(); - return { - newText: `{ ${newImportText} }`, - span: { start, length: importList.getEnd() - start } - }; + if (!importList || importList.elements.length === 0) { + const newImportClause = createImportClause(importClause.name, createNamedImports([newImportSpecifier])); + return createChangeTracker().replaceNode(sourceFile, importClause, newImportClause).getChanges(); } - // case 3: - // original text: import { foo, bar } from "module" - // change to: import { foo, bar, name } from "module" - const insertPoint = importList.elements[importList.elements.length - 1].getEnd(); /** * If the import list has one import per line, preserve that. Otherwise, insert on same line as last element * import { * foo * } from "./module"; */ - const startLine = getLineOfLocalPosition(sourceFile, importList.getStart()); - const endLine = getLineOfLocalPosition(sourceFile, importList.getEnd()); - const oneImportPerLine = endLine - startLine > importList.elements.length; - - return { - newText: `,${oneImportPerLine ? context.newLineCharacter : " "}${newImportText}`, - span: { start: insertPoint, length: 0 } - }; + return createChangeTracker().insertNodeInListAfter( + sourceFile, + importList.elements[importList.elements.length - 1], + newImportSpecifier).getChanges(); } function getCodeActionForNamespaceImport(declaration: ImportDeclaration | ImportEqualsDeclaration): ImportCodeAction { @@ -370,48 +350,47 @@ namespace ts.codefix { return createCodeAction( Diagnostics.Change_0_to_1, [name, `${namespacePrefix}.${name}`], - `${namespacePrefix}.`, - { start: token.getStart(), length: 0 }, - sourceFile.fileName, + createChangeTracker().replaceNode(sourceFile, token, createPropertyAccess(createIdentifier(namespacePrefix), name)).getChanges(), "CodeChange" ); } } function getCodeActionForNewImport(moduleSpecifier?: string): ImportCodeAction { - if (!cachedNewImportInsertPosition) { + if (!lastImportDeclaration) { // insert after any existing imports - let lastModuleSpecifierEnd = -1; - for (const moduleSpecifier of sourceFile.imports) { - const end = moduleSpecifier.getEnd(); - if (!lastModuleSpecifierEnd || end > lastModuleSpecifierEnd) { - lastModuleSpecifierEnd = end; + for (let i = sourceFile.statements.length - 1; i >= 0; i--) { + const statement = sourceFile.statements[i]; + if (statement.kind === SyntaxKind.ImportEqualsDeclaration || statement.kind === SyntaxKind.ImportDeclaration) { + lastImportDeclaration = statement; + break; } } - cachedNewImportInsertPosition = lastModuleSpecifierEnd > 0 ? sourceFile.getLineEndOfPosition(lastModuleSpecifierEnd) : sourceFile.getStart(); } const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); const moduleSpecifierWithoutQuotes = stripQuotes(moduleSpecifier || getModuleSpecifierForNewImport()); - const importStatementText = isDefault - ? `import ${name} from "${moduleSpecifierWithoutQuotes}"` + const changeTracker = createChangeTracker(); + const importClause = isDefault + ? createImportClause(createIdentifier(name), /*namedBindings*/ undefined) : isNamespaceImport - ? `import * as ${name} from "${moduleSpecifierWithoutQuotes}"` - : `import { ${name} } from "${moduleSpecifierWithoutQuotes}"`; + ? createImportClause(/*name*/ undefined, createNamespaceImport(createIdentifier(name))) + : createImportClause(/*name*/ undefined, createNamedImports([createImportSpecifier(/*propertyName*/ undefined, createIdentifier(name))])); + const importDecl = createImportDeclaration(/*decorators*/ undefined, /*modifiers*/ undefined, importClause, createLiteral(moduleSpecifierWithoutQuotes)); + if (!lastImportDeclaration) { + changeTracker.insertNodeAt(sourceFile, sourceFile.getStart(), importDecl, { suffix: `${context.newLineCharacter}${context.newLineCharacter}` }); + } + else { + changeTracker.insertNodeAfter(sourceFile, lastImportDeclaration, importDecl, { suffix: context.newLineCharacter }); + } // if this file doesn't have any import statements, insert an import statement and then insert a new line // between the only import statement and user code. Otherwise just insert the statement because chances // are there are already a new line seperating code and import statements. - const newText = cachedNewImportInsertPosition === sourceFile.getStart() - ? `${importStatementText};${context.newLineCharacter}${context.newLineCharacter}` - : `${context.newLineCharacter}${importStatementText};`; - return createCodeAction( Diagnostics.Import_0_from_1, [name, `"${moduleSpecifierWithoutQuotes}"`], - newText, - { start: cachedNewImportInsertPosition, length: 0 }, - sourceFile.fileName, + changeTracker.getChanges(), "NewImport", moduleSpecifierWithoutQuotes ); @@ -576,17 +555,19 @@ namespace ts.codefix { } + function createChangeTracker() { + return textChanges.ChangeTracker.fromCodeFixContext(context);; + } + function createCodeAction( description: DiagnosticMessage, diagnosticArgs: string[], - newText: string, - span: TextSpan, - fileName: string, + changes: FileTextChanges[], kind: ImportCodeActionKind, moduleSpecifier?: string): ImportCodeAction { return { description: formatMessage.apply(undefined, [undefined, description].concat(diagnosticArgs)), - changes: [{ fileName, textChanges: [{ newText, span }] }], + changes, kind, moduleSpecifier }; diff --git a/src/services/codefixes/unusedIdentifierFixes.ts b/src/services/codefixes/unusedIdentifierFixes.ts index 61d48bdc3bc..f77f9723d0f 100644 --- a/src/services/codefixes/unusedIdentifierFixes.ts +++ b/src/services/codefixes/unusedIdentifierFixes.ts @@ -25,17 +25,17 @@ namespace ts.codefix { const forStatement = token.parent.parent.parent; const forInitializer = forStatement.initializer; if (forInitializer.declarations.length === 1) { - return createCodeFixToRemoveNode(forInitializer); + return deleteNode(forInitializer); } else { - return removeSingleItem(forInitializer.declarations, token); + return deleteNodeInList(token.parent); } case SyntaxKind.ForOfStatement: const forOfStatement = token.parent.parent.parent; if (forOfStatement.initializer.kind === SyntaxKind.VariableDeclarationList) { const forOfInitializer = forOfStatement.initializer; - return createCodeFix("{}", forOfInitializer.declarations[0].getStart(), forOfInitializer.declarations[0].getWidth()); + return replaceNode(forOfInitializer.declarations[0], createObjectLiteral()); } break; @@ -47,51 +47,59 @@ namespace ts.codefix { case SyntaxKind.CatchClause: const catchClause = token.parent.parent; const parameter = catchClause.variableDeclaration.getChildren()[0]; - return createCodeFixToRemoveNode(parameter); + return deleteNode(parameter); default: const variableStatement = token.parent.parent.parent; if (variableStatement.declarationList.declarations.length === 1) { - return createCodeFixToRemoveNode(variableStatement); + return deleteNode(variableStatement); } else { - const declarations = variableStatement.declarationList.declarations; - return removeSingleItem(declarations, token); + return deleteNodeInList(token.parent); } } case SyntaxKind.TypeParameter: const typeParameters = (token.parent.parent).typeParameters; if (typeParameters.length === 1) { - return createCodeFix("", token.parent.pos - 1, token.parent.end - token.parent.pos + 2); + const previousToken = getTokenAtPosition(sourceFile, typeParameters.pos - 1); + if (!previousToken || previousToken.kind !== SyntaxKind.LessThanToken) { + return deleteRange(typeParameters); + } + const nextToken = getTokenAtPosition(sourceFile, typeParameters.end); + if (!nextToken || nextToken.kind !== SyntaxKind.GreaterThanToken) { + return deleteRange(typeParameters); + } + return deleteNodeRange(previousToken, nextToken); } else { - return removeSingleItem(typeParameters, token); + return deleteNodeInList(token.parent); } case ts.SyntaxKind.Parameter: const functionDeclaration = token.parent.parent; if (functionDeclaration.parameters.length === 1) { - return createCodeFixToRemoveNode(token.parent); + return deleteNode(token.parent); } else { - return removeSingleItem(functionDeclaration.parameters, token); + return deleteNodeInList(token.parent); } // handle case where 'import a = A;' case SyntaxKind.ImportEqualsDeclaration: - const importEquals = findImportDeclaration(token); - return createCodeFixToRemoveNode(importEquals); + const importEquals = getAncestor(token, SyntaxKind.ImportEqualsDeclaration); + return deleteNode(importEquals); case SyntaxKind.ImportSpecifier: const namedImports = token.parent.parent; if (namedImports.elements.length === 1) { // Only 1 import and it is unused. So the entire declaration should be removed. - const importSpec = findImportDeclaration(token); - return createCodeFixToRemoveNode(importSpec); + const importSpec = getAncestor(token, SyntaxKind.ImportDeclaration); + return deleteNode(importSpec); } else { - return removeSingleItem(namedImports.elements, token); + // delete import specifier + return deleteNodeInList(token.parent); } // handle case where "import d, * as ns from './file'" @@ -99,98 +107,79 @@ namespace ts.codefix { case SyntaxKind.ImportClause: // this covers both 'import |d|' and 'import |d,| *' const importClause = token.parent; if (!importClause.namedBindings) { // |import d from './file'| or |import * as ns from './file'| - const importDecl = findImportDeclaration(importClause); - return createCodeFixToRemoveNode(importDecl); + const importDecl = getAncestor(importClause, SyntaxKind.ImportDeclaration); + return deleteNode(importDecl); } else { // import |d,| * as ns from './file' - const start = importClause.name.getStart(); - let end = findFirstNonSpaceCharPosStarting(importClause.name.end); - if (sourceFile.text.charCodeAt(end) === CharacterCodes.comma) { - end = findFirstNonSpaceCharPosStarting(end + 1); + const start = importClause.name.getStart(sourceFile); + const nextToken = getTokenAtPosition(sourceFile, importClause.name.end); + if (nextToken && nextToken.kind === SyntaxKind.CommaToken) { + // shift first non-whitespace position after comma to the start position of the node + return deleteRange({ pos: start, end: skipTrivia(sourceFile.text, nextToken.end, /*stopAfterLineBreaks*/ false, /*stopAtComments*/true) }); + } + else { + return deleteNode(importClause.name); } - - return createCodeFix("", start, end - start); } case SyntaxKind.NamespaceImport: const namespaceImport = token.parent; if (namespaceImport.name == token && !(namespaceImport.parent).name) { - const importDecl = findImportDeclaration(namespaceImport); - return createCodeFixToRemoveNode(importDecl); + const importDecl = getAncestor(namespaceImport, SyntaxKind.ImportDeclaration); + return deleteNode(importDecl); } else { - const start = (namespaceImport.parent).name.end; - return createCodeFix("", start, (namespaceImport.parent).namedBindings.end - start); + const previousToken = getTokenAtPosition(sourceFile, namespaceImport.pos - 1); + if (previousToken && previousToken.kind === SyntaxKind.CommaToken) { + const startPosition = textChanges.getAdjustedStartPosition(sourceFile, previousToken, {}, textChanges.Position.FullStart); + return deleteRange({ pos: startPosition, end: namespaceImport.end }); + } + return deleteRange(namespaceImport); } } break; case SyntaxKind.PropertyDeclaration: case SyntaxKind.NamespaceImport: - return createCodeFixToRemoveNode(token.parent); + return deleteNode(token.parent); } if (isDeclarationName(token)) { - return createCodeFixToRemoveNode(token.parent); + return deleteNode(token.parent); } else if (isLiteralComputedPropertyDeclarationName(token)) { - return createCodeFixToRemoveNode(token.parent.parent); + return deleteNode(token.parent.parent); } else { return undefined; } - function findImportDeclaration(token: Node): Node { - let importDecl = token; - while (importDecl.kind != SyntaxKind.ImportDeclaration && importDecl.parent) { - importDecl = importDecl.parent; - } - - return importDecl; + function deleteNode(n: Node) { + return makeChange(textChanges.ChangeTracker.fromCodeFixContext(context).deleteNode(sourceFile, n)); } - function createCodeFixToRemoveNode(node: Node) { - let end = node.getEnd(); - const endCharCode = sourceFile.text.charCodeAt(end); - const afterEndCharCode = sourceFile.text.charCodeAt(end + 1); - if (isLineBreak(endCharCode)) { - end += 1; - } - // in the case of CR LF, you could have two consecutive new line characters for one new line. - // this needs to be differenciated from two LF LF chars that actually mean two new lines. - if (isLineBreak(afterEndCharCode) && endCharCode !== afterEndCharCode) { - end += 1; - } - - const start = node.getStart(); - return createCodeFix("", start, end - start); + function deleteRange(range: TextRange) { + return makeChange(textChanges.ChangeTracker.fromCodeFixContext(context).deleteRange(sourceFile, range)); } - function findFirstNonSpaceCharPosStarting(start: number) { - while (isWhiteSpace(sourceFile.text.charCodeAt(start))) { - start += 1; - } - return start; + function deleteNodeInList(n: Node) { + return makeChange(textChanges.ChangeTracker.fromCodeFixContext(context).deleteNodeInList(sourceFile, n)); } - function createCodeFix(newText: string, start: number, length: number): CodeAction[] { + function deleteNodeRange(start: Node, end: Node) { + return makeChange(textChanges.ChangeTracker.fromCodeFixContext(context).deleteNodeRange(sourceFile, start, end)); + } + + function replaceNode(n: Node, newNode: Node) { + return makeChange(textChanges.ChangeTracker.fromCodeFixContext(context).replaceNode(sourceFile, n, newNode)); + } + + function makeChange(changeTracker: textChanges.ChangeTracker) { return [{ description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Remove_declaration_for_Colon_0), { 0: token.getText() }), - changes: [{ - fileName: sourceFile.fileName, - textChanges: [{ newText, span: { start, length } }] - }] + changes: changeTracker.getChanges() }]; } - - function removeSingleItem(elements: NodeArray, token: T): CodeAction[] { - if (elements[0] === token.parent) { - return createCodeFix("", token.parent.pos, token.parent.end - token.parent.pos + 1); - } - else { - return createCodeFix("", token.parent.pos - 1, token.parent.end - token.parent.pos + 1); - } - } } }); } \ No newline at end of file diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index 201d64e4d7e..4fa3b71a3a2 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -314,24 +314,55 @@ namespace ts.formatting { return 0; } + /* @internal */ + export function formatNode(node: Node, sourceFileLike: SourceFileLike, languageVariant: LanguageVariant, initialIndentation: number, delta: number, rulesProvider: RulesProvider): TextChange[] { + const range = { pos: 0, end: sourceFileLike.text.length }; + return formatSpanWorker( + range, + node, + initialIndentation, + delta, + getFormattingScanner(sourceFileLike.text, languageVariant, range.pos, range.end), + rulesProvider.getFormatOptions(), + rulesProvider, + FormattingRequestKind.FormatSelection, + _ => false, // assume that node does not have any errors + sourceFileLike); + } + function formatSpan(originalRange: TextRange, sourceFile: SourceFile, options: FormatCodeSettings, rulesProvider: RulesProvider, requestKind: FormattingRequestKind): TextChange[] { + // find the smallest node that fully wraps the range and compute the initial indentation for the node + const enclosingNode = findEnclosingNode(originalRange, sourceFile); + return formatSpanWorker( + originalRange, + enclosingNode, + SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, options), + getOwnOrInheritedDelta(enclosingNode, options, sourceFile), + getFormattingScanner(sourceFile.text, sourceFile.languageVariant, getScanStartPosition(enclosingNode, originalRange, sourceFile), originalRange.end), + options, + rulesProvider, + requestKind, + prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange), + sourceFile); + } - const rangeContainsError = prepareRangeContainsErrorFunction(sourceFile.parseDiagnostics, originalRange); + function formatSpanWorker(originalRange: TextRange, + enclosingNode: Node, + initialIndentation: number, + delta: number, + formattingScanner: FormattingScanner, + options: FormatCodeSettings, + rulesProvider: RulesProvider, + requestKind: FormattingRequestKind, + rangeContainsError: (r: TextRange) => boolean, + sourceFile: SourceFileLike): TextChange[] { // formatting context is used by rules provider const formattingContext = new FormattingContext(sourceFile, requestKind); - - // find the smallest node that fully wraps the range and compute the initial indentation for the node - const enclosingNode = findEnclosingNode(originalRange, sourceFile); - - const formattingScanner = getFormattingScanner(sourceFile, getScanStartPosition(enclosingNode, originalRange, sourceFile), originalRange.end); - - const initialIndentation = SmartIndenter.getIndentationForNode(enclosingNode, originalRange, sourceFile, options); - let previousRangeHasError: boolean; let previousRange: TextRangeWithKind; let previousParent: Node; @@ -351,7 +382,6 @@ namespace ts.formatting { undecoratedStartLine = sourceFile.getLineAndCharacterOfPosition(getNonDecoratorTokenPosOfNode(enclosingNode, sourceFile)).line; } - const delta = getOwnOrInheritedDelta(enclosingNode, options, sourceFile); processNode(enclosingNode, enclosingNode, startLine, undecoratedStartLine, initialIndentation, delta); } diff --git a/src/services/formatting/formattingContext.ts b/src/services/formatting/formattingContext.ts index bd5ca352476..26b538dfc5e 100644 --- a/src/services/formatting/formattingContext.ts +++ b/src/services/formatting/formattingContext.ts @@ -15,7 +15,7 @@ namespace ts.formatting { private contextNodeBlockIsOnOneLine: boolean; private nextNodeBlockIsOnOneLine: boolean; - constructor(public sourceFile: SourceFile, public formattingRequestKind: FormattingRequestKind) { + constructor(public readonly sourceFile: SourceFileLike, public formattingRequestKind: FormattingRequestKind) { } public updateContext(currentRange: TextRangeWithKind, currentTokenParent: Node, nextRange: TextRangeWithKind, nextTokenParent: Node, commonParent: Node) { diff --git a/src/services/formatting/formattingScanner.ts b/src/services/formatting/formattingScanner.ts index 2020352f86b..20d190de2f3 100644 --- a/src/services/formatting/formattingScanner.ts +++ b/src/services/formatting/formattingScanner.ts @@ -30,11 +30,11 @@ namespace ts.formatting { RescanJsxText, } - export function getFormattingScanner(sourceFile: SourceFile, startPos: number, endPos: number): FormattingScanner { + export function getFormattingScanner(text: string, languageVariant: LanguageVariant, startPos: number, endPos: number): FormattingScanner { Debug.assert(scanner === undefined, "Scanner should be undefined"); - scanner = sourceFile.languageVariant === LanguageVariant.JSX ? jsxScanner : standardScanner; + scanner = languageVariant === LanguageVariant.JSX ? jsxScanner : standardScanner; - scanner.setText(sourceFile.text); + scanner.setText(text); scanner.setTextPos(startPos); let wasNewLine = true; @@ -276,8 +276,8 @@ namespace ts.formatting { function isOnToken(): boolean { Debug.assert(scanner !== undefined); - const current = (lastTokenInfo && lastTokenInfo.token.kind) || scanner.getToken(); - const startPos = (lastTokenInfo && lastTokenInfo.token.pos) || scanner.getStartPos(); + const current = lastTokenInfo ? lastTokenInfo.token.kind : scanner.getToken(); + const startPos = lastTokenInfo ? lastTokenInfo.token.pos : scanner.getStartPos(); return startPos < endPos && current !== SyntaxKind.EndOfFileToken && !isTrivia(current); } diff --git a/src/services/formatting/rulesProvider.ts b/src/services/formatting/rulesProvider.ts index 14e08e4857a..660d5f34a0e 100644 --- a/src/services/formatting/rulesProvider.ts +++ b/src/services/formatting/rulesProvider.ts @@ -24,6 +24,10 @@ namespace ts.formatting { return this.rulesMap; } + public getFormatOptions(): Readonly { + return this.options; + } + public ensureUpToDate(options: ts.FormatCodeSettings) { if (!this.options || !ts.compareDataObjects(this.options, options)) { const activeRules = this.createActiveRules(options); diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts index eb9e1ba04c5..b5c901482a7 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -8,7 +8,19 @@ namespace ts.formatting { Unknown = -1 } - export function getIndentation(position: number, sourceFile: SourceFile, options: EditorSettings): number { + /** + * Computed indentation for a given position in source file + * @param position - position in file + * @param sourceFile - target source file + * @param options - set of editor options that control indentation + * @param assumeNewLineBeforeCloseBrace - false when getIndentation is called on the text from the real source file. + * true - when we need to assume that position is on the newline. This is usefult for codefixes, i.e. + * function f() { + * |} + * when inserting some text after open brace we would like to get the value of indentation as if newline was already there. + * However by default indentation at position | will be 0 so 'assumeNewLineBeforeCloseBrace' allows to override this behavior, + */ + export function getIndentation(position: number, sourceFile: SourceFile, options: EditorSettings, assumeNewLineBeforeCloseBrace = false): number { if (position > sourceFile.text.length) { return getBaseIndentation(options); // past EOF } @@ -71,13 +83,14 @@ namespace ts.formatting { if (positionBelongsToNode(current, position, sourceFile) && shouldIndentChildNode(current, previous)) { currentStart = getStartLineAndCharacterForNode(current, sourceFile); - if (nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile)) { - indentationDelta = 0; + const nextTokenKind = nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken, current, lineAtPosition, sourceFile); + if (nextTokenKind !== NextTokenKind.Unknown) { + // handle cases when codefix is about to be inserted before the close brace + indentationDelta = assumeNewLineBeforeCloseBrace && nextTokenKind === NextTokenKind.CloseBrace ? options.indentSize : 0; } else { indentationDelta = lineAtPosition !== currentStart.line ? options.indentSize : 0; } - break; } @@ -218,15 +231,21 @@ namespace ts.formatting { return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options); } - function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): boolean { + const enum NextTokenKind { + Unknown, + OpenBrace, + CloseBrace + } + + function nextTokenIsCurlyBraceOnSameLineAsCursor(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): NextTokenKind { const nextToken = findNextToken(precedingToken, current); if (!nextToken) { - return false; + return NextTokenKind.Unknown; } if (nextToken.kind === SyntaxKind.OpenBraceToken) { // open braces are always indented at the parent level - return true; + return NextTokenKind.OpenBrace; } else if (nextToken.kind === SyntaxKind.CloseBraceToken) { // close braces are indented at the parent level if they are located on the same line with cursor @@ -239,17 +258,17 @@ namespace ts.formatting { // $} const nextTokenStartLine = getStartLineAndCharacterForNode(nextToken, sourceFile).line; - return lineAtPosition === nextTokenStartLine; + return lineAtPosition === nextTokenStartLine ? NextTokenKind.CloseBrace : NextTokenKind.Unknown; } - return false; + return NextTokenKind.Unknown; } - function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFile): LineAndCharacter { + function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFileLike): LineAndCharacter { return sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); } - export function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFile): boolean { + export function childStartsOnTheSameLineWithElseInIfStatement(parent: Node, child: TextRangeWithKind, childStartLine: number, sourceFile: SourceFileLike): boolean { if (parent.kind === SyntaxKind.IfStatement && (parent).elseStatement === child) { const elseKeyword = findChildOfKind(parent, SyntaxKind.ElseKeyword, sourceFile); Debug.assert(elseKeyword !== undefined); @@ -261,15 +280,15 @@ namespace ts.formatting { return false; } - function getContainingList(node: Node, sourceFile: SourceFile): NodeArray { + function getListIfStartEndIsInListRange(list: NodeArray, start: number, end: number) { + return list && rangeContainsStartEnd(list, start, end) ? list : undefined; + } + + export function getContainingList(node: Node, sourceFile: SourceFile): NodeArray { if (node.parent) { switch (node.parent.kind) { case SyntaxKind.TypeReference: - if ((node.parent).typeArguments && - rangeContainsStartEnd((node.parent).typeArguments, node.getStart(sourceFile), node.getEnd())) { - return (node.parent).typeArguments; - } - break; + return getListIfStartEndIsInListRange((node.parent).typeArguments, node.getStart(sourceFile), node.getEnd()); case SyntaxKind.ObjectLiteralExpression: return (node.parent).properties; case SyntaxKind.ArrayLiteralExpression: @@ -280,30 +299,26 @@ namespace ts.formatting { case SyntaxKind.MethodDeclaration: case SyntaxKind.MethodSignature: case SyntaxKind.CallSignature: + case SyntaxKind.Constructor: + case SyntaxKind.ConstructorType: case SyntaxKind.ConstructSignature: { const start = node.getStart(sourceFile); - if ((node.parent).typeParameters && - rangeContainsStartEnd((node.parent).typeParameters, start, node.getEnd())) { - return (node.parent).typeParameters; - } - if (rangeContainsStartEnd((node.parent).parameters, start, node.getEnd())) { - return (node.parent).parameters; - } - break; + return getListIfStartEndIsInListRange((node.parent).typeParameters, start, node.getEnd()) || + getListIfStartEndIsInListRange((node.parent).parameters, start, node.getEnd()); } + case SyntaxKind.ClassDeclaration: + return getListIfStartEndIsInListRange((node.parent).typeParameters, node.getStart(sourceFile), node.getEnd()); case SyntaxKind.NewExpression: case SyntaxKind.CallExpression: { const start = node.getStart(sourceFile); - if ((node.parent).typeArguments && - rangeContainsStartEnd((node.parent).typeArguments, start, node.getEnd())) { - return (node.parent).typeArguments; - } - if ((node.parent).arguments && - rangeContainsStartEnd((node.parent).arguments, start, node.getEnd())) { - return (node.parent).arguments; - } - break; + return getListIfStartEndIsInListRange((node.parent).typeArguments, start, node.getEnd()) || + getListIfStartEndIsInListRange((node.parent).arguments, start, node.getEnd()); } + case SyntaxKind.VariableDeclarationList: + return getListIfStartEndIsInListRange((node.parent).declarations, node.getStart(sourceFile), node.getEnd()); + case SyntaxKind.NamedImports: + case SyntaxKind.NamedExports: + return getListIfStartEndIsInListRange((node.parent).elements, node.getStart(sourceFile), node.getEnd()); } } return undefined; @@ -400,7 +415,7 @@ namespace ts.formatting { value of 'character' for '$' is 3 value of 'column' for '$' is 6 (assuming that tab size is 4) */ - export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFile, options: EditorSettings) { + export function findFirstNonWhitespaceCharacterAndColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings) { let character = 0; let column = 0; for (let pos = startPos; pos < endPos; pos++) { @@ -421,7 +436,7 @@ namespace ts.formatting { return { column, character }; } - export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: SourceFile, options: EditorSettings): number { + export function findFirstNonWhitespaceColumn(startPos: number, endPos: number, sourceFile: SourceFileLike, options: EditorSettings): number { return findFirstNonWhitespaceCharacterAndColumn(startPos, endPos, sourceFile, options).column; } diff --git a/src/services/services.ts b/src/services/services.ts index df5330ebaed..e370dccac32 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -24,6 +24,7 @@ /// /// /// +/// /// /// @@ -63,7 +64,7 @@ namespace ts { return getSourceFileOfNode(this); } - public getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number { + public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number { return getTokenPosOfNode(this, sourceFile, includeJsDocComment); } @@ -129,7 +130,7 @@ namespace ts { return list; } - private createChildren(sourceFile?: SourceFile) { + private createChildren(sourceFile?: SourceFileLike) { let children: Node[]; if (this.kind >= SyntaxKind.FirstNode) { scanner.setText((sourceFile || this.getSourceFile()).text); @@ -182,7 +183,7 @@ namespace ts { return this._children[index]; } - public getChildren(sourceFile?: SourceFile): Node[] { + public getChildren(sourceFile?: SourceFileLike): Node[] { if (!this._children) this.createChildren(sourceFile); return this._children; } @@ -231,7 +232,7 @@ namespace ts { return getSourceFileOfNode(this); } - public getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number { + public getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number { return getTokenPosOfNode(this, sourceFile, includeJsDocComment); } @@ -1682,7 +1683,7 @@ namespace ts { return []; } - function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[] { + function getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[] { synchronizeHostData(); const sourceFile = getValidSourceFile(fileName); const span = { start, length: end - start }; @@ -1700,7 +1701,8 @@ namespace ts { program: program, newLineCharacter: newLineChar, host: host, - cancellationToken: cancellationToken + cancellationToken: cancellationToken, + rulesProvider: getRuleProvider(formatOptions) }; const fixes = codefix.getFixes(context); diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts new file mode 100644 index 00000000000..6f8a67a0535 --- /dev/null +++ b/src/services/textChanges.ts @@ -0,0 +1,666 @@ +/* @internal */ +namespace ts.textChanges { + + /** + * Currently for simplicity we store recovered positions on the node itself. + * It can be changed to side-table later if we decide that current design is too invasive. + */ + function getPos(n: TextRange) { + return (n)["__pos"]; + } + + function setPos(n: TextRange, pos: number) { + (n)["__pos"] = pos; + } + + function getEnd(n: TextRange) { + return (n)["__end"]; + } + + function setEnd(n: TextRange, end: number) { + (n)["__end"] = end; + } + + export interface ConfigurableStart { + useNonAdjustedStartPosition?: boolean; + } + export interface ConfigurableEnd { + useNonAdjustedEndPosition?: boolean; + } + + export enum Position { + FullStart, + Start + } + + function skipWhitespacesAndLineBreaks(text: string, start: number) { + return skipTrivia(text, start, /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); + } + + function hasCommentsBeforeLineBreak(text: string, start: number) { + let i = start; + while (i < text.length) { + const ch = text.charCodeAt(i); + if (isWhiteSpaceSingleLine(ch)) { + i++; + continue; + } + return ch === CharacterCodes.slash; + } + return false; + } + + /** + * Usually node.pos points to a position immediately after the previous token. + * If this position is used as a beginning of the span to remove - it might lead to removing the trailing trivia of the previous node, i.e: + * const x; // this is x + * ^ - pos for the next variable declaration will point here + * const y; // this is y + * ^ - end for previous variable declaration + * Usually leading trivia of the variable declaration 'y' should not include trailing trivia (whitespace, comment 'this is x' and newline) from the preceding + * variable declaration and trailing trivia for 'y' should include (whitespace, comment 'this is y', newline). + * By default when removing nodes we adjust start and end positions to respect specification of the trivia above. + * If pos\end should be interpreted literally 'useNonAdjustedStartPosition' or 'useNonAdjustedEndPosition' should be set to true + */ + export type ConfigurableStartEnd = ConfigurableStart & ConfigurableEnd; + + export interface InsertNodeOptions { + /** + * Text to be inserted before the new node + */ + prefix?: string; + /** + * Text to be inserted after the new node + */ + suffix?: string; + /** + * Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node + */ + indentation?: number; + /** + * Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind + */ + delta?: number; + } + + export type ChangeNodeOptions = ConfigurableStartEnd & InsertNodeOptions; + + interface Change { + readonly sourceFile: SourceFile; + readonly range: TextRange; + readonly useIndentationFromFile?: boolean; + readonly node?: Node; + readonly options?: ChangeNodeOptions; + } + + export function getSeparatorCharacter(separator: Token) { + return tokenToString(separator.kind); + } + + export function getAdjustedStartPosition(sourceFile: SourceFile, node: Node, options: ConfigurableStart, position: Position) { + if (options.useNonAdjustedStartPosition) { + return node.getFullStart(); + } + const fullStart = node.getFullStart(); + const start = node.getStart(sourceFile); + if (fullStart === start) { + return start; + } + const fullStartLine = getLineStartPositionForPosition(fullStart, sourceFile); + const startLine = getLineStartPositionForPosition(start, sourceFile); + if (startLine === fullStartLine) { + // full start and start of the node are on the same line + // a, b; + // ^ ^ + // | start + // fullstart + // when b is replaced - we usually want to keep the leading trvia + // when b is deleted - we delete it + return position === Position.Start ? start : fullStart; + } + // get start position of the line following the line that contains fullstart position + let adjustedStartPosition = getStartPositionOfLine(getLineOfLocalPosition(sourceFile, fullStartLine) + 1, sourceFile); + // skip whitespaces/newlines + adjustedStartPosition = skipWhitespacesAndLineBreaks(sourceFile.text, adjustedStartPosition); + return getStartPositionOfLine(getLineOfLocalPosition(sourceFile, adjustedStartPosition), sourceFile); + } + + export function getAdjustedEndPosition(sourceFile: SourceFile, node: Node, options: ConfigurableEnd) { + if (options.useNonAdjustedEndPosition) { + return node.getEnd(); + } + const end = node.getEnd(); + const newEnd = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true); + // check if last character before newPos is linebreak + // if yes - considered all skipped trivia to be trailing trivia of the node + return newEnd !== end && isLineBreak(sourceFile.text.charCodeAt(newEnd - 1)) + ? newEnd + : end; + } + + /** + * Checks if 'candidate' argument is a legal separator in the list that contains 'node' as an element + */ + function isSeparator(node: Node, candidate: Node): candidate is Token { + return candidate && node.parent && (candidate.kind === SyntaxKind.CommaToken || (candidate.kind === SyntaxKind.SemicolonToken && node.parent.kind === SyntaxKind.ObjectLiteralExpression)); + } + + function spaces(count: number) { + let s = ""; + for (let i = 0; i < count; i++) { + s += " "; + } + return s; + } + + export class ChangeTracker { + private changes: Change[] = []; + private readonly newLineCharacter: string; + + public static fromCodeFixContext(context: CodeFixContext) { + return new ChangeTracker(context.newLineCharacter === "\n" ? NewLineKind.LineFeed : NewLineKind.CarriageReturnLineFeed, context.rulesProvider); + } + + constructor( + private readonly newLine: NewLineKind, + private readonly rulesProvider: formatting.RulesProvider, + private readonly validator?: (text: NonFormattedText) => void) { + this.newLineCharacter = getNewLineCharacter({ newLine }); + } + + public deleteNode(sourceFile: SourceFile, node: Node, options: ConfigurableStartEnd = {}) { + const startPosition = getAdjustedStartPosition(sourceFile, node, options, Position.FullStart); + const endPosition = getAdjustedEndPosition(sourceFile, node, options); + this.changes.push({ sourceFile, options, range: { pos: startPosition, end: endPosition } }); + return this; + } + + public deleteRange(sourceFile: SourceFile, range: TextRange) { + this.changes.push({ sourceFile, range }); + return this; + } + + public deleteNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, options: ConfigurableStartEnd = {}) { + const startPosition = getAdjustedStartPosition(sourceFile, startNode, options, Position.FullStart); + const endPosition = getAdjustedEndPosition(sourceFile, endNode, options); + this.changes.push({ sourceFile, options, range: { pos: startPosition, end: endPosition } }); + return this; + } + + public deleteNodeInList(sourceFile: SourceFile, node: Node) { + const containingList = formatting.SmartIndenter.getContainingList(node, sourceFile); + if (!containingList) { + Debug.fail("node is not a list element"); + return this; + } + const index = containingList.indexOf(node); + if (index < 0) { + return this; + } + if (containingList.length === 1) { + this.deleteNode(sourceFile, node); + return this; + } + if (index !== containingList.length - 1) { + const nextToken = getTokenAtPosition(sourceFile, node.end); + if (nextToken && isSeparator(node, nextToken)) { + // find first non-whitespace position in the leading trivia of the node + const startPosition = skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, node, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); + const nextElement = containingList[index + 1]; + /// find first non-whitespace position in the leading trivia of the next node + const endPosition = skipTrivia(sourceFile.text, getAdjustedStartPosition(sourceFile, nextElement, {}, Position.FullStart), /*stopAfterLineBreak*/ false, /*stopAtComments*/ true); + // shift next node so its first non-whitespace position will be moved to the first non-whitespace position of the deleted node + this.deleteRange(sourceFile, { pos: startPosition, end: endPosition }); + } + } + else { + const previousToken = getTokenAtPosition(sourceFile, containingList[index - 1].end); + if (previousToken && isSeparator(node, previousToken)) { + this.deleteNodeRange(sourceFile, previousToken, node); + } + } + return this; + } + + public replaceRange(sourceFile: SourceFile, range: TextRange, newNode: Node, options: InsertNodeOptions = {}) { + this.changes.push({ sourceFile, range, options, node: newNode }); + return this; + } + + public replaceNode(sourceFile: SourceFile, oldNode: Node, newNode: Node, options: ChangeNodeOptions = {}) { + const startPosition = getAdjustedStartPosition(sourceFile, oldNode, options, Position.Start); + const endPosition = getAdjustedEndPosition(sourceFile, oldNode, options); + this.changes.push({ sourceFile, options, useIndentationFromFile: true, node: newNode, range: { pos: startPosition, end: endPosition } }); + return this; + } + + public replaceNodeRange(sourceFile: SourceFile, startNode: Node, endNode: Node, newNode: Node, options: ChangeNodeOptions = {}) { + const startPosition = getAdjustedStartPosition(sourceFile, startNode, options, Position.Start); + const endPosition = getAdjustedEndPosition(sourceFile, endNode, options); + this.changes.push({ sourceFile, options, useIndentationFromFile: true, node: newNode, range: { pos: startPosition, end: endPosition } }); + return this; + } + + public insertNodeAt(sourceFile: SourceFile, pos: number, newNode: Node, options: InsertNodeOptions = {}) { + this.changes.push({ sourceFile, options, node: newNode, range: { pos: pos, end: pos } }); + return this; + } + + public insertNodeBefore(sourceFile: SourceFile, before: Node, newNode: Node, options: InsertNodeOptions & ConfigurableStart = {}) { + const startPosition = getAdjustedStartPosition(sourceFile, before, options, Position.Start); + this.changes.push({ sourceFile, options, useIndentationFromFile: true, node: newNode, range: { pos: startPosition, end: startPosition } }); + return this; + } + + 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) { + Debug.fail("node is not a list element"); + return this; + } + const index = containingList.indexOf(after); + if (index < 0) { + return this; + } + const end = after.getEnd(); + if (index !== containingList.length - 1) { + // any element except the last one + // use next sibling as an anchor + const nextToken = getTokenAtPosition(sourceFile, after.end); + if (nextToken && isSeparator(after, nextToken)) { + // for list + // a, b, c + // create change for adding 'e' after 'a' as + // - find start of next element after a (it is b) + // - use this start as start and end position in final change + // - build text of change by formatting the text of node + separator + whitespace trivia of b + + // in multiline case it will work as + // a, + // b, + // c, + // result - '*' denotes leading trivia that will be inserted after new text (displayed as '#') + // a,* + //***insertedtext# + //###b, + // c, + // find line and character of the next element + const lineAndCharOfNextElement = getLineAndCharacterOfPosition(sourceFile, skipWhitespacesAndLineBreaks(sourceFile.text, containingList[index + 1].getFullStart())); + // find line and character of the token that precedes next element (usually it is separator) + const lineAndCharOfNextToken = getLineAndCharacterOfPosition(sourceFile, nextToken.end); + let prefix: string; + let startPos: number; + if (lineAndCharOfNextToken.line === lineAndCharOfNextElement.line) { + // next element is located on the same line with separator: + // a,$$$$b + // ^ ^ + // | |-next element + // |-separator + // where $$$ is some leading trivia + // for a newly inserted node we'll maintain the same relative position comparing to separator and replace leading trivia with spaces + // a, x,$$$$b + // ^ ^ ^ + // | | |-next element + // | |-new inserted node padded with spaces + // |-separator + startPos = nextToken.end; + prefix = spaces(lineAndCharOfNextElement.character - lineAndCharOfNextToken.character); + } + else { + // next element is located on different line that separator + // let insert position be the beginning of the line that contains next element + startPos = getStartPositionOfLine(lineAndCharOfNextElement.line, sourceFile); + } + + this.changes.push({ + sourceFile, + range: { pos: startPos, end: containingList[index + 1].getStart(sourceFile) }, + node: newNode, + useIndentationFromFile: true, + options: { + prefix, + // write separator and leading trivia of the next element as suffix + suffix: `${tokenToString(nextToken.kind)}${sourceFile.text.substring(nextToken.end, containingList[index + 1].getStart(sourceFile))}` + } + }); + } + } + else { + const afterStart = after.getStart(sourceFile); + const afterStartLinePosition = getLineStartPositionForPosition(afterStart, sourceFile); + + let separator: SyntaxKind.CommaToken | SyntaxKind.SemicolonToken; + let multilineList = false; + + // insert element after the last element in the list that has more than one item + // pick the element preceding the after element to: + // - pick the separator + // - determine if list is a multiline + if (containingList.length === 1) { + // if list has only one element then we'll format is as multiline if node has comment in trailing trivia, or as singleline otherwise + // i.e. var x = 1 // this is x + // | new element will be inserted at this position + separator = SyntaxKind.CommaToken; + } + else { + // element has more than one element, pick separator from the list + const tokenBeforeInsertPosition = findPrecedingToken(after.pos, sourceFile); + separator = isSeparator(after, tokenBeforeInsertPosition) ? tokenBeforeInsertPosition.kind : SyntaxKind.CommaToken; + // determine if list is multiline by checking lines of after element and element that precedes it. + const afterMinusOneStartLinePosition = getLineStartPositionForPosition(containingList[index - 1].getStart(sourceFile), sourceFile); + multilineList = afterMinusOneStartLinePosition !== afterStartLinePosition; + } + if (hasCommentsBeforeLineBreak(sourceFile.text, after.end)) { + // in this case we'll always treat containing list as multiline + multilineList = true; + } + if (multilineList) { + // insert separator immediately following the 'after' node to preserve comments in trailing trivia + this.changes.push({ + sourceFile, + range: { pos: end, end }, + node: createToken(separator), + options: {} + }); + // use the same indentation as 'after' item + const indentation = formatting.SmartIndenter.findFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, this.rulesProvider.getFormatOptions()); + // insert element before the line break on the line that contains 'after' element + let insertPos = skipTrivia(sourceFile.text, end, /*stopAfterLineBreak*/ true, /*stopAtComments*/ false); + if (insertPos !== end && isLineBreak(sourceFile.text.charCodeAt(insertPos - 1))) { + insertPos-- + } + this.changes.push({ + sourceFile, + range: { pos: insertPos, end: insertPos }, + node: newNode, + options: { indentation, prefix: this.newLineCharacter } + }); + } + else { + this.changes.push({ + sourceFile, + range: { pos: end, end }, + node: newNode, + options: { prefix: `${tokenToString(separator)} ` } + }); + } + } + return this; + } + + public getChanges(): FileTextChanges[] { + const changesPerFile = createFileMap(); + // group changes per file + for (const c of this.changes) { + let changesInFile = changesPerFile.get(c.sourceFile.path); + if (!changesInFile) { + changesPerFile.set(c.sourceFile.path, changesInFile = []); + }; + changesInFile.push(c); + } + // convert changes + const fileChangesList: FileTextChanges[] = []; + changesPerFile.forEachValue(path => { + const changesInFile = changesPerFile.get(path); + const sourceFile = changesInFile[0].sourceFile; + const fileTextChanges: FileTextChanges = { fileName: sourceFile.fileName, textChanges: [] }; + for (const c of ChangeTracker.normalize(changesInFile)) { + fileTextChanges.textChanges.push({ + span: this.computeSpan(c, sourceFile), + newText: this.computeNewText(c, sourceFile) + }); + } + fileChangesList.push(fileTextChanges); + }); + + return fileChangesList; + } + + private computeSpan(change: Change, _sourceFile: SourceFile): TextSpan { + return createTextSpanFromBounds(change.range.pos, change.range.end); + } + + private computeNewText(change: Change, sourceFile: SourceFile): string { + if (!change.node) { + // deletion case + return ""; + } + const options = change.options || {}; + const nonFormattedText = getNonformattedText(change.node, sourceFile, this.newLine); + if (this.validator) { + this.validator(nonFormattedText); + } + + const formatOptions = this.rulesProvider.getFormatOptions(); + const pos = change.range.pos; + const posStartsLine = getLineStartPositionForPosition(pos, sourceFile) === pos; + + const initialIndentation = + change.options.indentation !== undefined + ? change.options.indentation + : change.useIndentationFromFile + ? formatting.SmartIndenter.getIndentation(change.range.pos, sourceFile, formatOptions, posStartsLine || (change.options.prefix == this.newLineCharacter)) + : 0; + const delta = + change.options.delta !== undefined + ? change.options.delta + : formatting.SmartIndenter.shouldIndentChildNode(change.node) + ? formatOptions.indentSize + : 0; + + let text = applyFormatting(nonFormattedText, sourceFile, initialIndentation, delta, this.rulesProvider); + // strip initial indentation (spaces or tabs) if text will be inserted in the middle of the line + // however keep indentation if it is was forced + text = posStartsLine || change.options.indentation !== undefined ? text : text.replace(/^\s+/, ""); + return (options.prefix || "") + text + (options.suffix || ""); + } + + private static normalize(changes: Change[]) { + // order changes by start position + 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); + } + return normalized; + } + } + + export interface NonFormattedText { + readonly text: string; + readonly node: Node; + } + + export function getNonformattedText(node: Node, sourceFile: SourceFile, newLine: NewLineKind): NonFormattedText { + const options = { newLine, target: sourceFile.languageVersion }; + const writer = new Writer(getNewLineCharacter(options)); + const printer = createPrinter(options, writer); + printer.writeNode(EmitHint.Unspecified, node, sourceFile, writer); + return { text: writer.getText(), node: assignPositionsToNode(node) }; + } + + export function applyFormatting(nonFormattedText: NonFormattedText, sourceFile: SourceFile, initialIndentation: number, delta: number, rulesProvider: formatting.RulesProvider) { + const lineMap = computeLineStarts(nonFormattedText.text); + const file: SourceFileLike = { + text: nonFormattedText.text, + lineMap, + getLineAndCharacterOfPosition: pos => computeLineAndCharacterOfPosition(lineMap, pos) + }; + const changes = formatting.formatNode(nonFormattedText.node, file, sourceFile.languageVariant, initialIndentation, delta, rulesProvider); + return applyChanges(nonFormattedText.text, changes); + } + + export function applyChanges(text: string, changes: TextChange[]): string { + for (let i = changes.length - 1; i >= 0; i--) { + const change = changes[i]; + text = `${text.substring(0, change.span.start)}${change.newText}${text.substring(textSpanEnd(change.span))}`; + } + return text; + } + + function isTrivia(s: string) { + return skipTrivia(s, 0) === s.length; + } + + const nullTransformationContext: TransformationContext = { + enableEmitNotification: noop, + enableSubstitution: noop, + endLexicalEnvironment: () => undefined, + getCompilerOptions: notImplemented, + getEmitHost: notImplemented, + getEmitResolver: notImplemented, + hoistFunctionDeclaration: noop, + hoistVariableDeclaration: noop, + isEmitNotificationEnabled: notImplemented, + isSubstitutionEnabled: notImplemented, + onEmitNode: noop, + onSubstituteNode: notImplemented, + readEmitHelpers: notImplemented, + requestEmitHelper: noop, + resumeLexicalEnvironment: noop, + startLexicalEnvironment: noop, + suspendLexicalEnvironment: noop + }; + + function assignPositionsToNode(node: Node): Node { + const visited = visitEachChild(node, assignPositionsToNode, nullTransformationContext, assignPositionsToNodeArray); + // create proxy node for non synthesized nodes + const newNode = nodeIsSynthesized(visited) + ? visited + : (Proxy.prototype = visited, new (Proxy)()); + newNode.pos = getPos(node); + newNode.end = getEnd(node); + return newNode; + + function Proxy() { } + } + + function assignPositionsToNodeArray(nodes: NodeArray, visitor: Visitor, test?: (node: Node) => boolean, start?: number, count?: number) { + const visited = visitNodes(nodes, visitor, test, start, count); + if (!visited) { + return visited; + } + // clone nodearray if necessary + const nodeArray = visited === nodes ? createNodeArray(visited.slice(0)) : visited; + nodeArray.pos = getPos(nodes); + nodeArray.end = getEnd(nodes); + return nodeArray; + } + + class Writer implements EmitTextWriter, PrintHandlers { + private lastNonTriviaPosition = 0; + private readonly writer: EmitTextWriter; + + public readonly onEmitNode: PrintHandlers["onEmitNode"]; + public readonly onBeforeEmitNodeArray: PrintHandlers["onBeforeEmitNodeArray"]; + public readonly onAfterEmitNodeArray: PrintHandlers["onAfterEmitNodeArray"]; + + constructor(newLine: string) { + this.writer = createTextWriter(newLine); + this.onEmitNode = (hint, node, printCallback) => { + if (node) { + setPos(node, this.lastNonTriviaPosition); + } + printCallback(hint, node); + if (node) { + setEnd(node, this.lastNonTriviaPosition); + } + }; + this.onBeforeEmitNodeArray = nodes => { + if (nodes) { + setPos(nodes, this.lastNonTriviaPosition); + } + }; + this.onAfterEmitNodeArray = nodes => { + if (nodes) { + setEnd(nodes, this.lastNonTriviaPosition); + } + }; + } + + private setLastNonTriviaPosition(s: string, force: boolean) { + if (force || !isTrivia(s)) { + this.lastNonTriviaPosition = this.writer.getTextPos(); + let i = 0; + while (isWhiteSpace(s.charCodeAt(s.length - i - 1))) { + i++; + } + // trim trailing whitespaces + this.lastNonTriviaPosition -= i; + } + } + + write(s: string): void { + this.writer.write(s); + this.setLastNonTriviaPosition(s, /*force*/ false); + } + writeTextOfNode(text: string, node: Node): void { + this.writer.writeTextOfNode(text, node); + } + writeLine(): void { + this.writer.writeLine(); + } + increaseIndent(): void { + this.writer.increaseIndent(); + } + decreaseIndent(): void { + this.writer.decreaseIndent(); + } + getText(): string { + return this.writer.getText(); + } + rawWrite(s: string): void { + this.writer.rawWrite(s); + this.setLastNonTriviaPosition(s, /*force*/ false); + } + writeLiteral(s: string): void { + this.writer.writeLiteral(s); + this.setLastNonTriviaPosition(s, /*force*/ true); + } + getTextPos(): number { + return this.writer.getTextPos(); + } + getLine(): number { + return this.writer.getLine(); + } + getColumn(): number { + return this.writer.getColumn(); + } + getIndent(): number { + return this.writer.getIndent(); + } + isAtStartOfLine(): boolean { + return this.writer.isAtStartOfLine(); + } + reset(): void { + this.writer.reset(); + this.lastNonTriviaPosition = 0; + } + } +} \ No newline at end of file diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index 93f861e80aa..b1391bd7300 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -63,6 +63,7 @@ "shims.ts", "signatureHelp.ts", "symbolDisplay.ts", + "textChanges.ts", "formatting/formatting.ts", "formatting/formattingContext.ts", "formatting/formattingRequestKind.ts", diff --git a/src/services/types.ts b/src/services/types.ts index c7852db6d6e..149bb4340dc 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -4,7 +4,11 @@ namespace ts { getChildCount(sourceFile?: SourceFile): number; getChildAt(index: number, sourceFile?: SourceFile): Node; getChildren(sourceFile?: SourceFile): Node[]; + /* @internal */ + getChildren(sourceFile?: SourceFileLike): Node[]; getStart(sourceFile?: SourceFile, includeJsDocComment?: boolean): number; + /* @internal */ + getStart(sourceFile?: SourceFileLike, includeJsDocComment?: boolean): number; getFullStart(): number; getEnd(): number; getWidth(sourceFile?: SourceFile): number; @@ -59,6 +63,10 @@ namespace ts { update(newText: string, textChangeRange: TextChangeRange): SourceFile; } + export interface SourceFileLike { + getLineAndCharacterOfPosition(pos: number): LineAndCharacter; + } + /** * Represents an immutable snapshot of a script at a specified time.Once acquired, the * snapshot is observably immutable. i.e. the same calls with the same parameters will return @@ -248,7 +256,7 @@ namespace ts { isValidBraceCompletionAtPosition(fileName: string, position: number, openingBrace: number): boolean; - getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[]): CodeAction[]; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: FormatCodeSettings): CodeAction[]; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 7c5bf862fc8..1b32b3bbad7 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -394,8 +394,8 @@ namespace ts { list: Node; } - export function getLineStartPositionForPosition(position: number, sourceFile: SourceFile): number { - const lineStarts = sourceFile.getLineStarts(); + export function getLineStartPositionForPosition(position: number, sourceFile: SourceFileLike): number { + const lineStarts = getLineStarts(sourceFile); const line = sourceFile.getLineAndCharacterOfPosition(position).line; return lineStarts[line]; } @@ -604,7 +604,7 @@ namespace ts { return !!findChildOfKind(n, kind, sourceFile); } - export function findChildOfKind(n: Node, kind: SyntaxKind, sourceFile?: SourceFile): Node | undefined { + export function findChildOfKind(n: Node, kind: SyntaxKind, sourceFile?: SourceFileLike): Node | undefined { return forEach(n.getChildren(sourceFile), c => c.kind === kind && c); } @@ -1003,10 +1003,6 @@ namespace ts { return undefined; } - export function isToken(n: Node): boolean { - return n.kind >= SyntaxKind.FirstToken && n.kind <= SyntaxKind.LastToken; - } - export function isWord(kind: SyntaxKind): boolean { return kind === SyntaxKind.Identifier || isKeyword(kind); } @@ -1384,8 +1380,8 @@ namespace ts { }; } - export function getOpenBraceEnd(constructor: ConstructorDeclaration, sourceFile: SourceFile) { + export function getOpenBrace(constructor: ConstructorDeclaration, sourceFile: SourceFile) { // First token is the open curly, this is where we want to put the 'super' call. - return constructor.body.getFirstToken(sourceFile).getEnd(); + return constructor.body.getFirstToken(sourceFile); } } diff --git a/tests/baselines/reference/textChanges/deleteNode1.js b/tests/baselines/reference/textChanges/deleteNode1.js new file mode 100644 index 00000000000..c73c0eaf9f9 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNode1.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +var x = 1; // some comment - 1 +/** + * comment 2 + */ +var y = 2; // comment 3 +var z = 3; // comment 4 + +===MODIFIED=== + +var x = 1; // some comment - 1 +var z = 3; // comment 4 diff --git a/tests/baselines/reference/textChanges/deleteNode2.js b/tests/baselines/reference/textChanges/deleteNode2.js new file mode 100644 index 00000000000..828a9b5b8fa --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNode2.js @@ -0,0 +1,12 @@ +===ORIGINAL=== + +var x = 1; // some comment - 1 +/** + * comment 2 + */ +var y = 2; // comment 3 +var z = 3; // comment 4 + +===MODIFIED=== + +var x = 1;var z = 3; // comment 4 diff --git a/tests/baselines/reference/textChanges/deleteNode3.js b/tests/baselines/reference/textChanges/deleteNode3.js new file mode 100644 index 00000000000..acb26c7daa7 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNode3.js @@ -0,0 +1,14 @@ +===ORIGINAL=== + +var x = 1; // some comment - 1 +/** + * comment 2 + */ +var y = 2; // comment 3 +var z = 3; // comment 4 + +===MODIFIED=== + +var x = 1; // some comment - 1 + // comment 3 +var z = 3; // comment 4 diff --git a/tests/baselines/reference/textChanges/deleteNode4.js b/tests/baselines/reference/textChanges/deleteNode4.js new file mode 100644 index 00000000000..b6edb25bc5c --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNode4.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +var x = 1; // some comment - 1 +/** + * comment 2 + */ +var y = 2; // comment 3 +var z = 3; // comment 4 + +===MODIFIED=== + +var x = 1; // comment 3 +var z = 3; // comment 4 diff --git a/tests/baselines/reference/textChanges/deleteNode5.js b/tests/baselines/reference/textChanges/deleteNode5.js new file mode 100644 index 00000000000..6f896535f0f --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNode5.js @@ -0,0 +1,16 @@ +===ORIGINAL=== + +var x = 1; // some comment - 1 +/** + * comment 2 + */ +var y = 2; // comment 3 +var z = 3; // comment 4 + +===MODIFIED=== + +/** + * comment 2 + */ +var y = 2; // comment 3 +var z = 3; // comment 4 diff --git a/tests/baselines/reference/textChanges/deleteNodeAfterInClass1.js b/tests/baselines/reference/textChanges/deleteNodeAfterInClass1.js new file mode 100644 index 00000000000..d8d54153eaa --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeAfterInClass1.js @@ -0,0 +1,12 @@ +===ORIGINAL=== + +class A { + x; + y = 1; +} + +===MODIFIED=== + +class A { + y = 1; +} diff --git a/tests/baselines/reference/textChanges/deleteNodeAfterInClass2.js b/tests/baselines/reference/textChanges/deleteNodeAfterInClass2.js new file mode 100644 index 00000000000..8d18d820214 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeAfterInClass2.js @@ -0,0 +1,12 @@ +===ORIGINAL=== + +class A { + x + y = 1; +} + +===MODIFIED=== + +class A { + y = 1; +} diff --git a/tests/baselines/reference/textChanges/deleteNodeInList1.js b/tests/baselines/reference/textChanges/deleteNodeInList1.js new file mode 100644 index 00000000000..b603913566d --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList1.js @@ -0,0 +1,4 @@ +===ORIGINAL=== +var a = 1, b = 2, c = 3; +===MODIFIED=== +var b = 2, c = 3; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList10.js b/tests/baselines/reference/textChanges/deleteNodeInList10.js new file mode 100644 index 00000000000..46da605a71a --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList10.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +function foo(a: number,b: string,c = true) { + return 1; +} +===MODIFIED=== + +function foo(b: string,c = true) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList11.js b/tests/baselines/reference/textChanges/deleteNodeInList11.js new file mode 100644 index 00000000000..f5aa666e527 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList11.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +function foo(a: number,b: string,c = true) { + return 1; +} +===MODIFIED=== + +function foo(a: number,c = true) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList12.js b/tests/baselines/reference/textChanges/deleteNodeInList12.js new file mode 100644 index 00000000000..db05ef06db9 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList12.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +function foo(a: number,b: string,c = true) { + return 1; +} +===MODIFIED=== + +function foo(a: number,b: string) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList13.js b/tests/baselines/reference/textChanges/deleteNodeInList13.js new file mode 100644 index 00000000000..599d3a31d43 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList13.js @@ -0,0 +1,15 @@ +===ORIGINAL=== + +function foo( + a: number, + b: string, + c = true) { + return 1; +} +===MODIFIED=== + +function foo( + b: string, + c = true) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList14.js b/tests/baselines/reference/textChanges/deleteNodeInList14.js new file mode 100644 index 00000000000..da3596b38b2 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList14.js @@ -0,0 +1,15 @@ +===ORIGINAL=== + +function foo( + a: number, + b: string, + c = true) { + return 1; +} +===MODIFIED=== + +function foo( + a: number, + c = true) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList15.js b/tests/baselines/reference/textChanges/deleteNodeInList15.js new file mode 100644 index 00000000000..02f649dabd2 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList15.js @@ -0,0 +1,15 @@ +===ORIGINAL=== + +function foo( + a: number, + b: string, + c = true) { + return 1; +} +===MODIFIED=== + +function foo( + a: number, + b: string) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList1_1.js b/tests/baselines/reference/textChanges/deleteNodeInList1_1.js new file mode 100644 index 00000000000..87709601c42 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList1_1.js @@ -0,0 +1,4 @@ +===ORIGINAL=== +var a = 1,b = 2,c = 3; +===MODIFIED=== +var b = 2,c = 3; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList2.js b/tests/baselines/reference/textChanges/deleteNodeInList2.js new file mode 100644 index 00000000000..18e4a0cd304 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList2.js @@ -0,0 +1,4 @@ +===ORIGINAL=== +var a = 1, b = 2, c = 3; +===MODIFIED=== +var a = 1, c = 3; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList2_1.js b/tests/baselines/reference/textChanges/deleteNodeInList2_1.js new file mode 100644 index 00000000000..b4098a89302 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList2_1.js @@ -0,0 +1,4 @@ +===ORIGINAL=== +var a = 1,b = 2,c = 3; +===MODIFIED=== +var a = 1,c = 3; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList3.js b/tests/baselines/reference/textChanges/deleteNodeInList3.js new file mode 100644 index 00000000000..1c6ff4aa294 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList3.js @@ -0,0 +1,4 @@ +===ORIGINAL=== +var a = 1, b = 2, c = 3; +===MODIFIED=== +var a = 1, b = 2; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList3_1.js b/tests/baselines/reference/textChanges/deleteNodeInList3_1.js new file mode 100644 index 00000000000..ead2a1a42f8 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList3_1.js @@ -0,0 +1,4 @@ +===ORIGINAL=== +var a = 1,b = 2,c = 3; +===MODIFIED=== +var a = 1,b = 2; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList4.js b/tests/baselines/reference/textChanges/deleteNodeInList4.js new file mode 100644 index 00000000000..61bdf9159cb --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList4.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +namespace M { + var a = 1, + b = 2, + c = 3; +} +===MODIFIED=== + +namespace M { + var b = 2, + c = 3; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList4_1.js b/tests/baselines/reference/textChanges/deleteNodeInList4_1.js new file mode 100644 index 00000000000..19ff5475d1a --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList4_1.js @@ -0,0 +1,17 @@ +===ORIGINAL=== + +namespace M { + var a = 1, // comment 1 + // comment 2 + b = 2, // comment 3 + // comment 4 + c = 3; // comment 5 +} +===MODIFIED=== + +namespace M { + var // comment 2 + b = 2, // comment 3 + // comment 4 + c = 3; // comment 5 +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList5.js b/tests/baselines/reference/textChanges/deleteNodeInList5.js new file mode 100644 index 00000000000..0ed65877d27 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList5.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +namespace M { + var a = 1, + b = 2, + c = 3; +} +===MODIFIED=== + +namespace M { + var a = 1, + c = 3; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList5_1.js b/tests/baselines/reference/textChanges/deleteNodeInList5_1.js new file mode 100644 index 00000000000..224b92481d3 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList5_1.js @@ -0,0 +1,16 @@ +===ORIGINAL=== + +namespace M { + var a = 1, // comment 1 + // comment 2 + b = 2, // comment 3 + // comment 4 + c = 3; // comment 5 +} +===MODIFIED=== + +namespace M { + var a = 1, // comment 1 + // comment 4 + c = 3; // comment 5 +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList6.js b/tests/baselines/reference/textChanges/deleteNodeInList6.js new file mode 100644 index 00000000000..4093329023f --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList6.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +namespace M { + var a = 1, + b = 2, + c = 3; +} +===MODIFIED=== + +namespace M { + var a = 1, + b = 2; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList6_1.js b/tests/baselines/reference/textChanges/deleteNodeInList6_1.js new file mode 100644 index 00000000000..6acc20aa826 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList6_1.js @@ -0,0 +1,16 @@ +===ORIGINAL=== + +namespace M { + var a = 1, // comment 1 + // comment 2 + b = 2, // comment 3 + // comment 4 + c = 3; // comment 5 +} +===MODIFIED=== + +namespace M { + var a = 1, // comment 1 + // comment 2 + b = 2; // comment 5 +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList7.js b/tests/baselines/reference/textChanges/deleteNodeInList7.js new file mode 100644 index 00000000000..564ff3f86aa --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList7.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +function foo(a: number, b: string, c = true) { + return 1; +} +===MODIFIED=== + +function foo(b: string, c = true) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList8.js b/tests/baselines/reference/textChanges/deleteNodeInList8.js new file mode 100644 index 00000000000..b3a0a0f4c47 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList8.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +function foo(a: number, b: string, c = true) { + return 1; +} +===MODIFIED=== + +function foo(a: number, c = true) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeInList9.js b/tests/baselines/reference/textChanges/deleteNodeInList9.js new file mode 100644 index 00000000000..96878a6086d --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeInList9.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +function foo(a: number, b: string, c = true) { + return 1; +} +===MODIFIED=== + +function foo(a: number, b: string) { + return 1; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/deleteNodeRange1.js b/tests/baselines/reference/textChanges/deleteNodeRange1.js new file mode 100644 index 00000000000..f9fa45ebb1a --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeRange1.js @@ -0,0 +1,16 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 + +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +// comment 6 +var a = 4; // comment 7 diff --git a/tests/baselines/reference/textChanges/deleteNodeRange2.js b/tests/baselines/reference/textChanges/deleteNodeRange2.js new file mode 100644 index 00000000000..aacea6ff5ee --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeRange2.js @@ -0,0 +1,15 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 + +===MODIFIED=== + +// comment 1 +var x = 1;// comment 6 +var a = 4; // comment 7 diff --git a/tests/baselines/reference/textChanges/deleteNodeRange3.js b/tests/baselines/reference/textChanges/deleteNodeRange3.js new file mode 100644 index 00000000000..bb2bb36ca2c --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeRange3.js @@ -0,0 +1,17 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 + +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 + // comment 5 +// comment 6 +var a = 4; // comment 7 diff --git a/tests/baselines/reference/textChanges/deleteNodeRange4.js b/tests/baselines/reference/textChanges/deleteNodeRange4.js new file mode 100644 index 00000000000..3e9ad960b13 --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteNodeRange4.js @@ -0,0 +1,16 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 + +===MODIFIED=== + +// comment 1 +var x = 1; // comment 5 +// comment 6 +var a = 4; // comment 7 diff --git a/tests/baselines/reference/textChanges/deleteRange1.js b/tests/baselines/reference/textChanges/deleteRange1.js new file mode 100644 index 00000000000..362e678452e --- /dev/null +++ b/tests/baselines/reference/textChanges/deleteRange1.js @@ -0,0 +1,15 @@ +===ORIGINAL=== + +function foo() { + return 1; +} + +function bar() { + return 2; +} + +===MODIFIED=== + +function bar() { + return 2; +} diff --git a/tests/baselines/reference/textChanges/extractMethodLike.js b/tests/baselines/reference/textChanges/extractMethodLike.js new file mode 100644 index 00000000000..fa1d625dc4d --- /dev/null +++ b/tests/baselines/reference/textChanges/extractMethodLike.js @@ -0,0 +1,49 @@ +===ORIGINAL=== + +namespace M +{ + namespace M2 + { + function foo() { + // comment 1 + const x = 1; + + /** + * comment 2 line 1 + * comment 2 line 2 + */ + function f() { + return 100; + } + const y = 2; // comment 3 + return 1; + } + } +} +===MODIFIED=== + +namespace M +{ + function bar(): any + { + /** + * comment 2 line 1 + * comment 2 line 2 + */ + function f() + { + return 100; + } + const y = 2; // comment 3 + return 1; + } + namespace M2 + { + function foo() { + // comment 1 + const x = 1; + + return bar(); + } + } +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeAfter1.js b/tests/baselines/reference/textChanges/insertNodeAfter1.js new file mode 100644 index 00000000000..e6d7387f6bc --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAfter1.js @@ -0,0 +1,26 @@ +===ORIGINAL=== + +namespace M { + // comment 1 + var x = 1; // comment 2 + // comment 3 + var y; // comment 4 + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +} +===MODIFIED=== + +namespace M { + // comment 1 + var x = 1; // comment 2 + // comment 3 + var y; // comment 4 + public class class1 implements interface1 + { + property1: boolean; + } + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeAfter2.js b/tests/baselines/reference/textChanges/insertNodeAfter2.js new file mode 100644 index 00000000000..7c027e35b50 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAfter2.js @@ -0,0 +1,26 @@ +===ORIGINAL=== + +namespace M { + // comment 1 + var x = 1; // comment 2 + // comment 3 + var y; // comment 4 + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +} +===MODIFIED=== + +namespace M { + // comment 1 + var x = 1; // comment 2 + // comment 3 + var y; // comment 4 + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +} +public class class1 implements interface1 +{ + property1: boolean; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeAfter3-block with newline.js b/tests/baselines/reference/textChanges/insertNodeAfter3-block with newline.js new file mode 100644 index 00000000000..41bfb72b574 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAfter3-block with newline.js @@ -0,0 +1,16 @@ +===ORIGINAL=== + +class A { + constructor() { + + } +} + +===MODIFIED=== + +class A { + constructor() { + super(); + + } +} diff --git a/tests/baselines/reference/textChanges/insertNodeAfter3.js b/tests/baselines/reference/textChanges/insertNodeAfter3.js new file mode 100644 index 00000000000..3765bbaf032 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAfter3.js @@ -0,0 +1,14 @@ +===ORIGINAL=== + +class A { + constructor() { + } +} + +===MODIFIED=== + +class A { + constructor() { + super(); + } +} diff --git a/tests/baselines/reference/textChanges/insertNodeAfter4.js b/tests/baselines/reference/textChanges/insertNodeAfter4.js new file mode 100644 index 00000000000..3ca51db4989 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAfter4.js @@ -0,0 +1,16 @@ +===ORIGINAL=== + +class A { + constructor() { + var x = 1; + } +} + +===MODIFIED=== + +class A { + constructor() { + var x = 1; + super(); + } +} diff --git a/tests/baselines/reference/textChanges/insertNodeAfterInClass1.js b/tests/baselines/reference/textChanges/insertNodeAfterInClass1.js new file mode 100644 index 00000000000..841d43fef97 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAfterInClass1.js @@ -0,0 +1,12 @@ +===ORIGINAL=== + +class A { + x +} + +===MODIFIED=== + +class A { + x; + a: boolean; +} diff --git a/tests/baselines/reference/textChanges/insertNodeAfterInClass2.js b/tests/baselines/reference/textChanges/insertNodeAfterInClass2.js new file mode 100644 index 00000000000..1d583d8ac90 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAfterInClass2.js @@ -0,0 +1,12 @@ +===ORIGINAL=== + +class A { + x; +} + +===MODIFIED=== + +class A { + x; + a: boolean; +} diff --git a/tests/baselines/reference/textChanges/insertNodeAfterMultipleNodes.js b/tests/baselines/reference/textChanges/insertNodeAfterMultipleNodes.js new file mode 100644 index 00000000000..1c3bbe4d3e2 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAfterMultipleNodes.js @@ -0,0 +1,21 @@ +===ORIGINAL=== + +class A { + x; +} +===MODIFIED=== + +class A { + x; + 0; + 1; + 2; + 3; + 4; + 5; + 6; + 7; + 8; + 9; + 10; +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeAt1.js b/tests/baselines/reference/textChanges/insertNodeAt1.js new file mode 100644 index 00000000000..9c7ac46f8a8 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAt1.js @@ -0,0 +1,22 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +public class class1 implements interface1 +{ + property1: boolean; +} +var y; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeAt2.js b/tests/baselines/reference/textChanges/insertNodeAt2.js new file mode 100644 index 00000000000..81c3e2afc35 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeAt2.js @@ -0,0 +1,20 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var yz1 = { + p1: 1 +}; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeBefore1.js b/tests/baselines/reference/textChanges/insertNodeBefore1.js new file mode 100644 index 00000000000..deebe10b694 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeBefore1.js @@ -0,0 +1,26 @@ +===ORIGINAL=== + +namespace M { + // comment 1 + var x = 1; // comment 2 + // comment 3 + var y; // comment 4 + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +} +===MODIFIED=== + +namespace M { + // comment 1 + var x = 1; // comment 2 + public class class1 implements interface1 + { + property1: boolean; + } + // comment 3 + var y; // comment 4 + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeBefore2.js b/tests/baselines/reference/textChanges/insertNodeBefore2.js new file mode 100644 index 00000000000..1792cd5e69c --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeBefore2.js @@ -0,0 +1,26 @@ +===ORIGINAL=== + +namespace M { + // comment 1 + var x = 1; // comment 2 + // comment 3 + var y; // comment 4 + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +} +===MODIFIED=== + +public class class1 implements interface1 +{ + property1: boolean; +} +namespace M { + // comment 1 + var x = 1; // comment 2 + // comment 3 + var y; // comment 4 + var z = 3; // comment 5 + // comment 6 + var a = 4; // comment 7 +} \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInClassAfterNodeWithoutSeparator1.js b/tests/baselines/reference/textChanges/insertNodeInClassAfterNodeWithoutSeparator1.js new file mode 100644 index 00000000000..0c47c26221f --- /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 00000000000..bd6302b29a6 --- /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 00000000000..61910e64dd5 --- /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 00000000000..6e88a371eb6 --- /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/insertNodeInListAfter1.js b/tests/baselines/reference/textChanges/insertNodeInListAfter1.js new file mode 100644 index 00000000000..9b0ed7b819d --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter1.js @@ -0,0 +1,6 @@ +===ORIGINAL=== + +const x = 1, y = 2; +===MODIFIED=== + +const x = 1, z = 1, y = 2; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter10.js b/tests/baselines/reference/textChanges/insertNodeInListAfter10.js new file mode 100644 index 00000000000..300230bf51c --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter10.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +import { + x +} from "bar" +===MODIFIED=== + +import { + x, b as a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter11.js b/tests/baselines/reference/textChanges/insertNodeInListAfter11.js new file mode 100644 index 00000000000..f0ba43a0f3a --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter11.js @@ -0,0 +1,11 @@ +===ORIGINAL=== + +import { + x // this is x +} from "bar" +===MODIFIED=== + +import { + x, // this is x + b as a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter12.js b/tests/baselines/reference/textChanges/insertNodeInListAfter12.js new file mode 100644 index 00000000000..79e7bb2c8b6 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter12.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +import { + x +} from "bar" +===MODIFIED=== + +import { + x, a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter13.js b/tests/baselines/reference/textChanges/insertNodeInListAfter13.js new file mode 100644 index 00000000000..a7ac7179ab6 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter13.js @@ -0,0 +1,11 @@ +===ORIGINAL=== + +import { + x // this is x +} from "bar" +===MODIFIED=== + +import { + x, // this is x + a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter14.js b/tests/baselines/reference/textChanges/insertNodeInListAfter14.js new file mode 100644 index 00000000000..e44a21723be --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter14.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +import { + x0, + x +} from "bar" +===MODIFIED=== + +import { + x0, + x, + b as a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter15.js b/tests/baselines/reference/textChanges/insertNodeInListAfter15.js new file mode 100644 index 00000000000..a80bdc2db5e --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter15.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +import { + x0, + x // this is x +} from "bar" +===MODIFIED=== + +import { + x0, + x, // this is x + b as a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter16.js b/tests/baselines/reference/textChanges/insertNodeInListAfter16.js new file mode 100644 index 00000000000..07f8db919f8 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter16.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +import { + x0, + x +} from "bar" +===MODIFIED=== + +import { + x0, + x, + a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter17.js b/tests/baselines/reference/textChanges/insertNodeInListAfter17.js new file mode 100644 index 00000000000..990139408ee --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter17.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +import { + x0, + x // this is x +} from "bar" +===MODIFIED=== + +import { + x0, + x, // this is x + a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter18.js b/tests/baselines/reference/textChanges/insertNodeInListAfter18.js new file mode 100644 index 00000000000..b25a3f46346 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter18.js @@ -0,0 +1,10 @@ +===ORIGINAL=== + +import { + x0, x +} from "bar" +===MODIFIED=== + +import { + x0, x, a +} from "bar" \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter2.js b/tests/baselines/reference/textChanges/insertNodeInListAfter2.js new file mode 100644 index 00000000000..32e8e63c0f6 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter2.js @@ -0,0 +1,6 @@ +===ORIGINAL=== + +const x = 1, y = 2; +===MODIFIED=== + +const x = 1, y = 2, z = 1; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter3.js b/tests/baselines/reference/textChanges/insertNodeInListAfter3.js new file mode 100644 index 00000000000..70a0b0bddcb --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter3.js @@ -0,0 +1,6 @@ +===ORIGINAL=== + +const /*x*/ x = 1, /*y*/ y = 2; +===MODIFIED=== + +const /*x*/ x = 1, z = 1, /*y*/ y = 2; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter4.js b/tests/baselines/reference/textChanges/insertNodeInListAfter4.js new file mode 100644 index 00000000000..4a30acd7071 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter4.js @@ -0,0 +1,6 @@ +===ORIGINAL=== + +const /*x*/ x = 1, /*y*/ y = 2; +===MODIFIED=== + +const /*x*/ x = 1, /*y*/ y = 2, z = 1; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter5.js b/tests/baselines/reference/textChanges/insertNodeInListAfter5.js new file mode 100644 index 00000000000..42d5fe2ca40 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter5.js @@ -0,0 +1,6 @@ +===ORIGINAL=== + +const x = 1; +===MODIFIED=== + +const x = 1, z = 1; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter6.js b/tests/baselines/reference/textChanges/insertNodeInListAfter6.js new file mode 100644 index 00000000000..06eae7372a9 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter6.js @@ -0,0 +1,9 @@ +===ORIGINAL=== + +const x = 1, + y = 2; +===MODIFIED=== + +const x = 1, + z = 1, + y = 2; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter7.js b/tests/baselines/reference/textChanges/insertNodeInListAfter7.js new file mode 100644 index 00000000000..bef01503683 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter7.js @@ -0,0 +1,9 @@ +===ORIGINAL=== + +const x = 1, + y = 2; +===MODIFIED=== + +const x = 1, + y = 2, + z = 1; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter8.js b/tests/baselines/reference/textChanges/insertNodeInListAfter8.js new file mode 100644 index 00000000000..e3c44b14274 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter8.js @@ -0,0 +1,9 @@ +===ORIGINAL=== + +const /*x*/ x = 1, + /*y*/ y = 2; +===MODIFIED=== + +const /*x*/ x = 1, + z = 1, + /*y*/ y = 2; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInListAfter9.js b/tests/baselines/reference/textChanges/insertNodeInListAfter9.js new file mode 100644 index 00000000000..510984b7574 --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInListAfter9.js @@ -0,0 +1,9 @@ +===ORIGINAL=== + +const /*x*/ x = 1, + /*y*/ y = 2; +===MODIFIED=== + +const /*x*/ x = 1, + /*y*/ y = 2, + z = 1; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/insertNodeInStatementListAfterNodeWithoutSeparator1.js b/tests/baselines/reference/textChanges/insertNodeInStatementListAfterNodeWithoutSeparator1.js new file mode 100644 index 00000000000..b28b1579dfc --- /dev/null +++ b/tests/baselines/reference/textChanges/insertNodeInStatementListAfterNodeWithoutSeparator1.js @@ -0,0 +1,8 @@ +===ORIGINAL=== + +let x = foo + +===MODIFIED=== + +let x = foo; +(1); diff --git a/tests/baselines/reference/textChanges/replaceNode1.js b/tests/baselines/reference/textChanges/replaceNode1.js new file mode 100644 index 00000000000..8511d510385 --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNode1.js @@ -0,0 +1,20 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +public class class1 implements interface1 +{ + property1: boolean; +} +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceNode1NoLineBreakBefore.js b/tests/baselines/reference/textChanges/replaceNode1NoLineBreakBefore.js new file mode 100644 index 00000000000..44183d402ab --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNode1NoLineBreakBefore.js @@ -0,0 +1,13 @@ +===ORIGINAL=== + +namespace A { + const x = 1, y = "2"; +} + +===MODIFIED=== + +namespace A { + const x = 1, z1 = { + p1: 1 + }; +} diff --git a/tests/baselines/reference/textChanges/replaceNode2.js b/tests/baselines/reference/textChanges/replaceNode2.js new file mode 100644 index 00000000000..66a17f7c910 --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNode2.js @@ -0,0 +1,20 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; +public class class1 implements interface1 +{ + property1: boolean; +} +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceNode3.js b/tests/baselines/reference/textChanges/replaceNode3.js new file mode 100644 index 00000000000..9f321a97431 --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNode3.js @@ -0,0 +1,21 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +public class class1 implements interface1 +{ + property1: boolean; +} + // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceNode4.js b/tests/baselines/reference/textChanges/replaceNode4.js new file mode 100644 index 00000000000..e5bbf0e306d --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNode4.js @@ -0,0 +1,19 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1;public class class1 implements interface1 +{ + property1: boolean; +} // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceNode5.js b/tests/baselines/reference/textChanges/replaceNode5.js new file mode 100644 index 00000000000..ad3ec0ab0ea --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNode5.js @@ -0,0 +1,19 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== +public class class1 implements interface1 +{ + property1: boolean; +} // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceNodeRange1.js b/tests/baselines/reference/textChanges/replaceNodeRange1.js new file mode 100644 index 00000000000..5fc16960a8a --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNodeRange1.js @@ -0,0 +1,19 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +public class class1 implements interface1 +{ + property1: boolean; +} +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceNodeRange2.js b/tests/baselines/reference/textChanges/replaceNodeRange2.js new file mode 100644 index 00000000000..e8f9506d608 --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNodeRange2.js @@ -0,0 +1,19 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; +public class class1 implements interface1 +{ + property1: boolean; +} +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceNodeRange3.js b/tests/baselines/reference/textChanges/replaceNodeRange3.js new file mode 100644 index 00000000000..a20dddc53fe --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNodeRange3.js @@ -0,0 +1,20 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +public class class1 implements interface1 +{ + property1: boolean; +} + // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceNodeRange4.js b/tests/baselines/reference/textChanges/replaceNodeRange4.js new file mode 100644 index 00000000000..ad50a1f01fc --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceNodeRange4.js @@ -0,0 +1,18 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1;public class class1 implements interface1 +{ + property1: boolean; +} // comment 5 +// comment 6 +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceRange.js b/tests/baselines/reference/textChanges/replaceRange.js new file mode 100644 index 00000000000..8cb83286169 --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceRange.js @@ -0,0 +1,19 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +public class class1 implements interface1 +{ + property1: boolean; +} +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceRangeNoLineBreakBefore.js b/tests/baselines/reference/textChanges/replaceRangeNoLineBreakBefore.js new file mode 100644 index 00000000000..72a45ee56e2 --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceRangeNoLineBreakBefore.js @@ -0,0 +1,6 @@ +===ORIGINAL=== +const x = 1, y = "2"; +===MODIFIED=== +const x = 1, z1 = { + p1: 1 +}; \ No newline at end of file diff --git a/tests/baselines/reference/textChanges/replaceRangeWithForcedIndentation.js b/tests/baselines/reference/textChanges/replaceRangeWithForcedIndentation.js new file mode 100644 index 00000000000..304b81b1359 --- /dev/null +++ b/tests/baselines/reference/textChanges/replaceRangeWithForcedIndentation.js @@ -0,0 +1,19 @@ +===ORIGINAL=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 +var y = 2; // comment 4 +var z = 3; // comment 5 +// comment 6 +var a = 4; // comment 7 +===MODIFIED=== + +// comment 1 +var x = 1; // comment 2 +// comment 3 + public class class1 implements interface1 + { + property1: boolean; + } +var a = 4; // comment 7 \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixAddForgottenThis01.ts b/tests/cases/fourslash/codeFixAddForgottenThis01.ts index 90d191b6ba5..07f06348212 100644 --- a/tests/cases/fourslash/codeFixAddForgottenThis01.ts +++ b/tests/cases/fourslash/codeFixAddForgottenThis01.ts @@ -2,9 +2,11 @@ ////class C { //// foo: number; -//// constructor() { -//// [|foo = 10|]; -//// } +//// constructor() {[| +//// foo = 10; +//// |]} ////} -verify.rangeAfterCodeFix("this.foo = 10"); \ No newline at end of file +verify.rangeAfterCodeFix(` + this.foo = 10; + `, /*includeWhitespace*/ true); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixSuperAfterThis.ts b/tests/cases/fourslash/codeFixSuperAfterThis.ts index 2bff98ceca0..55b44b07881 100644 --- a/tests/cases/fourslash/codeFixSuperAfterThis.ts +++ b/tests/cases/fourslash/codeFixSuperAfterThis.ts @@ -6,8 +6,10 @@ //// private a:number; //// constructor() {[| //// this.a = 12; -//// super();|] -//// } +//// super(); +//// |]} ////} - -verify.rangeAfterCodeFix("super(); this.a = 12;"); \ No newline at end of file +verify.rangeAfterCodeFix(` + super(); + this.a = 12; + `, /*includeWhiteSpace*/ true); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixSuperCall.ts b/tests/cases/fourslash/codeFixSuperCall.ts index c918e74dc38..f7584050a15 100644 --- a/tests/cases/fourslash/codeFixSuperCall.ts +++ b/tests/cases/fourslash/codeFixSuperCall.ts @@ -1,10 +1,11 @@ -/// +/// ////class Base{ ////} ////class C extends Base{ -//// constructor() {[| |] -//// } +//// constructor() {[| +//// |]} ////} - -verify.rangeAfterCodeFix('super();'); +verify.rangeAfterCodeFix(` + super(); + `, /*includeWhitespace*/ true); diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport10.ts b/tests/cases/fourslash/importNameCodeFixExistingImport10.ts index 25246e70123..8a178e72730 100644 --- a/tests/cases/fourslash/importNameCodeFixExistingImport10.ts +++ b/tests/cases/fourslash/importNameCodeFixExistingImport10.ts @@ -16,6 +16,6 @@ verify.importFixAtPosition([ `{ v1, v2, -f1 + f1 }` ]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport11.ts b/tests/cases/fourslash/importNameCodeFixExistingImport11.ts index 304ffb896df..8822356c13a 100644 --- a/tests/cases/fourslash/importNameCodeFixExistingImport11.ts +++ b/tests/cases/fourslash/importNameCodeFixExistingImport11.ts @@ -15,6 +15,7 @@ verify.importFixAtPosition([ `{ v1, v2, - v3, f1 + v3, + f1 }` ]); \ No newline at end of file diff --git a/tests/cases/fourslash/importNameCodeFixExistingImport9.ts b/tests/cases/fourslash/importNameCodeFixExistingImport9.ts index 05d17927454..51496abff12 100644 --- a/tests/cases/fourslash/importNameCodeFixExistingImport9.ts +++ b/tests/cases/fourslash/importNameCodeFixExistingImport9.ts @@ -11,7 +11,6 @@ verify.importFixAtPosition([ `{ - v1, -f1 + v1, f1 }` ]); \ No newline at end of file diff --git a/tests/cases/fourslash/unusedFunctionInNamespace1.ts b/tests/cases/fourslash/unusedFunctionInNamespace1.ts index 288573608f2..7a63ff13caa 100644 --- a/tests/cases/fourslash/unusedFunctionInNamespace1.ts +++ b/tests/cases/fourslash/unusedFunctionInNamespace1.ts @@ -8,5 +8,4 @@ //// } |] verify.rangeAfterCodeFix(`namespace greeter { - // some legit comments }`); diff --git a/tests/cases/fourslash/unusedVariableInForLoop7FS.ts b/tests/cases/fourslash/unusedVariableInForLoop7FS.ts index 5aa3251c0b4..7f99863ba46 100644 --- a/tests/cases/fourslash/unusedVariableInForLoop7FS.ts +++ b/tests/cases/fourslash/unusedVariableInForLoop7FS.ts @@ -1,12 +1,16 @@ /// // @noUnusedLocals: true -//// function f1 () { -//// for (const elem of ["a", "b", "c"]) { -//// elem; -//// [|var x = 20; -//// }|] -////} +////function f1 () [|{ +//// for (const elem of ["a", "b", "c"]) { +//// elem; +//// var x = 20; +//// } +////}|] //// -verify.rangeAfterCodeFix("}"); +verify.rangeAfterCodeFix(`{ + for (const elem of ["a", "b", "c"]) { + elem; + } +}`, /*includeWhiteSpace*/ true);