Skip to content

Commit ddbe031

Browse files
committed
use actual indentation if possible
1 parent d119de8 commit ddbe031

File tree

2 files changed

+161
-63
lines changed

2 files changed

+161
-63
lines changed

src/services/formatting/smartIndenter.ts

Lines changed: 144 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
module ts.formatting {
44
export module SmartIndenter {
5+
6+
interface LineAndCharacter {
7+
line: number;
8+
character: number;
9+
}
10+
511
export function getIndentation(position: number, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number {
612
if (position > sourceFile.text.length) {
713
return 0; // past EOF
@@ -22,56 +28,38 @@ module ts.formatting {
2228
var lineAtPosition = sourceFile.getLineAndCharacterFromPosition(position).line;
2329

2430
if (precedingToken.kind === SyntaxKind.CommaToken && precedingToken.parent.kind !== SyntaxKind.BinaryExpression) {
25-
2631
// previous token is comma that separates items in list - find the previous item and try to derive indentation from it
27-
var precedingListItem = findPrecedingListItem(precedingToken);
28-
var precedingListItemStartLineAndChar = sourceFile.getLineAndCharacterFromPosition(precedingListItem.getStart(sourceFile));
29-
var listStartLine = getStartLineForNode(precedingListItem.parent, sourceFile);
30-
31-
if (precedingListItemStartLineAndChar.line !== listStartLine) {
32-
return findFirstNonWhitespaceCharacterInLine(precedingListItemStartLineAndChar.line, precedingListItemStartLineAndChar.character, sourceFile);
33-
// previous list item starts on the different line with list, find first non-whitespace character in this line and use its position as indentation
34-
var lineStartPosition = sourceFile.getPositionFromLineAndCharacter(precedingListItemStartLineAndChar.line, 1);
35-
for (var i = 0; i < precedingListItemStartLineAndChar.character; ++i) {
36-
if (!isWhiteSpace(sourceFile.text.charCodeAt(lineStartPosition + i))) {
37-
return i;
38-
}
39-
}
40-
41-
// seems that this is the first non-whitespace character on the line - return it
42-
return precedingListItemStartLineAndChar.character;
32+
var actualIndentation = getActualIndentationForListItemBeforeComma(precedingToken, sourceFile, options);
33+
if (actualIndentation !== -1) {
34+
return actualIndentation;
4335
}
4436
}
4537

4638
// try to find the node that will include 'position' starting from 'precedingToken'
4739
// if such node is found - compute initial indentation for 'position' inside this node
4840
var previous: Node;
4941
var current = precedingToken;
50-
var currentStartLine: number;
42+
var currentStart: LineAndCharacter;
5143
var indentation: number;
5244

5345
while (current) {
5446
if (isPositionBelongToNode(current, position, sourceFile)) {
55-
currentStartLine = getStartLineForNode(current, sourceFile);
47+
currentStart = getStartLineAndCharacterForNode(current, sourceFile);
5648

5749
if (discardInitialIndentationIfNextTokenIsOpenOrCloseBrace(precedingToken, current, lineAtPosition, sourceFile)) {
5850
indentation = 0;
5951
}
6052
else {
61-
indentation = isNodeContentIndented(current, previous) && lineAtPosition !== currentStartLine ? options.indentSpaces : 0;
53+
indentation = isNodeContentIndented(current, previous) && lineAtPosition !== currentStart.line ? options.indentSpaces : 0;
6254
}
6355

6456
break;
6557
}
66-
var customIndentation = getCustomIndentationForListItem(current, sourceFile);
67-
if (customIndentation !== -1) {
68-
return customIndentation;
69-
}
7058

7159
// check if current node is a list item - if yes, take indentation from it
72-
var customIndentation = getCustomIndentationForListItem(current, sourceFile);
73-
if (customIndentation !== -1) {
74-
return customIndentation;
60+
var actualIndentation = getActualIndentationForListItem(current, sourceFile, options);
61+
if (actualIndentation !== -1) {
62+
return actualIndentation;
7563
}
7664

7765
previous = current;
@@ -85,37 +73,121 @@ module ts.formatting {
8573

8674

8775
var parent: Node = current.parent;
88-
var parentStartLine: number;
76+
var parentStart: LineAndCharacter;
8977

9078
// walk upwards and collect indentations for pairs of parent-child nodes
9179
// indentation is not added if parent and child nodes start on the same line or if parent is IfStatement and child starts on the same line with 'else clause'
9280
while (parent) {
9381

9482
// check if current node is a list item - if yes, take indentation from it
95-
var customIndentation = getCustomIndentationForListItem(current, sourceFile);
96-
if (customIndentation !== -1) {
97-
return customIndentation + indentation;
83+
var actualIndentation = getActualIndentationForListItem(current, sourceFile, options);
84+
if (actualIndentation !== -1) {
85+
return actualIndentation + indentation;
86+
}
87+
88+
parentStart = sourceFile.getLineAndCharacterFromPosition(parent.getStart(sourceFile));
89+
var parentAndChildShareLine =
90+
parentStart.line === currentStart.line ||
91+
isChildStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStart.line, sourceFile);
92+
93+
// try to fetch actual indentation for current node from source text
94+
var actualIndentation = getActualIndentationForNode(current, parent, currentStart, parentAndChildShareLine, sourceFile, options);
95+
if (actualIndentation !== -1) {
96+
return actualIndentation + indentation;
9897
}
9998

100-
parentStartLine = sourceFile.getLineAndCharacterFromPosition(parent.getStart(sourceFile)).line;
10199
// increase indentation if parent node wants its content to be indented and parent and child nodes don't start on the same line
102-
var increaseIndentation =
103-
isNodeContentIndented(parent, current) &&
104-
parentStartLine !== currentStartLine &&
105-
!isChildStartsOnTheSameLineWithElseInIfStatement(parent, current, currentStartLine, sourceFile);
100+
var increaseIndentation = isNodeContentIndented(parent, current) && !parentAndChildShareLine;
106101

107102
if (increaseIndentation) {
108103
indentation += options.indentSpaces;
109104
}
110105

111106
current = parent;
112-
currentStartLine = parentStartLine;
107+
currentStart = parentStart;
113108
parent = current.parent;
114109
}
115110

116111
return indentation;
117112
}
118113

114+
function isDeclaration(n: Node): boolean {
115+
switch(n.kind) {
116+
case SyntaxKind.ClassDeclaration:
117+
case SyntaxKind.EnumDeclaration:
118+
case SyntaxKind.FunctionDeclaration:
119+
case SyntaxKind.ImportDeclaration:
120+
case SyntaxKind.Method:
121+
case SyntaxKind.Property:
122+
case SyntaxKind.ModuleDeclaration:
123+
case SyntaxKind.InterfaceDeclaration:
124+
case SyntaxKind.VariableDeclaration:
125+
return true;
126+
default:
127+
return false;
128+
}
129+
}
130+
131+
function isStatement(n: Node): boolean {
132+
switch(n.kind) {
133+
case SyntaxKind.BreakStatement:
134+
case SyntaxKind.ContinueStatement:
135+
case SyntaxKind.DebuggerStatement:
136+
case SyntaxKind.DoStatement:
137+
case SyntaxKind.ExpressionStatement:
138+
case SyntaxKind.EmptyStatement:
139+
case SyntaxKind.ForInStatement:
140+
case SyntaxKind.ForStatement:
141+
case SyntaxKind.IfStatement:
142+
case SyntaxKind.LabelledStatement:
143+
case SyntaxKind.ReturnStatement:
144+
case SyntaxKind.SwitchStatement:
145+
case SyntaxKind.ThrowKeyword:
146+
case SyntaxKind.TryStatement:
147+
case SyntaxKind.VariableStatement:
148+
case SyntaxKind.WhileStatement:
149+
case SyntaxKind.WithStatement:
150+
return true;
151+
default:
152+
return false;
153+
}
154+
}
155+
156+
function getActualIndentationForListItemBeforeComma(commaToken: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number {
157+
// previous token is comma that separates items in list - find the previous item and try to derive indentation from it
158+
var precedingListItem = findPrecedingListItem(commaToken);
159+
var precedingListItemStartLineAndChar = sourceFile.getLineAndCharacterFromPosition(precedingListItem.getStart(sourceFile));
160+
var listStart = getStartLineAndCharacterForNode(precedingListItem.parent, sourceFile);
161+
162+
if (precedingListItemStartLineAndChar.line !== listStart.line) {
163+
return findColumnForFirstNonWhitespaceCharacterInLine(precedingListItemStartLineAndChar, sourceFile, options);
164+
// previous list item starts on the different line with list, find first non-whitespace character in this line and use its position as indentation
165+
var lineStartPosition = sourceFile.getPositionFromLineAndCharacter(precedingListItemStartLineAndChar.line, 1);
166+
for (var i = 0; i < precedingListItemStartLineAndChar.character; ++i) {
167+
if (!isWhiteSpace(sourceFile.text.charCodeAt(lineStartPosition + i))) {
168+
return i;
169+
}
170+
}
171+
172+
// seems that this is the first non-whitespace character on the line - return it
173+
return precedingListItemStartLineAndChar.character;
174+
}
175+
176+
return -1;
177+
}
178+
179+
function getActualIndentationForNode(current: Node, parent: Node, currentLineAndChar: LineAndCharacter, parentAndChildShareLine: boolean, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number {
180+
var useActualIndentation =
181+
(isDeclaration(current) || isStatement(current)) &&
182+
(parent.kind === SyntaxKind.SourceFile || !parentAndChildShareLine);
183+
184+
if (!useActualIndentation) {
185+
return -1;
186+
}
187+
188+
return findColumnForFirstNonWhitespaceCharacterInLine(currentLineAndChar, sourceFile, options);
189+
}
190+
119191
function discardInitialIndentationIfNextTokenIsOpenOrCloseBrace(precedingToken: Node, current: Node, lineAtPosition: number, sourceFile: SourceFile): boolean {
120192
var nextToken = findNextToken(precedingToken, current);
121193
if (!nextToken) {
@@ -136,15 +208,15 @@ module ts.formatting {
136208
// class A {
137209
// $}
138210

139-
var nextTokenStartLine = getStartLineForNode(nextToken, sourceFile);
211+
var nextTokenStartLine = getStartLineAndCharacterForNode(nextToken, sourceFile).line;
140212
return lineAtPosition === nextTokenStartLine;
141213
}
142214

143215
return false;
144216
}
145217

146-
function getStartLineForNode(n: Node, sourceFile: SourceFile): number {
147-
return sourceFile.getLineAndCharacterFromPosition(n.getStart(sourceFile)).line;
218+
function getStartLineAndCharacterForNode(n: Node, sourceFile: SourceFile): LineAndCharacter {
219+
return sourceFile.getLineAndCharacterFromPosition(n.getStart(sourceFile));
148220
}
149221

150222
function findPrecedingListItem(commaToken: Node): Node {
@@ -174,39 +246,39 @@ module ts.formatting {
174246
var elseKeyword = forEach(parent.getChildren(), c => c.kind === SyntaxKind.ElseKeyword && c);
175247
Debug.assert(elseKeyword);
176248

177-
var elseKeywordStartLine = getStartLineForNode(elseKeyword, sourceFile);
249+
var elseKeywordStartLine = getStartLineAndCharacterForNode(elseKeyword, sourceFile).line;
178250
return elseKeywordStartLine === childStartLine;
179251
}
180252
}
181253

182-
function getCustomIndentationForListItem(node: Node, sourceFile: SourceFile): number {
254+
function getActualIndentationForListItem(node: Node, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number {
183255
if (node.parent) {
184256
switch (node.parent.kind) {
185257
case SyntaxKind.ObjectLiteral:
186-
return getCustomIndentationFromList((<ObjectLiteral>node.parent).properties);
258+
return getActualIndentationFromList((<ObjectLiteral>node.parent).properties);
187259
case SyntaxKind.TypeLiteral:
188-
return getCustomIndentationFromList((<TypeLiteralNode>node.parent).members);
260+
return getActualIndentationFromList((<TypeLiteralNode>node.parent).members);
189261
case SyntaxKind.ArrayLiteral:
190-
return getCustomIndentationFromList((<ArrayLiteral>node.parent).elements);
262+
return getActualIndentationFromList((<ArrayLiteral>node.parent).elements);
191263
case SyntaxKind.FunctionDeclaration:
192264
case SyntaxKind.FunctionExpression:
193265
case SyntaxKind.ArrowFunction:
194266
case SyntaxKind.Method:
195267
case SyntaxKind.CallSignature:
196268
case SyntaxKind.ConstructSignature:
197269
if ((<SignatureDeclaration>node.parent).typeParameters && node.end < (<SignatureDeclaration>node.parent).typeParameters.end) {
198-
return getCustomIndentationFromList((<SignatureDeclaration>node.parent).typeParameters);
270+
return getActualIndentationFromList((<SignatureDeclaration>node.parent).typeParameters);
199271
}
200272
else {
201-
return getCustomIndentationFromList((<SignatureDeclaration>node.parent).parameters);
273+
return getActualIndentationFromList((<SignatureDeclaration>node.parent).parameters);
202274
}
203275
case SyntaxKind.NewExpression:
204276
case SyntaxKind.CallExpression:
205277
if ((<CallExpression>node.parent).typeArguments && node.end < (<CallExpression>node.parent).typeArguments.end) {
206-
return getCustomIndentationFromList((<CallExpression>node.parent).typeArguments);
278+
return getActualIndentationFromList((<CallExpression>node.parent).typeArguments);
207279
}
208280
else {
209-
return getCustomIndentationFromList((<CallExpression>node.parent).arguments);
281+
return getActualIndentationFromList((<CallExpression>node.parent).arguments);
210282
}
211283

212284
break;
@@ -215,31 +287,40 @@ module ts.formatting {
215287

216288
return -1;
217289

218-
function getCustomIndentationFromList(list: Node[]): number {
290+
function getActualIndentationFromList(list: Node[]): number {
219291
var index = indexOf(list, node);
220292
if (index !== -1) {
221-
var lineAndCol = sourceFile.getLineAndCharacterFromPosition(node.getStart(sourceFile));
293+
var lineAndCharacter = getStartLineAndCharacterForNode(node, sourceFile);;
222294
for (var i = index - 1; i >= 0; --i) {
223-
var prevLineAndCol = sourceFile.getLineAndCharacterFromPosition(list[i].getStart(sourceFile));
224-
if (lineAndCol.line !== prevLineAndCol.line) {
225-
return findFirstNonWhitespaceCharacterInLine(lineAndCol.line, lineAndCol.character, sourceFile);
295+
var prevLineAndCharacter = getStartLineAndCharacterForNode(list[i], sourceFile);
296+
if (lineAndCharacter.line !== prevLineAndCharacter.line) {
297+
return findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter, sourceFile, options);
226298
}
227-
lineAndCol = prevLineAndCol;
299+
lineAndCharacter = prevLineAndCharacter;
228300
}
229301
}
230302
return -1;
231303
}
232304
}
233305

234-
function findFirstNonWhitespaceCharacterInLine(line: number, maxCharacter: number, sourceFile: SourceFile): number {
235-
var lineStart = sourceFile.getPositionFromLineAndCharacter(line, 1);
236-
for (var i = 0; i < maxCharacter; ++i) {
237-
if (!isWhiteSpace(sourceFile.text.charCodeAt(lineStart + i))) {
238-
return i;
306+
function findColumnForFirstNonWhitespaceCharacterInLine(lineAndCharacter: LineAndCharacter, sourceFile: SourceFile, options: TypeScript.FormattingOptions): number {
307+
var lineStart = sourceFile.getPositionFromLineAndCharacter(lineAndCharacter.line, 1);
308+
var column = 0;
309+
for (var i = 0; i < lineAndCharacter.character; ++i) {
310+
var charCode = sourceFile.text.charCodeAt(lineStart + i);
311+
if (!isWhiteSpace(charCode)) {
312+
return column;
313+
}
314+
315+
if (charCode === CharacterCodes.tab) {
316+
column += options.spacesPerTab;
317+
}
318+
else {
319+
column++;
239320
}
240321
}
241322

242-
return maxCharacter;
323+
return column;
243324
}
244325

245326
function findNextToken(previousToken: Node, parent: Node): Node {
@@ -257,10 +338,10 @@ module ts.formatting {
257338
var shouldDiveInChildNode =
258339
// previous token is enclosed somewhere in the child
259340
(child.pos <= previousToken.pos && child.end > previousToken.end) ||
260-
// previous token end exactly at the beginning of child
341+
// previous token ends exactly at the beginning of child
261342
(child.pos === previousToken.end);
262343

263-
if (shouldDiveInChildNode && isCandidateNode(child)) {
344+
if (shouldDiveInChildNode && isCandidateNode(child)) {
264345
return find(child);
265346
}
266347
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// <reference path='fourslash.ts'/>
2+
3+
//// class A {
4+
//// /*1*/
5+
//// }
6+
7+
////module M {
8+
//// class C {
9+
//// /*2*/
10+
//// }
11+
////}
12+
13+
goTo.marker("1");
14+
verify.indentationIs(12);
15+
16+
goTo.marker("2");
17+
verify.indentationIs(16);

0 commit comments

Comments
 (0)