mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-30 01:04:49 -05:00
initial revision of infrastructure to produce text changes that uses existing node factory, formatter and printer
This commit is contained in:
@@ -13,6 +13,7 @@ namespace ts {
|
||||
newLineCharacter: string;
|
||||
host: LanguageServiceHost;
|
||||
cancellationToken: CancellationToken;
|
||||
rulesProvider: formatting.RulesProvider;
|
||||
}
|
||||
|
||||
export namespace codefix {
|
||||
|
||||
@@ -26,22 +26,13 @@ namespace ts.codefix {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newPosition = getOpenBraceEnd(<ConstructorDeclaration>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(<ConstructorDeclaration>constructor, sourceFile), superCall, { insertTrailingNewLine: true });
|
||||
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 {
|
||||
|
||||
@@ -10,10 +10,13 @@ namespace ts.codefix {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const newPosition = getOpenBraceEnd(<ConstructorDeclaration>token.parent, sourceFile);
|
||||
const changeTracker = textChanges.ChangeTracker.fromCodeFixContext(context);
|
||||
const superCall = createStatement(createCall(createSuper(), /*typeArguments*/ undefined, /*argumentsArray*/ emptyArray));
|
||||
changeTracker.insertNodeAfter(sourceFile, getOpenBrace(<ConstructorDeclaration>token.parent, sourceFile), superCall, { insertTrailingNewLine: true });
|
||||
|
||||
return [{
|
||||
description: getLocaleSpecificMessage(Diagnostics.Add_missing_super_call),
|
||||
changes: [{ fileName: sourceFile.fileName, textChanges: [{ newText: "super();", span: { start: newPosition, length: 0 } }] }]
|
||||
changes: changeTracker.getChanges()
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(), <Identifier>token));
|
||||
|
||||
return [{
|
||||
description: getLocaleSpecificMessage(Diagnostics.Add_this_to_unresolved_variable),
|
||||
changes: [{ fileName: sourceFile.fileName, textChanges: [{ newText: "this.", span: { start, length: 0 } }] }]
|
||||
changes: changeTracker.getChanges()
|
||||
}];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -25,17 +25,17 @@ namespace ts.codefix {
|
||||
const forStatement = <ForStatement>token.parent.parent.parent;
|
||||
const forInitializer = <VariableDeclarationList>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 = <ForOfStatement>token.parent.parent.parent;
|
||||
if (forOfStatement.initializer.kind === SyntaxKind.VariableDeclarationList) {
|
||||
const forOfInitializer = <VariableDeclarationList>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 = <CatchClause>token.parent.parent;
|
||||
const parameter = catchClause.variableDeclaration.getChildren()[0];
|
||||
return createCodeFixToRemoveNode(parameter);
|
||||
return deleteNode(parameter);
|
||||
|
||||
default:
|
||||
const variableStatement = <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 = (<DeclarationWithTypeParameters>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 = <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 = <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 = <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 = <NamespaceImport>token.parent;
|
||||
if (namespaceImport.name == token && !(<ImportClause>namespaceImport.parent).name) {
|
||||
const importDecl = findImportDeclaration(namespaceImport);
|
||||
return createCodeFixToRemoveNode(importDecl);
|
||||
const importDecl = getAncestor(namespaceImport, SyntaxKind.ImportDeclaration);
|
||||
return deleteNode(importDecl);
|
||||
}
|
||||
else {
|
||||
const start = (<ImportClause>namespaceImport.parent).name.end;
|
||||
return createCodeFix("", start, (<ImportClause>namespaceImport.parent).namedBindings.end - start);
|
||||
const previousToken = getTokenAtPosition(sourceFile, namespaceImport.pos - 1);
|
||||
if (previousToken && previousToken.kind === SyntaxKind.CommaToken) {
|
||||
const startPosition = textChanges.getAdjustedStartPosition(sourceFile, previousToken, {}, /*forDeleteOperation*/ true);
|
||||
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<T extends Node>(elements: NodeArray<T>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -24,6 +24,10 @@ namespace ts.formatting {
|
||||
return this.rulesMap;
|
||||
}
|
||||
|
||||
public getFormatOptions(): Readonly<ts.FormatCodeSettings> {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
public ensureUpToDate(options: ts.FormatCodeSettings) {
|
||||
if (!this.options || !ts.compareDataObjects(this.options, options)) {
|
||||
const activeRules = this.createActiveRules(options);
|
||||
|
||||
@@ -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 && (<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<Node> {
|
||||
function getListIfStartEndIsInListRange(list: NodeArray<Node>, start: number, end: number) {
|
||||
return list && rangeContainsStartEnd(list, start, end) ? list : undefined;
|
||||
}
|
||||
|
||||
export function getContainingList(node: Node, sourceFile: SourceFile): NodeArray<Node> {
|
||||
if (node.parent) {
|
||||
switch (node.parent.kind) {
|
||||
case SyntaxKind.TypeReference:
|
||||
if ((<TypeReferenceNode>node.parent).typeArguments &&
|
||||
rangeContainsStartEnd((<TypeReferenceNode>node.parent).typeArguments, node.getStart(sourceFile), node.getEnd())) {
|
||||
return (<TypeReferenceNode>node.parent).typeArguments;
|
||||
}
|
||||
break;
|
||||
return getListIfStartEndIsInListRange((<TypeReferenceNode>node.parent).typeArguments, node.getStart(sourceFile), node.getEnd());
|
||||
case SyntaxKind.ObjectLiteralExpression:
|
||||
return (<ObjectLiteralExpression>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 ((<SignatureDeclaration>node.parent).typeParameters &&
|
||||
rangeContainsStartEnd((<SignatureDeclaration>node.parent).typeParameters, start, node.getEnd())) {
|
||||
return (<SignatureDeclaration>node.parent).typeParameters;
|
||||
}
|
||||
if (rangeContainsStartEnd((<SignatureDeclaration>node.parent).parameters, start, node.getEnd())) {
|
||||
return (<SignatureDeclaration>node.parent).parameters;
|
||||
}
|
||||
break;
|
||||
return getListIfStartEndIsInListRange((<SignatureDeclaration>node.parent).typeParameters, start, node.getEnd()) ||
|
||||
getListIfStartEndIsInListRange((<SignatureDeclaration>node.parent).parameters, start, node.getEnd());
|
||||
}
|
||||
case SyntaxKind.ClassDeclaration:
|
||||
return getListIfStartEndIsInListRange((<ClassDeclaration>node.parent).typeParameters, node.getStart(sourceFile), node.getEnd());
|
||||
case SyntaxKind.NewExpression:
|
||||
case SyntaxKind.CallExpression: {
|
||||
const start = node.getStart(sourceFile);
|
||||
if ((<CallExpression>node.parent).typeArguments &&
|
||||
rangeContainsStartEnd((<CallExpression>node.parent).typeArguments, start, node.getEnd())) {
|
||||
return (<CallExpression>node.parent).typeArguments;
|
||||
}
|
||||
if ((<CallExpression>node.parent).arguments &&
|
||||
rangeContainsStartEnd((<CallExpression>node.parent).arguments, start, node.getEnd())) {
|
||||
return (<CallExpression>node.parent).arguments;
|
||||
}
|
||||
break;
|
||||
return getListIfStartEndIsInListRange((<CallExpression>node.parent).typeArguments, start, node.getEnd()) ||
|
||||
getListIfStartEndIsInListRange((<CallExpression>node.parent).arguments, start, node.getEnd());
|
||||
}
|
||||
case SyntaxKind.VariableDeclarationList:
|
||||
return getListIfStartEndIsInListRange((<VariableDeclarationList>node.parent).declarations, node.getStart(sourceFile), node.getEnd());
|
||||
case SyntaxKind.NamedImports:
|
||||
case SyntaxKind.NamedExports:
|
||||
return getListIfStartEndIsInListRange((<NamedImportsOrExports>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;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
/// <reference path='transpile.ts' />
|
||||
/// <reference path='formatting\formatting.ts' />
|
||||
/// <reference path='formatting\smartIndenter.ts' />
|
||||
/// <reference path='textChanges.ts' />
|
||||
/// <reference path='codeFixProvider.ts' />
|
||||
/// <reference path='codefixes\fixes.ts' />
|
||||
|
||||
@@ -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);
|
||||
|
||||
475
src/services/textChanges.ts
Normal file
475
src/services/textChanges.ts
Normal file
@@ -0,0 +1,475 @@
|
||||
/* @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 (<any>n)["__pos"];
|
||||
}
|
||||
|
||||
function setPos(n: TextRange, pos: number) {
|
||||
(<any>n)["__pos"] = pos;
|
||||
}
|
||||
|
||||
function getEnd(n: TextRange) {
|
||||
return (<any>n)["__end"];
|
||||
}
|
||||
|
||||
function setEnd(n: TextRange, end: number) {
|
||||
(<any>n)["__end"] = end;
|
||||
}
|
||||
|
||||
export interface ConfigurableStart {
|
||||
useNonAdjustedStartPosition?: boolean;
|
||||
}
|
||||
export interface ConfigurableEnd {
|
||||
useNonAdjustedEndPosition?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* Set this value to true to make sure that node text of newly inserted node ends with new line
|
||||
*/
|
||||
insertTrailingNewLine?: boolean;
|
||||
/**
|
||||
* Set this value to true to make sure that node text of newly inserted node starts with new line
|
||||
*/
|
||||
insertLeadingNewLine?: boolean;
|
||||
/**
|
||||
* 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 oldNode?: Node;
|
||||
readonly node?: Node;
|
||||
readonly options?: ChangeNodeOptions;
|
||||
}
|
||||
|
||||
export function getAdjustedStartPosition(sourceFile: SourceFile, node: Node, options: ConfigurableStart, forDeleteOperation: boolean) {
|
||||
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 forDeleteOperation ? fullStart : start;
|
||||
}
|
||||
// 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 = skipTrivia(sourceFile.text, adjustedStartPosition, /*stopAfterLineBreak*/ false, /*stopAtComments*/ true);
|
||||
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;
|
||||
}
|
||||
|
||||
function isSeparator(node: Node, separator: Node): boolean {
|
||||
return node.parent && (separator.kind === SyntaxKind.CommaToken || (separator.kind === SyntaxKind.SemicolonToken && node.parent.kind === SyntaxKind.ObjectLiteralExpression));
|
||||
}
|
||||
|
||||
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, /*forDeleteOperation*/ true);
|
||||
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, /*forDeleteOperation*/ true);
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
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, {}, /*forDeleteOperation*/ true), /*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, {}, /*forDeleteOperation*/ true), /*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, /*forDeleteOperation*/ false);
|
||||
const endPosition = getAdjustedEndPosition(sourceFile, oldNode, options);
|
||||
this.changes.push({ sourceFile, options, oldNode, 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, /*forDeleteOperation*/ false);
|
||||
const endPosition = getAdjustedEndPosition(sourceFile, endNode, options);
|
||||
this.changes.push({ sourceFile, options, oldNode: startNode, 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, /*forDeleteOperation*/ false);
|
||||
this.changes.push({ sourceFile, options, oldNode: before, node: newNode, range: { pos: startPosition, end: startPosition } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public insertNodeAfter(sourceFile: SourceFile, after: Node, newNode: Node, options: InsertNodeOptions & ConfigurableEnd = {}) {
|
||||
const endPosition = getAdjustedEndPosition(sourceFile, after, options);
|
||||
this.changes.push({ sourceFile, options, oldNode: after, node: newNode, range: { pos: endPosition, end: endPosition } });
|
||||
return this;
|
||||
}
|
||||
|
||||
public getChanges(): FileTextChanges[] {
|
||||
const changesPerFile = createFileMap<Change[]>();
|
||||
// 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;
|
||||
ChangeTracker.normalize(changesInFile);
|
||||
|
||||
const fileTextChanges: FileTextChanges = { fileName: sourceFile.fileName, textChanges: [] };
|
||||
for (const c of 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.oldNode
|
||||
? formatting.SmartIndenter.getIndentation(change.range.pos, sourceFile, formatOptions, posStartsLine || change.options.insertLeadingNewLine)
|
||||
: 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
|
||||
text = posStartsLine ? text : text.replace(/^\s+/, "");
|
||||
|
||||
if (options.insertLeadingNewLine) {
|
||||
text = this.newLineCharacter + text;
|
||||
}
|
||||
if (options.insertTrailingNewLine) {
|
||||
text = text + this.newLineCharacter;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static normalize(changes: Change[]) {
|
||||
// order changes by start position
|
||||
changes.sort((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 < changes.length - 2; i++) {
|
||||
Debug.assert(changes[i].range.end <= changes[i + 1].range.pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 (<any>Proxy)());
|
||||
newNode.pos = getPos(node);
|
||||
newNode.end = getEnd(node);
|
||||
return newNode;
|
||||
|
||||
function Proxy() { }
|
||||
}
|
||||
|
||||
function assignPositionsToNodeArray(nodes: NodeArray<any>, 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) : 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) => {
|
||||
setPos(node, this.lastNonTriviaPosition);
|
||||
printCallback(hint, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@
|
||||
"shims.ts",
|
||||
"signatureHelp.ts",
|
||||
"symbolDisplay.ts",
|
||||
"textChanges.ts",
|
||||
"formatting/formatting.ts",
|
||||
"formatting/formattingContext.ts",
|
||||
"formatting/formattingRequestKind.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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user