Skip to content

Add semicolon preference to formatter options #33402

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
df151ac
Add UserPreferences for semicolons
andrewbranch Aug 29, 2019
7cca9f4
Prototype formatter semicolon removal
andrewbranch Aug 30, 2019
461e6ad
Implement semicolon insertion
andrewbranch Sep 5, 2019
0d96d40
Fix existing tests
andrewbranch Sep 5, 2019
7cae3cd
Start adding tests
andrewbranch Sep 10, 2019
e27bc96
Fix some edge cases of semicolon deletion
andrewbranch Sep 11, 2019
ae9a234
Fix semicolon removal before comments
andrewbranch Sep 11, 2019
0ac2807
Fix indentation
andrewbranch Sep 11, 2019
4027433
Test on checker
andrewbranch Sep 11, 2019
149eaf6
Replace semicolon-omitting writer with formatter preference
andrewbranch Sep 11, 2019
4555bb8
Fix writing new nodes, update protocol
andrewbranch Sep 12, 2019
ac30127
Rename option
andrewbranch Sep 12, 2019
ca8b8f0
Really fix formatting synthetic nodes
andrewbranch Sep 12, 2019
9c33590
Fix refactoring misses
andrewbranch Sep 12, 2019
d5d23a2
Merge branch 'master' into feature/semicolon-preference
andrewbranch Sep 12, 2019
5743d76
Un-update submodules gahhhh
andrewbranch Sep 12, 2019
d36155f
Update APIs
andrewbranch Sep 12, 2019
047b55f
Update for ESLint
andrewbranch Sep 13, 2019
9bd7d54
Revert accidental test change
andrewbranch Sep 13, 2019
2c1eef2
De-kludge deduplication of EOF processing
andrewbranch Sep 13, 2019
4675779
Merge branch 'master' into feature/semicolon-preference
andrewbranch Sep 24, 2019
5625cb0
Omit last element semicolon from single-line object-like types
andrewbranch Sep 25, 2019
525e32e
Revert "Omit last element semicolon from single-line object-like types"
andrewbranch Sep 25, 2019
498a320
Fix straggler test
andrewbranch Sep 25, 2019
68cf56d
Add test for leading semicolon class members
andrewbranch Sep 25, 2019
623b4b3
Rename a lot of stuff for clarity
andrewbranch Sep 26, 2019
61c3e61
Invert some boolean logic
andrewbranch Sep 26, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,21 @@ namespace ts {
return undefined;
}

/**
* Like `forEach`, but iterates in reverse order.
*/
export function forEachRight<T, U>(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
if (array) {
for (let i = array.length - 1; i >= 0; i--) {
const result = callback(array[i], i);
if (result) {
return result;
}
}
}
return undefined;
}

/** Like `forEach`, but suitable for use with numbers and strings (which may be falsy). */
export function firstDefined<T, U>(array: readonly T[] | undefined, callback: (element: T, index: number) => U | undefined): U | undefined {
if (array === undefined) {
Expand Down Expand Up @@ -2159,8 +2174,19 @@ namespace ts {
return (arg: T) => f(arg) && g(arg);
}

export function or<T>(f: (arg: T) => boolean, g: (arg: T) => boolean): (arg: T) => boolean {
return arg => f(arg) || g(arg);
export function or<T extends unknown>(...fs: ((arg: T) => boolean)[]): (arg: T) => boolean {
return arg => {
for (const f of fs) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you choose between for-of, for-increment, and forEach?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My personal tendencies:

  • for-of: lets you early return enclosing function, doesn’t give you element index
  • for-increment: generally only use when element index is important, or if I need to start somewhere other than the first element
  • forEach: gives you early termination of iteration but not return of the enclosing function, result transformation, and element index. It’s generally replaceable by for-of but is more elegant if you return something, like
const firstMatchingSignature = forEach(types, type => {
  return firstDefined(type.getCallSignatures(), somePredicateThing);
});
  • Array.prototype.forEach: no early termination, no return value 👎

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arg => forEach(fs, f => f(arg)) || false?

Copy link
Member Author

@andrewbranch andrewbranch Sep 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that would work. I think I also have a tendency to implement abstractions with building blocks that are less abstract than the function I’m implementing, if that makes sense. or and forEach are both fairly low-level atomic abstractions, so it makes sense to me that neither would depend on the other, but would rather be built with primitive loops. (I’ve never been consciously aware of this thought process before now; it’s typically just driven by a vague intuition.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess, in my mind, if or can straightforwardly be reduced to forEach it is higher-level. forEach also seems like the sort of thing that might eventually be performance-tuned and it would be nice to pick up the benefits automatically. I don't feel strongly about it though.

if (f(arg)) {
return true;
}
}
return false;
};
}

export function not<T extends unknown[]>(fn: (...args: T) => boolean): (...args: T) => boolean {
return (...args) => !fn(...args);
}

export function assertType<T>(_: T): void { }
Expand Down
20 changes: 1 addition & 19 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3380,11 +3380,7 @@ namespace ts {
};
}

export interface TrailingSemicolonDeferringWriter extends EmitTextWriter {
resetPendingTrailingSemicolon(): void;
}

export function getTrailingSemicolonDeferringWriter(writer: EmitTextWriter): TrailingSemicolonDeferringWriter {
export function getTrailingSemicolonDeferringWriter(writer: EmitTextWriter): EmitTextWriter {
let pendingTrailingSemicolon = false;

function commitPendingTrailingSemicolon() {
Expand Down Expand Up @@ -3451,20 +3447,6 @@ namespace ts {
commitPendingTrailingSemicolon();
writer.decreaseIndent();
},
resetPendingTrailingSemicolon() {
pendingTrailingSemicolon = false;
}
};
}

export function getTrailingSemicolonOmittingWriter(writer: EmitTextWriter): EmitTextWriter {
const deferringWriter = getTrailingSemicolonDeferringWriter(writer);
return {
...deferringWriter,
writeLine() {
deferringWriter.resetPendingTrailingSemicolon();
writer.writeLine();
},
};
}

Expand Down
34 changes: 12 additions & 22 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1683,7 +1683,7 @@ namespace FourSlash {
if (this.enableFormatting) {
const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeSettings);
if (edits.length) {
offset += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true);
offset += this.applyEdits(this.activeFile.fileName, edits);
}
}
}
Expand Down Expand Up @@ -1756,7 +1756,7 @@ namespace FourSlash {
if (this.enableFormatting) {
const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, offset, ch, this.formatCodeSettings);
if (edits.length) {
offset += this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true);
offset += this.applyEdits(this.activeFile.fileName, edits);
}
}
}
Expand All @@ -1775,7 +1775,7 @@ namespace FourSlash {
if (this.enableFormatting) {
const edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, offset, this.formatCodeSettings);
if (edits.length) {
this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true);
this.applyEdits(this.activeFile.fileName, edits);
}
}

Expand Down Expand Up @@ -1810,9 +1810,7 @@ namespace FourSlash {
* @returns The number of characters added to the file as a result of the edits.
* May be negative.
*/
private applyEdits(fileName: string, edits: readonly ts.TextChange[], isFormattingEdit: boolean): number {
// Get a snapshot of the content of the file so we can make sure any formatting edits didn't destroy non-whitespace characters
const oldContent = this.getFileContent(fileName);
private applyEdits(fileName: string, edits: readonly ts.TextChange[]): number {
let runningOffset = 0;

forEachTextChange(edits, edit => {
Expand All @@ -1833,14 +1831,6 @@ namespace FourSlash {
runningOffset += editDelta;
});

if (isFormattingEdit) {
const newContent = this.getFileContent(fileName);

if (this.removeWhitespace(newContent) !== this.removeWhitespace(oldContent)) {
this.raiseError("Formatting operation destroyed non-whitespace content");
}
}

return runningOffset;
}

Expand All @@ -1856,17 +1846,17 @@ namespace FourSlash {

public formatDocument() {
const edits = this.languageService.getFormattingEditsForDocument(this.activeFile.fileName, this.formatCodeSettings);
this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true);
this.applyEdits(this.activeFile.fileName, edits);
}

public formatSelection(start: number, end: number) {
const edits = this.languageService.getFormattingEditsForRange(this.activeFile.fileName, start, end, this.formatCodeSettings);
this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true);
this.applyEdits(this.activeFile.fileName, edits);
}

public formatOnType(pos: number, key: string) {
const edits = this.languageService.getFormattingEditsAfterKeystroke(this.activeFile.fileName, pos, key, this.formatCodeSettings);
this.applyEdits(this.activeFile.fileName, edits, /*isFormattingEdit*/ true);
this.applyEdits(this.activeFile.fileName, edits);
}

private editScriptAndUpdateMarkers(fileName: string, editStart: number, editEnd: number, newText: string) {
Expand Down Expand Up @@ -2414,7 +2404,7 @@ namespace FourSlash {

if (options.applyChanges) {
for (const change of action.changes) {
this.applyEdits(change.fileName, change.textChanges, /*isFormattingEdit*/ false);
this.applyEdits(change.fileName, change.textChanges);
}
this.verifyNewContentAfterChange(options, action.changes.map(c => c.fileName));
}
Expand Down Expand Up @@ -2497,7 +2487,7 @@ namespace FourSlash {

private applyChanges(changes: readonly ts.FileTextChanges[]): void {
for (const change of changes) {
this.applyEdits(change.fileName, change.textChanges, /*isFormattingEdit*/ false);
this.applyEdits(change.fileName, change.textChanges);
}
}

Expand Down Expand Up @@ -2525,7 +2515,7 @@ namespace FourSlash {
ts.Debug.assert(codeFix.changes.length === 1);
const change = ts.first(codeFix.changes);
ts.Debug.assert(change.fileName === fileName);
this.applyEdits(change.fileName, change.textChanges, /*isFormattingEdit*/ false);
this.applyEdits(change.fileName, change.textChanges);
const text = range ? this.rangeText(range) : this.getFileContent(this.activeFile.fileName);
actualTextArray.push(text);
scriptInfo.updateContent(originalContent);
Expand Down Expand Up @@ -2929,7 +2919,7 @@ namespace FourSlash {

const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactorName, actionName, ts.emptyOptions)!;
for (const edit of editInfo.edits) {
this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false);
this.applyEdits(edit.fileName, edit.textChanges);
}

let renameFilename: string | undefined;
Expand Down Expand Up @@ -3045,7 +3035,7 @@ namespace FourSlash {
const editInfo = this.languageService.getEditsForRefactor(marker.fileName, formattingOptions, marker.position, refactorNameToApply, actionName, ts.emptyOptions)!;

for (const edit of editInfo.edits) {
this.applyEdits(edit.fileName, edit.textChanges, /*isFormattingEdit*/ false);
this.applyEdits(edit.fileName, edit.textChanges);
}
const actualContent = this.getFileContent(marker.fileName);

Expand Down
7 changes: 7 additions & 0 deletions src/server/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2956,6 +2956,12 @@ namespace ts.server.protocol {
Smart = "Smart",
}

export enum SemicolonPreference {
Ignore = "ignore",
Insert = "insert",
Remove = "remove",
}

export interface EditorSettings {
baseIndentSize?: number;
indentSize?: number;
Expand All @@ -2982,6 +2988,7 @@ namespace ts.server.protocol {
placeOpenBraceOnNewLineForFunctions?: boolean;
placeOpenBraceOnNewLineForControlBlocks?: boolean;
insertSpaceBeforeTypeAnnotation?: boolean;
semicolons?: SemicolonPreference;
}

export interface UserPreferences {
Expand Down
2 changes: 1 addition & 1 deletion src/services/codefixes/importFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ namespace ts.codefix {
// We sort the best codefixes first, so taking `first` is best for completions.
const moduleSpecifier = first(getNewImportInfos(program, sourceFile, position, exportInfos, host, preferences)).moduleSpecifier;
const fix = first(getFixForImport(exportInfos, symbolName, position, program, sourceFile, host, preferences));
return { moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) };
return { moduleSpecifier, codeAction: codeFixActionToCodeAction(codeActionForFix({ host, formatContext, preferences }, sourceFile, symbolName, fix, getQuotePreference(sourceFile, preferences))) };
}

function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction {
Expand Down
Loading