Skip to content

Commit

Permalink
feat: #231 - Node.formatText() - Format individual nodes.
Browse files Browse the repository at this point in the history
  • Loading branch information
dsherret committed Jan 21, 2018
1 parent 9db12f5 commit 34f61ea
Show file tree
Hide file tree
Showing 11 changed files with 151 additions and 52 deletions.
1 change: 1 addition & 0 deletions docs/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ <h1 onclick="document.location.href = '{{ "/" | prepend: site.baseurl }}'" class
<li class="{% if page.path == 'manipulation/settings.md' %}active{% endif %}"><a href="{{ "/manipulation/settings" | prepend: site.baseurl }}">Settings</a></li>
<li class="{% if page.path == 'manipulation/renaming.md' %}active{% endif %}"><a href="{{ "/manipulation/renaming" | prepend: site.baseurl }}">Renaming</a></li>
<li class="{% if page.path == 'manipulation/removing.md' %}active{% endif %}"><a href="{{ "/manipulation/removing" | prepend: site.baseurl }}">Removing</a></li>
<li class="{% if page.path == 'manipulation/formatting.md' %}active{% endif %}"><a href="{{ "/manipulation/formatting" | prepend: site.baseurl }}">Formatting</a></li>
<li class="{% if page.path == 'manipulation/order.md' %}active{% endif %}"><a href="{{ "/manipulation/order" | prepend: site.baseurl }}">Order</a></li>
<li class="{% if page.path == 'manipulation/code-writer.md' %}active{% endif %}"><a href="{{ "/manipulation/code-writer" | prepend: site.baseurl }}">Code Writer</a></li>
<li class="{% if page.path == 'manipulation/performance.md' %}active{% endif %}"><a href="{{ "/manipulation/performance" | prepend: site.baseurl }}">Performance</a></li>
Expand Down
32 changes: 0 additions & 32 deletions docs/details/source-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,38 +155,6 @@ sourceFile.unindent(10, -1); // indent line containing position 10 (specify nega

This will indent and unindent based on your [manipulation settings](../manipulation/settings).

### Formatting Text

Sometimes you might encounter code that looks terrible. For example:

```typescript
// BadlyFormattedFile.ts
var myVariable : string | number;
function myFunction(param : MyClass){
return "";
}
```

Automatically format the text of this file by calling format text on it:

```typescript
sourceFile.formatText();
// or provide optional formatting settings
sourceFile.formatText({
placeOpenBraceOnNewLineForFunctions: true
});
```

This will run the source file's text through the TypeScript compiler's formatting API, which will change the source file to contain the following text:

```typescript
// BadlyFormattedFile.ts (not anymore!)
var myVariable: string | number;
function myFunction(param: MyClass) {
return "";
}
```

## Getting Exported Declarations

The exported declarations of a file can be retrieved via `.getExportedDeclarations()`.
Expand Down
73 changes: 73 additions & 0 deletions docs/manipulation/formatting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
title: Formatting
---

## Formatting

Sometimes you might encounter code that looks terrible. For example:

```typescript
// BadlyFormattedFile.ts
var myVariable : string | number;
function myFunction(param : MyClass){
return "";
}
```

Automatically format the text of this file by calling format text on it:

```typescript
sourceFile.formatText();
// or provide optional formatting settings
sourceFile.formatText({
placeOpenBraceOnNewLineForFunctions: true
});
```

This will run the source file's text through the TypeScript compiler's formatting API, which will change the source file to contain the following text:

```typescript
// BadlyFormattedFile.ts (not anymore!)
var myVariable: string | number;
function myFunction(param: MyClass) {
return "";
}
```

### Individual Nodes

Individual nodes can also be formatted. For example, say you have a file that looks like this:

```ts
// file.ts
export class MyClass {
prop : string ;

myMethod( example: string ) {
console.log( example );
}
}
```

You can select down to the specific node you want to format:

```ts
ast.getSourceFileOrThrow("file.ts")
.getClassOrThrow("MyClass")
.getInstanceMethodOrThrow("myMethod")
.getStatements()[0]
.formatText();
```

Which would selectively only format the first statement in the method:

```ts
// file.ts
export class MyClass {
prop : string ;

myMethod( example: string ) {
console.log(example);
}
}
```
2 changes: 1 addition & 1 deletion src/compiler/class/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from "./base";
export * from "./ClassDeclaration";
export * from "./ConstructorDeclaration";
export * from "./GetAccessorDeclaration";
export * from "./MethodDeclaration";
export * from "./PropertyDeclaration";
export * from "./SetAccessorDeclaration";
export * from "./base";
20 changes: 19 additions & 1 deletion src/compiler/common/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import * as errors from "./../../errors";
import {GlobalContainer} from "./../../GlobalContainer";
import {IndentationText} from "./../../ManipulationSettings";
import {StructureToText} from "./../../structureToTexts";
import {insertIntoParent, getNextNonWhitespacePos, getPreviousMatchingPos} from "./../../manipulation";
import {insertIntoParent, getNextNonWhitespacePos, getPreviousMatchingPos, replaceSourceFileTextForFormatting,
getTextFromFormattingEdits} from "./../../manipulation";
import {TypeGuards, getTextFromStringOrWriter, ArrayUtils, isStringKind, printNode, PrintNodeOptions} from "./../../utils";
import {SourceFile} from "./../file";
import * as base from "./../base";
import {ConstructorDeclaration, MethodDeclaration} from "./../class";
import {FunctionDeclaration} from "./../function";
import {FormatCodeSettings} from "./../tools";
import {TypeAliasDeclaration} from "./../type";
import {InterfaceDeclaration} from "./../interface";
import {QuoteType} from "./../literal/QuoteType";
Expand Down Expand Up @@ -809,6 +811,22 @@ export class Node<NodeType extends ts.Node = ts.Node> {
}
}

/**
* Formats the node's text using the internal TypeScript formatting API.
* @param settings - Format code settings.
*/
formatText(settings: FormatCodeSettings = {}) {
const formattingEdits = this.global.languageService.getFormattingEditsForRange(
this.sourceFile.getFilePath(),
[this.getStart(true), this.getEnd()],
settings);

replaceSourceFileTextForFormatting({
sourceFile: this.sourceFile,
newText: getTextFromFormattingEdits(this.sourceFile, formattingEdits)
});
}

/**
* Gets the children based on a kind.
* @param kind - Syntax kind.
Expand Down
1 change: 1 addition & 0 deletions src/compiler/file/SourceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ export class SourceFile extends SourceFileBase<ts.SourceFile> {

/**
* Formats the source file text using the internal TypeScript formatting API.
* @param settings - Format code settings.
*/
formatText(settings: FormatCodeSettings = {}) {
replaceSourceFileTextForFormatting({
Expand Down
34 changes: 17 additions & 17 deletions src/compiler/tools/LanguageService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as ts from "typescript";
import {GlobalContainer} from "./../../GlobalContainer";
import {replaceSourceFileTextForRename} from "./../../manipulation";
import {replaceSourceFileTextForRename, getTextFromFormattingEdits} from "./../../manipulation";
import * as errors from "./../../errors";
import {DefaultFileSystemHost} from "./../../fileSystem";
import {KeyValueCache, FileUtils, StringUtils, fillDefaultFormatCodeSettings} from "./../../utils";
Expand Down Expand Up @@ -205,6 +205,18 @@ export class LanguageService {
return renameLocations.map(l => new RenameLocation(this.global, l));
}

/**
* Gets the formatting edits for a range.
* @param filePath - File path.
* @param range - Position range.
* @param settings - Settings.
*/
getFormattingEditsForRange(filePath: string, range: [number, number], settings: FormatCodeSettings) {
fillDefaultFormatCodeSettings(settings, this.global.manipulationSettings);

return (this.compilerObject.getFormattingEditsForRange(filePath, range[0], range[1], settings) || []).map(e => new TextChange(e));
}

/**
* Gets the formatting edits for a document.
* @param filePath - File path of the source file.
Expand All @@ -213,8 +225,7 @@ export class LanguageService {
getFormattingEditsForDocument(filePath: string, settings: FormatCodeSettings) {
fillDefaultFormatCodeSettings(settings, this.global.manipulationSettings);

const formattingEdits = this.compilerObject.getFormattingEditsForDocument(filePath, settings) || [];
return formattingEdits.map(e => new TextChange(e));
return (this.compilerObject.getFormattingEditsForDocument(filePath, settings) || []).map(e => new TextChange(e));
}

/**
Expand All @@ -227,25 +238,14 @@ export class LanguageService {
if (sourceFile == null)
throw new errors.FileNotFoundError(filePath);

let newText = getCompilerFormattedText(this);
const formattingEdits = this.getFormattingEditsForDocument(filePath, settings);
let newText = getTextFromFormattingEdits(sourceFile, formattingEdits);
const newLineChar = settings.newLineCharacter!; // this is filled in getFormattingEditsForDocument

if (settings.ensureNewLineAtEndOfFile && !StringUtils.endsWith(newText, newLineChar))
newText += newLineChar;

return newText.replace(/\r?\n/g, newLineChar);

function getCompilerFormattedText(languageService: LanguageService) {
const formattingEditsInReverseOrder = languageService.getFormattingEditsForDocument(filePath, settings)
.sort((a, b) => b.getSpan().getStart() - a.getSpan().getStart());
let fullText = sourceFile!.getFullText();

for (const textChange of formattingEditsInReverseOrder) {
const span = textChange.getSpan();
fullText = fullText.slice(0, span.getStart()) + textChange.getNewText() + fullText.slice(span.getEnd());
}

return fullText;
}
}

/**
Expand Down
14 changes: 14 additions & 0 deletions src/manipulation/formatting/getTextFromFormattingEdits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {TextChange, SourceFile} from "./../../compiler";

export function getTextFromFormattingEdits(sourceFile: SourceFile, formattingEdits: TextChange[]) {
// reverse the order
formattingEdits = [...formattingEdits].sort((a, b) => b.getSpan().getStart() - a.getSpan().getStart());
let text = sourceFile.getFullText();

for (const textChange of formattingEdits) {
const span = textChange.getSpan();
text = text.slice(0, span.getStart()) + textChange.getNewText() + text.slice(span.getEnd());
}

return text;
}
1 change: 1 addition & 0 deletions src/manipulation/formatting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./getFormattingKindText";
export * from "./getGeneralFormatting";
export * from "./getInterfaceMemberFormatting";
export * from "./getStatementedNodeChildFormatting";
export * from "./getTextFromFormattingEdits";
1 change: 1 addition & 0 deletions src/manipulation/textManipulators/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./ChangingChildOrderTextManipulator";
export * from "./FullReplacementTextManipulator";
export * from "./getSpacingBetweenNodes";
export * from "./getTextForError";
export * from "./InsertIntoBracesTextManipulator";
export * from "./InsertionTextManipulator";
export * from "./RemoveChildrenTextManipulator";
Expand Down
24 changes: 23 additions & 1 deletion src/tests/compiler/common/nodeTests.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as ts from "typescript";
import CodeBlockWriter from "code-block-writer";
import {expect} from "chai";
import {Node, EnumDeclaration, ClassDeclaration, FunctionDeclaration, InterfaceDeclaration, PropertySignature, PropertyAccessExpression} from "./../../../compiler";
import {Node, EnumDeclaration, ClassDeclaration, FunctionDeclaration, InterfaceDeclaration, PropertySignature, PropertyAccessExpression,
SourceFile, FormatCodeSettings} from "./../../../compiler";
import * as errors from "./../../../errors";
import {TypeGuards} from "./../../../utils";
import {NewLineKind} from "./../../../ManipulationSettings";
Expand Down Expand Up @@ -716,4 +717,25 @@ describe(nameof(Node), () => {
expect(firstChild.print({ newLineKind: NewLineKind.CarriageReturnLineFeed })).to.equal(nodeText.replace(/\n/g, "\r\n"));
});
});

describe(nameof<Node>(n => n.formatText), () => {
function doTest(text: string, getNode: (sourceFile: SourceFile) => Node, expectedText: string, settings: FormatCodeSettings = {}) {
const {sourceFile} = getInfoFromText(text);
getNode(sourceFile).formatText(settings);
expect(sourceFile.getFullText()).to.equal(expectedText);
}

it("should format only the provided node", () => {
doTest("class Test{\n prop:string;\n }\nclass Test2{\n prop:string;\n }\n", f => f.getClassOrThrow("Test2"),
"class Test{\n prop:string;\n }\nclass Test2 {\n prop: string;\n}\n");
});

it("should format only the provided node with the specified settings", () => {
doTest("class Test{\n\tprop:string;\n}\n /** Testing */\nclass Test2{\n\t prop:string;\n\t}\n", f => f.getClassOrThrow("Test2"),
"class Test{\n\tprop:string;\n}\n/** Testing */\nclass Test2 {\n prop: string;\n}\n", {
convertTabsToSpaces: true,
indentSize: 2
});
});
});
});

0 comments on commit 34f61ea

Please sign in to comment.