diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index e5786f31335..be200c6d0ce 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -310,6 +310,7 @@ namespace FourSlash { } this.formatCodeOptions = { + BaseIndentSize: 0, IndentSize: 4, TabSize: 4, NewLineCharacter: Harness.IO.newLine(), @@ -1659,24 +1660,25 @@ namespace FourSlash { } } - private getIndentation(fileName: string, position: number, indentStyle: ts.IndentStyle): number { + private getIndentation(fileName: string, position: number, indentStyle: ts.IndentStyle, baseIndentSize: number): number { const formatOptions = ts.clone(this.formatCodeOptions); formatOptions.IndentStyle = indentStyle; + formatOptions.BaseIndentSize = baseIndentSize; return this.languageService.getIndentationAtPosition(fileName, position, formatOptions); } - public verifyIndentationAtCurrentPosition(numberOfSpaces: number, indentStyle: ts.IndentStyle = ts.IndentStyle.Smart) { - const actual = this.getIndentation(this.activeFile.fileName, this.currentCaretPosition, indentStyle); + public verifyIndentationAtCurrentPosition(numberOfSpaces: number, indentStyle: ts.IndentStyle = ts.IndentStyle.Smart, baseIndentSize = 0) { + const actual = this.getIndentation(this.activeFile.fileName, this.currentCaretPosition, indentStyle, baseIndentSize); const lineCol = this.getLineColStringAtPosition(this.currentCaretPosition); if (actual !== numberOfSpaces) { this.raiseError(`verifyIndentationAtCurrentPosition failed at ${lineCol} - expected: ${numberOfSpaces}, actual: ${actual}`); } } - public verifyIndentationAtPosition(fileName: string, position: number, numberOfSpaces: number, indentStyle: ts.IndentStyle = ts.IndentStyle.Smart) { - const actual = this.getIndentation(fileName, position, indentStyle); + public verifyIndentationAtPosition(fileName: string, position: number, numberOfSpaces: number, indentStyle: ts.IndentStyle = ts.IndentStyle.Smart, baseIndentSize = 0) { + const actual = this.getIndentation(fileName, position, indentStyle, baseIndentSize); const lineCol = this.getLineColStringAtPosition(position); if (actual !== numberOfSpaces) { this.raiseError(`verifyIndentationAtPosition failed at ${lineCol} - expected: ${numberOfSpaces}, actual: ${actual}`); @@ -2938,8 +2940,8 @@ namespace FourSlashInterface { this.state.verifyIndentationAtCurrentPosition(numberOfSpaces); } - public indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle = ts.IndentStyle.Smart) { - this.state.verifyIndentationAtPosition(fileName, position, numberOfSpaces, indentStyle); + public indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle = ts.IndentStyle.Smart, baseIndentSize = 0) { + this.state.verifyIndentationAtPosition(fileName, position, numberOfSpaces, indentStyle, baseIndentSize); } public textAtCaretIs(text: string) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 9c0662e5534..e48d6192017 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1566,6 +1566,7 @@ namespace ts.server { static getDefaultFormatCodeOptions(host: ServerHost): ts.FormatCodeOptions { return ts.clone({ + BaseIndentSize: 0, IndentSize: 4, TabSize: 4, NewLineCharacter: host.newLine || "\n", diff --git a/src/server/protocol.d.ts b/src/server/protocol.d.ts index 62162b3237c..6442848abbe 100644 --- a/src/server/protocol.d.ts +++ b/src/server/protocol.d.ts @@ -438,6 +438,9 @@ declare namespace ts.server.protocol { /** Number of spaces to indent during formatting. Default value is 4. */ indentSize?: number; + /** Number of additional spaces to indent during formatting to preserve base indentation (ex. script block indentation). Default value is 0. */ + baseIndentSize?: number; + /** The new line character to be used. Default value is the OS line delimiter. */ newLineCharacter?: string; @@ -478,7 +481,7 @@ declare namespace ts.server.protocol { placeOpenBraceOnNewLineForControlBlocks?: boolean; /** Index operator */ - [key: string]: string | number | boolean; + [key: string]: string | number | boolean | undefined; } /** diff --git a/src/server/session.ts b/src/server/session.ts index 7a779383352..7e1ca81e2af 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -703,6 +703,7 @@ namespace ts.server { if (lineText.search("\\S") < 0) { // TODO: get these options from host const editorOptions: ts.EditorOptions = { + BaseIndentSize: formatOptions.BaseIndentSize, IndentSize: formatOptions.IndentSize, TabSize: formatOptions.TabSize, NewLineCharacter: formatOptions.NewLineCharacter, diff --git a/src/services/formatting/formatting.ts b/src/services/formatting/formatting.ts index ef8fddcfb3a..5dae6393b99 100644 --- a/src/services/formatting/formatting.ts +++ b/src/services/formatting/formatting.ts @@ -394,7 +394,10 @@ namespace ts.formatting { const startLinePosition = getLineStartPositionForPosition(startPos, sourceFile); const column = SmartIndenter.findFirstNonWhitespaceColumn(startLinePosition, startPos, sourceFile, options); if (startLine !== parentStartLine || startPos === column) { - return column; + // Use the base indent size if it is greater than + // the indentation of the inherited predecessor. + const baseIndentSize = SmartIndenter.getBaseIndentation(options); + return baseIndentSize > column ? baseIndentSize : column; } } diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts index 23a1d937869..d25182c73df 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -10,7 +10,7 @@ namespace ts.formatting { export function getIndentation(position: number, sourceFile: SourceFile, options: EditorOptions): number { if (position > sourceFile.text.length) { - return 0; // past EOF + return getBaseIndentation(options); // past EOF } // no indentation when the indent style is set to none, @@ -21,7 +21,7 @@ namespace ts.formatting { const precedingToken = findPrecedingToken(position, sourceFile); if (!precedingToken) { - return 0; + return getBaseIndentation(options); } // no indentation in string \regex\template literals @@ -96,13 +96,17 @@ namespace ts.formatting { } if (!current) { - // no parent was found - return 0 to be indented on the level of SourceFile - return 0; + // no parent was found - return the base indentation of the SourceFile + return getBaseIndentation(options); } return getIndentationForNodeWorker(current, currentStart, /*ignoreActualIndentationRange*/ undefined, indentationDelta, sourceFile, options); } + export function getBaseIndentation(options: EditorOptions) { + return options.BaseIndentSize || 0; + } + export function getIndentationForNode(n: Node, ignoreActualIndentationRange: TextRange, sourceFile: SourceFile, options: FormatCodeOptions): number { const start = sourceFile.getLineAndCharacterOfPosition(n.getStart(sourceFile)); return getIndentationForNodeWorker(n, start, ignoreActualIndentationRange, /*indentationDelta*/ 0, sourceFile, options); @@ -162,7 +166,7 @@ namespace ts.formatting { parent = current.parent; } - return indentationDelta; + return indentationDelta + getBaseIndentation(options); } diff --git a/src/services/services.ts b/src/services/services.ts index 492be6f1719..d9425c9df50 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1244,6 +1244,7 @@ namespace ts { } export interface EditorOptions { + BaseIndentSize?: number; IndentSize: number; TabSize: number; NewLineCharacter: string; @@ -1268,7 +1269,7 @@ namespace ts { InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: boolean; PlaceOpenBraceOnNewLineForFunctions: boolean; PlaceOpenBraceOnNewLineForControlBlocks: boolean; - [s: string]: boolean | number | string; + [s: string]: boolean | number | string | undefined; } export interface DefinitionInfo { diff --git a/tests/cases/fourslash/formatWithBaseIndent.ts b/tests/cases/fourslash/formatWithBaseIndent.ts new file mode 100644 index 00000000000..660131996c9 --- /dev/null +++ b/tests/cases/fourslash/formatWithBaseIndent.ts @@ -0,0 +1,264 @@ +/// + +//// +/////*1*/ module classes { +/////*2*/ class Bar { +//// +/////*3*/ constructor() { +/////*4*/ } +//// +/////*5*/private foo: string = ""; +//// +/////*6*/ private f() { +/////*7*/ var a: any[] = [[1, 2], [3, 4], 5]; +/////*8*/ return ((1 + 1)); +/////*9*/ } +//// +/////*10*/ private f2() { +/////*11*/ if (true) { } { }; +/////*12*/ } +/////*13*/ } +/////*14*/ } +//// +//// +/////*15*/ module interfaces { +/////*16*/ interface Foo { +//// +/////*17*/ x: number; +//// +/////*18*/ foo(): number; +/////*19*/ } +/////*20*/ } +//// +//// +/////*21*/ module nestedModules { +/////*22*/ module Foo2 { +/////*23*/ function f() { +/////*24*/ } +/////*25*/ var x: number; +/////*26*/} +/////*27*/ } +//// +//// +/////*28*/ module Enums { +/////*29*/ enum Foo3 { +/////*30*/ val1 , +/////*31*/ val2, +/////*32*/ } +/////*33*/ } +//// +//// +/////*34*/ function controlStatements() { +/////*35*/ for (var i = 0; i < 10; i++) { +/////*36*/ } +//// +/////*37*/ for (var e in foo.bar) { +/////*38*/ } +//// +/////*39*/with (foo.bar) { +/////*40*/ } +//// +/////*41*/ while (false) { +/////*42*/ } +//// +/////*43*/ do { +/////*44*/ } while (false); +//// +/////*45*/ switch (foo.bar) { +/////*46*/ } +//// +/////*47*/ switch (foo.bar) { +/////*48*/ case 1: +/////*49*/ break; +/////*50*/ default: +/////*51*/ break; +/////*52*/ } +/////*53*/ } +//// +//// +/////*54*/ function tryCatch() { +/////*55*/try { +/////*56*/} +/////*57*/catch (err) { +/////*58*/ } +/////*59*/ } +//// +//// +/////*60*/ function tryFinally() { +/////*61*/ try { +/////*62*/ } +/////*63*/ finally { +/////*64*/ } +/////*65*/ } +//// +//// +/////*66*/ function tryCatchFinally() { +/////*67*/ try { +/////*68*/ } +/////*69*/ catch (err) { +/////*70*/ } +/////*71*/ finally { +/////*72*/ } +/////*73*/ } +//// +//// +/////*74*/ class indentBeforeCurly +/////*75*/ { +/////*76*/ } +//// +//// +/////*77*/ function argumentsListIndentation(bar, +/////*78*/ blah, +/////*79*/ ); +//// +//// +/////*80*/ function blockIndentAfterIndentedParameter1(bar, +/////*81*/ blah) { +/////*82*/ } +//// +//// +/////*83*/ function blockIndentAfterIndentedParameter2(bar, +/////*84*/ blah) { +/////*85*/ if (foo) { +/////*86*/ } +/////*87*/} +//// +/////*88*/ var templateLiterals = `abcdefghi +/////*89*/jklmnop +/////*90*/qrstuvwxyz`; + +var originalOptions = format.copyFormatOptions(); +var copy = format.copyFormatOptions(); +copy.BaseIndentSize = 10; +copy.IndentSize = 4; + +format.setFormatOptions(copy); +format.document(); + +verify.currentFileContentIs(` + module classes { + class Bar { + + constructor() { + } + + private foo: string = ""; + + private f() { + var a: any[] = [[1, 2], [3, 4], 5]; + return ((1 + 1)); + } + + private f2() { + if (true) { } { }; + } + } + } + + + module interfaces { + interface Foo { + + x: number; + + foo(): number; + } + } + + + module nestedModules { + module Foo2 { + function f() { + } + var x: number; + } + } + + + module Enums { + enum Foo3 { + val1, + val2, + } + } + + + function controlStatements() { + for (var i = 0; i < 10; i++) { + } + + for (var e in foo.bar) { + } + + with (foo.bar) { + } + + while (false) { + } + + do { + } while (false); + + switch (foo.bar) { + } + + switch (foo.bar) { + case 1: + break; + default: + break; + } + } + + + function tryCatch() { + try { + } + catch (err) { + } + } + + + function tryFinally() { + try { + } + finally { + } + } + + + function tryCatchFinally() { + try { + } + catch (err) { + } + finally { + } + } + + + class indentBeforeCurly { + } + + + function argumentsListIndentation(bar, + blah, + ); + + + function blockIndentAfterIndentedParameter1(bar, + blah) { + } + + + function blockIndentAfterIndentedParameter2(bar, + blah) { + if (foo) { + } + } + + var templateLiterals = \`abcdefghi +jklmnop +qrstuvwxyz\`;`); + +format.setFormatOptions(originalOptions); \ No newline at end of file diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 9cb816a13bd..3a1d2e9839d 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -68,6 +68,7 @@ declare namespace FourSlashInterface { data?: any; } interface EditorOptions { + BaseIndentSize?: number, IndentSize: number; TabSize: number; NewLineCharacter: string; @@ -84,7 +85,7 @@ declare namespace FourSlashInterface { InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: boolean; PlaceOpenBraceOnNewLineForFunctions: boolean; PlaceOpenBraceOnNewLineForControlBlocks: boolean; - [s: string]: boolean | number | string; + [s: string]: boolean | number | string | undefined; } interface Range { fileName: string; @@ -141,7 +142,7 @@ declare namespace FourSlashInterface { assertHasRanges(ranges: Range[]): void; caretAtMarker(markerName?: string): void; indentationIs(numberOfSpaces: number): void; - indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle?: ts.IndentStyle): void; + indentationAtPositionIs(fileName: string, position: number, numberOfSpaces: number, indentStyle?: ts.IndentStyle, baseIndentSize?: number): void; textAtCaretIs(text: string): void; /** * Compiles the current file and evaluates 'expr' in a context containing diff --git a/tests/cases/fourslash/indentationWithBaseIndent.ts b/tests/cases/fourslash/indentationWithBaseIndent.ts new file mode 100644 index 00000000000..53ee01c9a3c --- /dev/null +++ b/tests/cases/fourslash/indentationWithBaseIndent.ts @@ -0,0 +1,211 @@ +/// + +//// +////{| "indent": 10 , "baseIndentSize": 10 |} +//// module classes { +////{| "indent": 14 , "baseIndentSize": 10 |} +//// class Bar { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// +//// constructor() { +////{| "indent": 22, "baseIndentSize": 10 |} +//// } +//// +//// private foo: string = ""; +////{| "indent": 18, "baseIndentSize": 10 |} +//// +//// private f() { +//// var a: any[] = [[1, 2], [3, 4], 5]; +////{| "indent": 22, "baseIndentSize": 10 |} +//// return ((1 + 1)); +//// } +//// +////{| "indent": 18, "baseIndentSize": 10 |} +//// private f2() { +//// if (true) { } { }; +//// } +//// } +//// } +//// +//// +//// module interfaces { +////{| "indent": 14 , "baseIndentSize": 10 |} +//// interface Foo { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// +//// x: number; +////{| "indent": 18 , "baseIndentSize": 10 |} +//// +//// foo(): number; +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +//// } +//// +//// +//// module nestedModules { +//// module Foo2 { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// function f() { +//// } +////{| "indent": 18 , "baseIndentSize": 10 |} +//// var x: number; +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +//// } +//// +//// +//// module Enums { +//// enum Foo3 { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// val1, +////{| "indent": 18 , "baseIndentSize": 10 |} +//// val2, +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +////{| "indent": 14 , "baseIndentSize": 10 |} +//// } +//// +//// +//// function controlStatements() { +//// for (var i = 0; i < 10; i++) { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +//// +//// for (var e in foo.bar) { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +//// +//// with (foo.bar) { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +//// +//// while (false) { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +//// +//// do { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } while (false); +//// +//// switch (foo.bar) { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +//// +//// switch (foo.bar) { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// case 1: +////{| "indent": 22 , "baseIndentSize": 10 |} +//// break; +//// default: +////{| "indent": 22 , "baseIndentSize": 10 |} +//// break; +//// } +//// } +//// +//// +//// function tryCatch() { +////{| "indent": 14 , "baseIndentSize": 10 |} +//// try { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +////{| "indent": 14 , "baseIndentSize": 10 |} +//// catch (err) { +////{| "indent": 18, "baseIndentSize": 10 |} +//// } +////{| "indent": 14, "baseIndentSize": 10 |} +//// } +//// +//// +//// function tryFinally() { +////{| "indent": 14 , "baseIndentSize": 10 |} +//// try { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +////{| "indent": 14 , "baseIndentSize": 10 |} +//// finally { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +////{| "indent": 14 , "baseIndentSize": 10 |} +//// } +//// +//// +//// function tryCatchFinally() { +////{| "indent": 14 , "baseIndentSize": 10 |} +//// try { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +////{| "indent": 14 , "baseIndentSize": 10 |} +//// catch (err) { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +////{| "indent": 14 , "baseIndentSize": 10 |} +//// finally { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +////{| "indent": 14 , "baseIndentSize": 10 |} +//// } +//// +//// +//// class indentBeforeCurly +////{| "indent": 10 , "baseIndentSize": 10 |} +////{| "indent": 10 , "baseIndentSize": 10 |} +//// { +////{| "indent": 14 , "baseIndentSize": 10 |} +//// } +////{| "indent": 10 , "baseIndentSize": 10 |} +//// +//// +//// function argumentsListIndentation(bar, +//// blah, +//// {| "indent": 14 , "baseIndentSize": 10 |} +//// ); +//// +//// +//// function blockIndentAfterIndentedParameter1(bar, +//// blah) { +////{| "indent": 14 , "baseIndentSize": 10 |} +//// } +//// +//// +//// function blockIndentAfterIndentedParameter2(bar, +//// blah) { +//// if (foo) { +////{| "indent": 18 , "baseIndentSize": 10 |} +//// } +////} +////{| "indent": 10 , "baseIndentSize": 10 |} +//// +//// var templateLiterals = `abcdefghi +////{| "indent": 0 , "baseIndentSize": 10 |} +////jklmnop +////{| "indent": 0 , "baseIndentSize": 10 |} +////qrstuvwxyz`; +////{| "indent": 10 , "baseIndentSize": 10 |} +//// +//// +//// module changeBaseIndentSizeInSameFile { +////{| "indent": 21 , "baseIndentSize": 17 |} +//// interface Foo { +////{| "indent": 25 , "baseIndentSize": 17 |} +//// +//// x: number; +////{| "indent": 25 , "baseIndentSize": 10 |} +//// +//// foo(): number; +////{| "indent": 25 , "baseIndentSize": 10 |} +//// } +////{| "indent": 21 , "baseIndentSize": 10 |} +//// } +////{| "indent": 17 , "baseIndentSize": 17 |} +//// +//// +////// Note: Do not add more tests at the end of this file, as +////// the purpose of this test is to verify smart indent +////// works for unterminated function arguments at the end of a file. +//// function unterminatedListIndentation(a, +////{| "indent": 14 , "baseIndentSize": 10 |} + +debugger; +test.markers().forEach(marker => { + verify.indentationAtPositionIs(marker.fileName, marker.position, marker.data.indent, ts.IndentStyle.Smart, marker.data.baseIndentSize); +});