@@ -61,6 +61,13 @@ getLocForContentStartOnSameLine(SourceManager &SM, SourceLoc Loc) {
61
61
return LineStart.getAdvancedLoc (Indentation.size ());
62
62
}
63
63
64
+ // / \returns true if the line at \c Loc is either empty or only contains whitespace
65
+ static bool isLineAtLocEmpty (SourceManager &SM, SourceLoc Loc) {
66
+ SourceLoc LineStart = Lexer::getLocForStartOfLine (SM, Loc);
67
+ SourceLoc Next = Lexer::getTokenAtLocation (SM, LineStart, CommentRetentionMode::ReturnAsTokens).getLoc ();
68
+ return Next.isInvalid () || !isOnSameLine (SM, Loc, Next);
69
+ }
70
+
64
71
// / \returns the first token after the token at \c Loc.
65
72
static Optional<Token>
66
73
getTokenAfter (SourceManager &SM, SourceLoc Loc, bool SkipComments = true ) {
@@ -136,6 +143,21 @@ static ClosureExpr *findTrailingClosureFromArgument(Expr *arg) {
136
143
return nullptr ;
137
144
}
138
145
146
+ static size_t calcVisibleWhitespacePrefix (StringRef Line,
147
+ CodeFormatOptions Options) {
148
+ size_t Indent = 0 ;
149
+ for (auto Char : Line) {
150
+ if (Char == ' \t ' ) {
151
+ Indent += Options.TabWidth ;
152
+ } else if (Char == ' ' || Char == ' \v ' || Char == ' \f ' ) {
153
+ Indent++;
154
+ } else {
155
+ break ;
156
+ }
157
+ }
158
+ return Indent;
159
+ }
160
+
139
161
// / An indentation context of the target location
140
162
struct IndentContext {
141
163
enum ContextKind { Exact, LineStart };
@@ -258,16 +280,14 @@ class FormatContext {
258
280
Optional<IndentContext> InnermostCtx;
259
281
bool InDocCommentBlock;
260
282
bool InCommentLine;
261
- bool InStringLiteral;
262
283
263
284
public:
264
285
FormatContext (SourceManager &SM,
265
286
Optional<IndentContext> IndentCtx,
266
287
bool InDocCommentBlock = false ,
267
- bool InCommentLine = false ,
268
- bool InStringLiteral = false )
288
+ bool InCommentLine = false )
269
289
:SM(SM), InnermostCtx(IndentCtx), InDocCommentBlock(InDocCommentBlock),
270
- InCommentLine (InCommentLine), InStringLiteral(InStringLiteral) { }
290
+ InCommentLine (InCommentLine) { }
271
291
272
292
bool IsInDocCommentBlock () {
273
293
return InDocCommentBlock;
@@ -277,10 +297,6 @@ class FormatContext {
277
297
return InCommentLine;
278
298
}
279
299
280
- bool IsInStringLiteral () const {
281
- return InStringLiteral;
282
- }
283
-
284
300
void padToExactColumn (StringBuilder &Builder,
285
301
const CodeFormatOptions &FmtOptions) {
286
302
assert (isExact () && " Context is not exact?" );
@@ -1208,8 +1224,9 @@ class FormatWalker : public ASTWalker {
1208
1224
bool InDocCommentBlock = false ;
1209
1225
// / Whether the target location appears within a line comment.
1210
1226
bool InCommentLine = false ;
1211
- // / Whether the target location appears within a string literal.
1212
- bool InStringLiteral = false ;
1227
+
1228
+ // / The range of the string literal the target is inside of (if any, invalid otherwise).
1229
+ CharSourceRange StringLiteralRange;
1213
1230
1214
1231
public:
1215
1232
explicit FormatWalker (SourceFile &SF, SourceManager &SM, CodeFormatOptions &Options)
@@ -1224,7 +1241,8 @@ class FormatWalker : public ASTWalker {
1224
1241
CtxOverride.clear ();
1225
1242
TargetLocation = Loc;
1226
1243
TargetLineLoc = Lexer::getLocForStartOfLine (SM, TargetLocation);
1227
- InDocCommentBlock = InCommentLine = InStringLiteral = false ;
1244
+ InDocCommentBlock = InCommentLine = false ;
1245
+ StringLiteralRange = CharSourceRange ();
1228
1246
NodesToSkip.clear ();
1229
1247
CurrentTokIt = TokenList.begin ();
1230
1248
@@ -1234,14 +1252,99 @@ class FormatWalker : public ASTWalker {
1234
1252
if (InnermostCtx)
1235
1253
CtxOverride.applyIfNeeded (SM, *InnermostCtx);
1236
1254
1255
+ if (StringLiteralRange.isValid ()) {
1256
+ assert (!InDocCommentBlock && !InCommentLine &&
1257
+ " Target is in both a string and comment" );
1258
+ InnermostCtx = indentWithinStringLiteral ();
1259
+ } else {
1260
+ assert (!InDocCommentBlock || !InCommentLine &&
1261
+ " Target is in both a doc comment block and comment line" );
1262
+ }
1263
+
1237
1264
return FormatContext (SM, InnermostCtx, InDocCommentBlock,
1238
- InCommentLine, InStringLiteral );
1265
+ InCommentLine);
1239
1266
}
1240
1267
1268
+ private:
1269
+
1270
+ Optional<IndentContext> indentWithinStringLiteral () {
1271
+ assert (StringLiteralRange.isValid () && " Target is not within a string literal" );
1272
+
1273
+ // This isn't ideal since if the user types """""" and then an enter
1274
+ // inside after, we won't indent the end quotes. But indenting the end
1275
+ // quotes could lead to an error in the rest of the string, so best to
1276
+ // avoid it entirely for now.
1277
+ if (isOnSameLine (SM, TargetLineLoc, StringLiteralRange.getEnd ()))
1278
+ return IndentContext {TargetLocation, false , IndentContext::Exact};
1279
+
1280
+ // If there's contents before the end quotes then it's likely the quotes
1281
+ // are actually the start quotes of the next string in the file. Pretend
1282
+ // they don't exist so their indent doesn't affect the indenting.
1283
+ SourceLoc EndLineContentLoc =
1284
+ getLocForContentStartOnSameLine (SM, StringLiteralRange.getEnd ());
1285
+ bool HaveEndQuotes = CharSourceRange (SM, EndLineContentLoc,
1286
+ StringLiteralRange.getEnd ())
1287
+ .str ().equals (StringRef (" \"\"\" " ));
1288
+
1289
+ if (!HaveEndQuotes) {
1290
+ // Indent to the same indentation level as the first non-empty line
1291
+ // before the target. If that line is the start line then either use
1292
+ // the same indentation of the start quotes if they are on their own
1293
+ // line, or an extra indentation otherwise.
1294
+ //
1295
+ // This will indent lines with content on it as well, which should be
1296
+ // fine since it is quite unlikely anyone would format a range that
1297
+ // includes an unterminated string.
1298
+
1299
+ SourceLoc AlignLoc = TargetLineLoc;
1300
+ while (SM.isBeforeInBuffer (StringLiteralRange.getStart (), AlignLoc)) {
1301
+ AlignLoc = Lexer::getLocForStartOfLine (SM, AlignLoc.getAdvancedLoc (-1 ));
1302
+ if (!isLineAtLocEmpty (SM, AlignLoc)) {
1303
+ AlignLoc = getLocForContentStartOnSameLine (SM, AlignLoc);
1304
+ break ;
1305
+ }
1306
+ }
1307
+
1308
+ if (isOnSameLine (SM, AlignLoc, StringLiteralRange.getStart ())) {
1309
+ SourceLoc StartLineContentLoc =
1310
+ getLocForContentStartOnSameLine (SM, StringLiteralRange.getStart ());
1311
+ bool StartLineOnlyQuotes = CharSourceRange (SM, StartLineContentLoc,
1312
+ StringLiteralRange.getEnd ())
1313
+ .str ().startswith (StringRef (" \"\"\" " ));
1314
+ if (!StartLineOnlyQuotes)
1315
+ return IndentContext {StringLiteralRange.getStart (), true };
1316
+
1317
+ AlignLoc = StringLiteralRange.getStart ();
1318
+ }
1319
+
1320
+ return IndentContext {AlignLoc, false , IndentContext::Exact};
1321
+ }
1322
+
1323
+ // If there are end quotes, only enforce a minimum indentation. We don't
1324
+ // want to add any other indentation since that could add unintended
1325
+ // whitespace to existing strings. Could change this if the full range
1326
+ // was passed rather than a single line - in that case we *would* indent
1327
+ // if the range was a single empty line.
1328
+
1329
+ CharSourceRange TargetIndentRange =
1330
+ CharSourceRange (SM, Lexer::getLocForStartOfLine (SM, TargetLocation),
1331
+ TargetLocation);
1332
+ CharSourceRange EndIndentRange =
1333
+ CharSourceRange (SM, Lexer::getLocForStartOfLine (SM, EndLineContentLoc),
1334
+ EndLineContentLoc);
1335
+
1336
+ size_t TargetIndent =
1337
+ calcVisibleWhitespacePrefix (TargetIndentRange.str (), FmtOptions);
1338
+ size_t EndIndent =
1339
+ calcVisibleWhitespacePrefix (EndIndentRange.str (), FmtOptions);
1340
+ if (TargetIndent >= EndIndent)
1341
+ return IndentContext {TargetLocation, false , IndentContext::Exact};
1342
+
1343
+ return IndentContext {EndLineContentLoc, false , IndentContext::Exact};
1344
+ }
1241
1345
1242
1346
#pragma mark ASTWalker overrides and helpers
1243
1347
1244
- private:
1245
1348
bool walkCustomAttributes (Decl *D) {
1246
1349
// CustomAttrs of non-param VarDecls are handled when this method is called
1247
1350
// on their containing PatternBindingDecls (below).
@@ -1330,7 +1433,7 @@ class FormatWalker : public ASTWalker {
1330
1433
SM.isBeforeInBuffer (E->getStartLoc (), TargetLocation) &&
1331
1434
SM.isBeforeInBuffer (TargetLocation,
1332
1435
Lexer::getLocForEndOfToken (SM, E->getEndLoc ()))) {
1333
- InStringLiteral = true ;
1436
+ StringLiteralRange = Lexer::getCharSourceRangeFromSourceRange (SM, E-> getSourceRange ()) ;
1334
1437
}
1335
1438
1336
1439
// Create a default indent context for all top-level expressions
@@ -1352,7 +1455,7 @@ class FormatWalker : public ASTWalker {
1352
1455
1353
1456
// Don't visit the child expressions of interpolated strings directly -
1354
1457
// visit only the argument of each appendInterpolation call instead, and
1355
- // update InStringLiteral for each segment.
1458
+ // set StringLiteralRange if needed for each segment.
1356
1459
if (auto *ISL = dyn_cast<InterpolatedStringLiteralExpr>(E)) {
1357
1460
if (Action.shouldVisitChildren ()) {
1358
1461
llvm::SaveAndRestore<ASTWalker::ParentTy>(Parent, ISL);
@@ -1364,7 +1467,8 @@ class FormatWalker : public ASTWalker {
1364
1467
// Handle the preceeding string segment.
1365
1468
CharSourceRange StringRange (SM, PrevStringStart, CE->getStartLoc ());
1366
1469
if (StringRange.contains (TargetLocation)) {
1367
- InStringLiteral = true ;
1470
+ StringLiteralRange =
1471
+ Lexer::getCharSourceRangeFromSourceRange (SM, E->getSourceRange ());
1368
1472
return ;
1369
1473
}
1370
1474
// Walk into the interpolation segment.
@@ -1378,7 +1482,8 @@ class FormatWalker : public ASTWalker {
1378
1482
SourceLoc End = Lexer::getLocForEndOfToken (SM, ISL->getStartLoc ());
1379
1483
CharSourceRange StringRange (SM, PrevStringStart, End);
1380
1484
if (StringRange.contains (TargetLocation))
1381
- InStringLiteral = true ;
1485
+ StringLiteralRange =
1486
+ Lexer::getCharSourceRangeFromSourceRange (SM, E->getSourceRange ());
1382
1487
1383
1488
return {false , E};
1384
1489
}
@@ -1467,7 +1572,7 @@ class FormatWalker : public ASTWalker {
1467
1572
}
1468
1573
1469
1574
void scanTokensUntil (SourceLoc Loc) {
1470
- if (InDocCommentBlock || InCommentLine)
1575
+ if (InDocCommentBlock || InCommentLine || StringLiteralRange. isValid () )
1471
1576
return ;
1472
1577
for (auto Invalid = Loc.isInvalid (); CurrentTokIt != TokenList.end () &&
1473
1578
(Invalid || SM.isBeforeInBuffer (CurrentTokIt->getLoc (), Loc));
@@ -1477,16 +1582,16 @@ class FormatWalker : public ASTWalker {
1477
1582
SourceLoc StartLineLoc = Lexer::getLocForStartOfLine (
1478
1583
SM, CommentRange.getStart ());
1479
1584
1480
- // The -1 is needed in case the past-the-end position is a newline
1481
- // character. In that case getLocForStartOfLine returns the start of
1482
- // the next line.
1483
1585
SourceLoc EndLineLoc = Lexer::getLocForStartOfLine (
1484
- SM, CommentRange.getEnd (). getAdvancedLoc (- 1 ) );
1586
+ SM, CommentRange.getEnd ());
1485
1587
auto TokenStr = CurrentTokIt->getRange ().str ();
1486
1588
InDocCommentBlock |= SM.isBeforeInBuffer (StartLineLoc, TargetLineLoc) && !SM.isBeforeInBuffer (EndLineLoc, TargetLineLoc) &&
1487
1589
TokenStr.startswith (" /*" );
1488
1590
InCommentLine |= StartLineLoc == TargetLineLoc &&
1489
1591
TokenStr.startswith (" //" );
1592
+ } else if (CurrentTokIt->getKind () == tok::unknown &&
1593
+ CurrentTokIt->getRange ().str ().startswith (" \"\"\" " )) {
1594
+ StringLiteralRange = CurrentTokIt->getRange ();
1490
1595
}
1491
1596
}
1492
1597
}
@@ -2797,12 +2902,6 @@ class CodeFormatter {
2797
2902
std::pair<LineRange, std::string> indent (unsigned LineIndex,
2798
2903
FormatContext &FC,
2799
2904
StringRef Text) {
2800
- if (FC.IsInStringLiteral ()) {
2801
- return std::make_pair (
2802
- LineRange (LineIndex, 1 ),
2803
- swift::ide::getTextForLine (LineIndex, Text, /* Trim*/ false ).str ());
2804
- }
2805
-
2806
2905
if (FC.isExact ()) {
2807
2906
StringRef Line = swift::ide::getTextForLine (LineIndex, Text, /* Trim*/ true );
2808
2907
StringBuilder Builder;
0 commit comments