OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library dart_style.src.chunk_builder; | |
6 | |
7 import 'chunk.dart'; | |
8 import 'dart_formatter.dart'; | |
9 import 'debug.dart' as debug; | |
10 import 'line_writer.dart'; | |
11 import 'nesting_level.dart'; | |
12 import 'nesting_builder.dart'; | |
13 import 'rule/rule.dart'; | |
14 import 'source_code.dart'; | |
15 import 'whitespace.dart'; | |
16 | |
17 /// Takes the incremental serialized output of [SourceVisitor]--the source text | |
18 /// along with any comments and preserved whitespace--and produces a coherent | |
19 /// tree of [Chunk]s which can then be split into physical lines. | |
20 /// | |
21 /// Keeps track of leading indentation, expression nesting, and all of the hairy | |
22 /// code required to seamlessly integrate existing comments into the pure | |
23 /// output produced by [SourceVisitor]. | |
24 class ChunkBuilder { | |
25 final DartFormatter _formatter; | |
26 | |
27 /// The builder for the code surrounding the block that this writer is for, or | |
28 /// `null` if this is writing the top-level code. | |
29 final ChunkBuilder _parent; | |
30 | |
31 final SourceCode _source; | |
32 | |
33 final List<Chunk> _chunks; | |
34 | |
35 /// The whitespace that should be written to [_chunks] before the next | |
36 /// non-whitespace token. | |
37 /// | |
38 /// This ensures that changes to indentation and nesting also apply to the | |
39 /// most recent split, even if the visitor "creates" the split before changing | |
40 /// indentation or nesting. | |
41 Whitespace _pendingWhitespace = Whitespace.none; | |
42 | |
43 /// The nested stack of rules that are currently in use. | |
44 /// | |
45 /// New chunks are implicitly split by the innermost rule when the chunk is | |
46 /// ended. | |
47 final _rules = <Rule>[]; | |
48 | |
49 /// The set of rules known to contain hard splits that will in turn force | |
50 /// these rules to harden. | |
51 /// | |
52 /// This is accumulated lazily while chunks are being built. Then, once they | |
53 /// are all done, the rules are all hardened. We do this later because some | |
54 /// rules may not have all of their constraints fully wired up until after | |
55 /// the hard split appears. For example, a hard split in a positional | |
56 /// argument list needs to force the named arguments to split too, but we | |
57 /// don't create that rule until after the positional arguments are done. | |
58 final _hardSplitRules = new Set<Rule>(); | |
59 | |
60 /// The list of rules that are waiting until the next whitespace has been | |
61 /// written before they start. | |
62 final _lazyRules = <Rule>[]; | |
63 | |
64 /// The indexes of the chunks owned by each rule (except for hard splits). | |
65 final _ruleChunks = <Rule, List<int>>{}; | |
66 | |
67 /// The nested stack of spans that are currently being written. | |
68 final _openSpans = <OpenSpan>[]; | |
69 | |
70 /// The current state. | |
71 final _nesting = new NestingBuilder(); | |
72 | |
73 /// The stack of nesting levels where block arguments may start. | |
74 /// | |
75 /// A block argument's contents will nest at the last level in this stack. | |
76 final _blockArgumentNesting = <NestingLevel>[]; | |
77 | |
78 /// The index of the "current" chunk being written. | |
79 /// | |
80 /// If the last chunk is still being appended to, this is its index. | |
81 /// Otherwise, it is the index of the next chunk which will be created. | |
82 int get _currentChunkIndex { | |
83 if (_chunks.isEmpty) return 0; | |
84 if (_chunks.last.canAddText) return _chunks.length - 1; | |
85 return _chunks.length; | |
86 } | |
87 | |
88 /// Whether or not there was a leading comment that was flush left before any | |
89 /// other content was written. | |
90 /// | |
91 /// This is used when writing child blocks to make the parent chunk have the | |
92 /// right flush left value when a comment appears immediately inside the | |
93 /// block. | |
94 bool _firstFlushLeft = false; | |
95 | |
96 /// Whether there is pending whitespace that depends on the number of | |
97 /// newlines in the source. | |
98 /// | |
99 /// This is used to avoid calculating the newlines between tokens unless | |
100 /// actually needed since doing so is slow when done between every single | |
101 /// token pair. | |
102 bool get needsToPreserveNewlines => | |
103 _pendingWhitespace == Whitespace.oneOrTwoNewlines || | |
104 _pendingWhitespace == Whitespace.spaceOrNewline || | |
105 _pendingWhitespace == Whitespace.splitOrNewline; | |
106 | |
107 /// The number of characters of code that can fit in a single line. | |
108 int get pageWidth => _formatter.pageWidth; | |
109 | |
110 /// The current innermost rule. | |
111 Rule get rule => _rules.last; | |
112 | |
113 ChunkBuilder(this._formatter, this._source) | |
114 : _parent = null, | |
115 _chunks = [] { | |
116 indent(_formatter.indent); | |
117 startBlockArgumentNesting(); | |
118 } | |
119 | |
120 ChunkBuilder._(this._parent, this._formatter, this._source, this._chunks) { | |
121 startBlockArgumentNesting(); | |
122 } | |
123 | |
124 /// Writes [string], the text for a single token, to the output. | |
125 /// | |
126 /// By default, this also implicitly adds one level of nesting if we aren't | |
127 /// currently nested at all. We do this here so that if a comment appears | |
128 /// after any token within a statement or top-level form and that comment | |
129 /// leads to splitting, we correctly nest. Even pathological cases like: | |
130 /// | |
131 /// | |
132 /// import // comment | |
133 /// "this_gets_nested.dart"; | |
134 /// | |
135 /// If we didn't do this here, we'd have to call [nestExpression] after the | |
136 /// first token of practically every grammar production. | |
137 void write(String string) { | |
138 _emitPendingWhitespace(); | |
139 _writeText(string); | |
140 | |
141 _lazyRules.forEach(startRule); | |
142 _lazyRules.clear(); | |
143 | |
144 _nesting.commitNesting(); | |
145 } | |
146 | |
147 /// Writes a [WhitespaceChunk] of [type]. | |
148 void writeWhitespace(Whitespace type) { | |
149 _pendingWhitespace = type; | |
150 } | |
151 | |
152 /// Write a split owned by the current innermost rule. | |
153 /// | |
154 /// If [nesting] is given, uses that. Otherwise, uses the current nesting | |
155 /// level. If unsplit, it expands to a space if [space] is `true`. | |
156 /// | |
157 /// If [flushLeft] is `true`, then forces the next line to start at column | |
158 /// one regardless of any indentation or nesting. | |
159 /// | |
160 /// If [isDouble] is passed, forces the split to either be a single or double | |
161 /// newline. Otherwise, leaves it indeterminate. | |
162 Chunk split({bool space, bool isDouble, bool flushLeft}) => | |
163 _writeSplit(_rules.last, null, | |
164 flushLeft: flushLeft, isDouble: isDouble, spaceWhenUnsplit: space); | |
165 | |
166 /// Write a split owned by the current innermost rule. | |
167 /// | |
168 /// Unlike [split()], this ignores any current expression nesting. It always | |
169 /// indents the next line at the statement level. | |
170 Chunk blockSplit({bool space, bool isDouble}) => | |
171 _writeSplit(_rules.last, _nesting.blockNesting, | |
172 isDouble: isDouble, spaceWhenUnsplit: space); | |
173 | |
174 /// Outputs the series of [comments] and associated whitespace that appear | |
175 /// before [token] (which is not written by this). | |
176 /// | |
177 /// The list contains each comment as it appeared in the source between the | |
178 /// last token written and the next one that's about to be written. | |
179 /// | |
180 /// [linesBeforeToken] is the number of lines between the last comment (or | |
181 /// previous token if there are no comments) and the next token. | |
182 void writeComments( | |
183 List<SourceComment> comments, int linesBeforeToken, String token) { | |
184 // Corner case: if we require a blank line, but there exists one between | |
185 // some of the comments, or after the last one, then we don't need to | |
186 // enforce one before the first comment. Example: | |
187 // | |
188 // library foo; | |
189 // // comment | |
190 // | |
191 // class Bar {} | |
192 // | |
193 // Normally, a blank line is required after `library`, but since there is | |
194 // one after the comment, we don't need one before it. This is mainly so | |
195 // that commented out directives stick with their preceding group. | |
196 if (_pendingWhitespace == Whitespace.twoNewlines && | |
197 comments.first.linesBefore < 2) { | |
198 if (linesBeforeToken > 1) { | |
199 _pendingWhitespace = Whitespace.newline; | |
200 } else { | |
201 for (var i = 1; i < comments.length; i++) { | |
202 if (comments[i].linesBefore > 1) { | |
203 _pendingWhitespace = Whitespace.newline; | |
204 break; | |
205 } | |
206 } | |
207 } | |
208 } | |
209 | |
210 // Corner case: if the comments are completely inline (i.e. just a series | |
211 // of block comments with no newlines before, after, or between them), then | |
212 // they will eat any pending newlines. Make sure that doesn't happen by | |
213 // putting the pending whitespace before the first comment and moving them | |
214 // to their own line. Turns this: | |
215 // | |
216 // library foo; /* a */ /* b */ import 'a.dart'; | |
217 // | |
218 // into: | |
219 // | |
220 // library foo; | |
221 // | |
222 // /* a */ /* b */ | |
223 // import 'a.dart'; | |
224 if (linesBeforeToken == 0 && | |
225 comments.every((comment) => comment.isInline)) { | |
226 if (_pendingWhitespace.minimumLines > 0) { | |
227 comments.first.linesBefore = _pendingWhitespace.minimumLines; | |
228 linesBeforeToken = 1; | |
229 } | |
230 } | |
231 | |
232 // Write each comment and the whitespace between them. | |
233 for (var i = 0; i < comments.length; i++) { | |
234 var comment = comments[i]; | |
235 | |
236 preserveNewlines(comment.linesBefore); | |
237 | |
238 // Don't emit a space because we'll handle it below. If we emit it here, | |
239 // we may get a trailing space if the comment needs a line before it. | |
240 if (_pendingWhitespace == Whitespace.space) { | |
241 _pendingWhitespace = Whitespace.none; | |
242 } | |
243 _emitPendingWhitespace(); | |
244 | |
245 if (comment.linesBefore == 0) { | |
246 // If we're sitting on a split, move the comment before it to adhere it | |
247 // to the preceding text. | |
248 if (_shouldMoveCommentBeforeSplit(comment.text)) { | |
249 _chunks.last.allowText(); | |
250 } | |
251 | |
252 // The comment follows other text, so we need to decide if it gets a | |
253 // space before it or not. | |
254 if (_needsSpaceBeforeComment(isLineComment: comment.isLineComment)) { | |
255 _writeText(" "); | |
256 } | |
257 } else { | |
258 // The comment starts a line, so make sure it stays on its own line. | |
259 _writeHardSplit( | |
260 nest: true, | |
261 flushLeft: comment.flushLeft, | |
262 double: comment.linesBefore > 1); | |
263 } | |
264 | |
265 _writeText(comment.text); | |
266 | |
267 if (comment.selectionStart != null) { | |
268 startSelectionFromEnd(comment.text.length - comment.selectionStart); | |
269 } | |
270 | |
271 if (comment.selectionEnd != null) { | |
272 endSelectionFromEnd(comment.text.length - comment.selectionEnd); | |
273 } | |
274 | |
275 // Make sure there is at least one newline after a line comment and allow | |
276 // one or two after a block comment that has nothing after it. | |
277 var linesAfter; | |
278 if (i < comments.length - 1) { | |
279 linesAfter = comments[i + 1].linesBefore; | |
280 } else { | |
281 linesAfter = linesBeforeToken; | |
282 | |
283 // Always force a newline after multi-line block comments. Prevents | |
284 // mistakes like: | |
285 // | |
286 // /** | |
287 // * Some doc comment. | |
288 // */ someFunction() { ... } | |
289 if (linesAfter == 0 && comments.last.text.contains("\n")) { | |
290 linesAfter = 1; | |
291 } | |
292 } | |
293 | |
294 if (linesAfter > 0) _writeHardSplit(nest: true, double: linesAfter > 1); | |
295 } | |
296 | |
297 // If the comment has text following it (aside from a grouping character), | |
298 // it needs a trailing space. | |
299 if (_needsSpaceAfterLastComment(comments, token)) { | |
300 _pendingWhitespace = Whitespace.space; | |
301 } | |
302 | |
303 preserveNewlines(linesBeforeToken); | |
304 } | |
305 | |
306 /// If the current pending whitespace allows some source discretion, pins | |
307 /// that down given that the source contains [numLines] newlines at that | |
308 /// point. | |
309 void preserveNewlines(int numLines) { | |
310 // If we didn't know how many newlines the user authored between the last | |
311 // token and this one, now we do. | |
312 switch (_pendingWhitespace) { | |
313 case Whitespace.spaceOrNewline: | |
314 if (numLines > 0) { | |
315 _pendingWhitespace = Whitespace.nestedNewline; | |
316 } else { | |
317 _pendingWhitespace = Whitespace.space; | |
318 } | |
319 break; | |
320 | |
321 case Whitespace.splitOrNewline: | |
322 if (numLines > 0) { | |
323 _pendingWhitespace = Whitespace.nestedNewline; | |
324 } else { | |
325 _pendingWhitespace = Whitespace.none; | |
326 split(space: true); | |
327 } | |
328 break; | |
329 | |
330 case Whitespace.oneOrTwoNewlines: | |
331 if (numLines > 1) { | |
332 _pendingWhitespace = Whitespace.twoNewlines; | |
333 } else { | |
334 _pendingWhitespace = Whitespace.newline; | |
335 } | |
336 break; | |
337 } | |
338 } | |
339 | |
340 /// Creates a new indentation level [spaces] deeper than the current one. | |
341 /// | |
342 /// If omitted, [spaces] defaults to [Indent.block]. | |
343 void indent([int spaces]) { | |
344 _nesting.indent(spaces); | |
345 } | |
346 | |
347 /// Discards the most recent indentation level. | |
348 void unindent() { | |
349 _nesting.unindent(); | |
350 } | |
351 | |
352 /// Starts a new span with [cost]. | |
353 /// | |
354 /// Each call to this needs a later matching call to [endSpan]. | |
355 void startSpan([int cost = Cost.normal]) { | |
356 _openSpans.add(new OpenSpan(_currentChunkIndex, cost)); | |
357 } | |
358 | |
359 /// Ends the innermost span. | |
360 void endSpan() { | |
361 var openSpan = _openSpans.removeLast(); | |
362 | |
363 // A span that just covers a single chunk can't be split anyway. | |
364 var end = _currentChunkIndex; | |
365 if (openSpan.start == end) return; | |
366 | |
367 // Add the span to every chunk that can split it. | |
368 var span = new Span(openSpan.cost); | |
369 for (var i = openSpan.start; i < end; i++) { | |
370 var chunk = _chunks[i]; | |
371 if (!chunk.isHardSplit) chunk.spans.add(span); | |
372 } | |
373 } | |
374 | |
375 /// Starts a new [Rule]. | |
376 /// | |
377 /// If omitted, defaults to a new [SimpleRule]. | |
378 void startRule([Rule rule]) { | |
379 if (rule == null) rule = new SimpleRule(); | |
380 | |
381 // See if any of the rules that contain this one care if it splits. | |
382 _rules.forEach((outer) => outer.contain(rule)); | |
383 _rules.add(rule); | |
384 } | |
385 | |
386 /// Starts a new [Rule] that comes into play *after* the next whitespace | |
387 /// (including comments) is written. | |
388 /// | |
389 /// This is used for binary operators who want to start a rule before the | |
390 /// first operand but not get forced to split if a comment appears before the | |
391 /// entire expression. | |
392 /// | |
393 /// If [rule] is omitted, defaults to a new [SimpleRule]. | |
394 void startLazyRule([Rule rule]) { | |
395 if (rule == null) rule = new SimpleRule(); | |
396 | |
397 _lazyRules.add(rule); | |
398 } | |
399 | |
400 /// Ends the innermost rule. | |
401 void endRule() { | |
402 _rules.removeLast(); | |
403 } | |
404 | |
405 /// Pre-emptively forces all of the current rules to become hard splits. | |
406 /// | |
407 /// This is called by [SourceVisitor] when it can determine that a rule will | |
408 /// will always be split. Turning it (and the surrounding rules) into hard | |
409 /// splits lets the writer break the output into smaller pieces for the line | |
410 /// splitter, which helps performance and avoids failing on very large input. | |
411 /// | |
412 /// In particular, it's easy for the visitor to know that collections with a | |
413 /// large number of items must split. Doing that early avoids crashing the | |
414 /// splitter when it tries to recurse on huge collection literals. | |
415 void forceRules() => _handleHardSplit(); | |
416 | |
417 /// Begins a new expression nesting level [indent] spaces deeper than the | |
418 /// current one if it splits. | |
419 /// | |
420 /// If [indent] is omitted, defaults to [Indent.expression]. If [now] is | |
421 /// `true`, commits the nesting change immediately instead of waiting until | |
422 /// after the next chunk of text is written. | |
423 void nestExpression({int indent, bool now}) { | |
424 if (now == null) now = false; | |
425 | |
426 _nesting.nest(indent); | |
427 if (now) _nesting.commitNesting(); | |
428 } | |
429 | |
430 /// Discards the most recent level of expression nesting. | |
431 /// | |
432 /// Expressions that are more nested will get increased indentation when split | |
433 /// if the previous line has a lower level of nesting. | |
434 void unnest() { | |
435 _nesting.unnest(); | |
436 } | |
437 | |
438 /// Marks the selection starting point as occurring [fromEnd] characters to | |
439 /// the left of the end of what's currently been written. | |
440 /// | |
441 /// It counts backwards from the end because this is called *after* the chunk | |
442 /// of text containing the selection has been output. | |
443 void startSelectionFromEnd(int fromEnd) { | |
444 assert(_chunks.isNotEmpty); | |
445 _chunks.last.startSelectionFromEnd(fromEnd); | |
446 } | |
447 | |
448 /// Marks the selection ending point as occurring [fromEnd] characters to the | |
449 /// left of the end of what's currently been written. | |
450 /// | |
451 /// It counts backwards from the end because this is called *after* the chunk | |
452 /// of text containing the selection has been output. | |
453 void endSelectionFromEnd(int fromEnd) { | |
454 assert(_chunks.isNotEmpty); | |
455 _chunks.last.endSelectionFromEnd(fromEnd); | |
456 } | |
457 | |
458 /// Captures the current nesting level as marking where subsequent block | |
459 /// arguments should start. | |
460 void startBlockArgumentNesting() { | |
461 _blockArgumentNesting.add(_nesting.currentNesting); | |
462 } | |
463 | |
464 /// Releases the last nesting level captured by [startBlockArgumentNesting]. | |
465 void endBlockArgumentNesting() { | |
466 _blockArgumentNesting.removeLast(); | |
467 } | |
468 | |
469 /// Starts a new block as a child of the current chunk. | |
470 /// | |
471 /// Nested blocks are handled using their own independent [LineWriter]. | |
472 ChunkBuilder startBlock() { | |
473 var builder = | |
474 new ChunkBuilder._(this, _formatter, _source, _chunks.last.blockChunks); | |
475 | |
476 // A block always starts off indented one level. | |
477 builder.indent(); | |
478 | |
479 return builder; | |
480 } | |
481 | |
482 /// Ends this [ChunkBuilder], which must have been created by [startBlock()]. | |
483 /// | |
484 /// Forces the chunk that owns the block to split if it can tell that the | |
485 /// block contents will always split. It does that by looking for hard splits | |
486 /// in the block. If [ignoredSplit] is given, that rule will be ignored | |
487 /// when determining if a block contains a hard split. If [forceSplit] is | |
488 /// `true`, the block is considered to always split. | |
489 /// | |
490 /// Returns the previous writer for the surrounding block. | |
491 ChunkBuilder endBlock(HardSplitRule ignoredSplit, {bool forceSplit}) { | |
492 _divideChunks(); | |
493 | |
494 // If we don't already know if the block is going to split, see if it | |
495 // contains any hard splits or is longer than a page. | |
496 if (!forceSplit) { | |
497 var length = 0; | |
498 for (var chunk in _chunks) { | |
499 length += chunk.length + chunk.unsplitBlockLength; | |
500 if (length > _formatter.pageWidth) { | |
501 forceSplit = true; | |
502 break; | |
503 } | |
504 | |
505 if (chunk.isHardSplit && chunk.rule != ignoredSplit) { | |
506 forceSplit = true; | |
507 break; | |
508 } | |
509 } | |
510 } | |
511 | |
512 // If there is a hard newline within the block, force the surrounding rule | |
513 // for it so that we apply that constraint. | |
514 if (forceSplit) _parent.forceRules(); | |
515 | |
516 // Write the split for the block contents themselves. | |
517 _parent._writeSplit(_parent._rules.last, _parent._blockArgumentNesting.last, | |
518 flushLeft: _firstFlushLeft); | |
519 return _parent; | |
520 } | |
521 | |
522 /// Finishes writing and returns a [SourceCode] containing the final output | |
523 /// and updated selection, if any. | |
524 SourceCode end() { | |
525 _writeHardSplit(); | |
526 _divideChunks(); | |
527 | |
528 if (debug.traceChunkBuilder) { | |
529 debug.log(debug.green("\nBuilt:")); | |
530 debug.dumpChunks(0, _chunks); | |
531 debug.log(); | |
532 } | |
533 | |
534 var writer = new LineWriter(_formatter, _chunks); | |
535 var result = writer.writeLines(_formatter.indent, | |
536 isCompilationUnit: _source.isCompilationUnit); | |
537 | |
538 var selectionStart; | |
539 var selectionLength; | |
540 if (_source.selectionStart != null) { | |
541 selectionStart = result.selectionStart; | |
542 var selectionEnd = result.selectionEnd; | |
543 | |
544 // If we haven't hit the beginning and/or end of the selection yet, they | |
545 // must be at the very end of the code. | |
546 if (selectionStart == null) selectionStart = writer.length; | |
547 if (selectionEnd == null) selectionEnd = writer.length; | |
548 | |
549 selectionLength = selectionEnd - selectionStart; | |
550 } | |
551 | |
552 return new SourceCode(result.text, | |
553 uri: _source.uri, | |
554 isCompilationUnit: _source.isCompilationUnit, | |
555 selectionStart: selectionStart, | |
556 selectionLength: selectionLength); | |
557 } | |
558 | |
559 /// Writes the current pending [Whitespace] to the output, if any. | |
560 /// | |
561 /// This should only be called after source lines have been preserved to turn | |
562 /// any ambiguous whitespace into a concrete choice. | |
563 void _emitPendingWhitespace() { | |
564 // Output any pending whitespace first now that we know it won't be | |
565 // trailing. | |
566 switch (_pendingWhitespace) { | |
567 case Whitespace.space: | |
568 _writeText(" "); | |
569 break; | |
570 | |
571 case Whitespace.newline: | |
572 _writeHardSplit(); | |
573 break; | |
574 | |
575 case Whitespace.nestedNewline: | |
576 _writeHardSplit(nest: true); | |
577 break; | |
578 | |
579 case Whitespace.newlineFlushLeft: | |
580 _writeHardSplit(nest: true, flushLeft: true); | |
581 break; | |
582 | |
583 case Whitespace.twoNewlines: | |
584 _writeHardSplit(double: true); | |
585 break; | |
586 | |
587 case Whitespace.spaceOrNewline: | |
588 case Whitespace.splitOrNewline: | |
589 case Whitespace.oneOrTwoNewlines: | |
590 // We should have pinned these down before getting here. | |
591 assert(false); | |
592 break; | |
593 } | |
594 | |
595 _pendingWhitespace = Whitespace.none; | |
596 } | |
597 | |
598 /// Returns `true` if the last chunk is a split that should be moved after the | |
599 /// comment that is about to be written. | |
600 bool _shouldMoveCommentBeforeSplit(String comment) { | |
601 // Not if there is nothing before it. | |
602 if (_chunks.isEmpty) return false; | |
603 | |
604 // Multi-line comments are always pushed to the next line. | |
605 if (comment.contains("\n")) return false; | |
606 | |
607 // If the text before the split is an open grouping character, we don't | |
608 // want to adhere the comment to that. | |
609 var text = _chunks.last.text; | |
610 return !text.endsWith("(") && !text.endsWith("[") && !text.endsWith("{"); | |
611 } | |
612 | |
613 /// Returns `true` if a space should be output between the end of the current | |
614 /// output and the subsequent comment which is about to be written. | |
615 /// | |
616 /// This is only called if the comment is trailing text in the unformatted | |
617 /// source. In most cases, a space will be output to separate the comment | |
618 /// from what precedes it. This returns false if: | |
619 /// | |
620 /// * This comment does begin the line in the output even if it didn't in | |
621 /// the source. | |
622 /// * The comment is a block comment immediately following a grouping | |
623 /// character (`(`, `[`, or `{`). This is to allow `foo(/* comment */)`, | |
624 /// et. al. | |
625 bool _needsSpaceBeforeComment({bool isLineComment}) { | |
626 // Not at the start of the file. | |
627 if (_chunks.isEmpty) return false; | |
628 | |
629 // Not at the start of a line. | |
630 if (!_chunks.last.canAddText) return false; | |
631 | |
632 var text = _chunks.last.text; | |
633 if (text.endsWith("\n")) return false; | |
634 | |
635 // Always put a space before line comments. | |
636 if (isLineComment) return true; | |
637 | |
638 // Block comments do not get a space if following a grouping character. | |
639 return !text.endsWith("(") && !text.endsWith("[") && !text.endsWith("{"); | |
640 } | |
641 | |
642 /// Returns `true` if a space should be output after the last comment which | |
643 /// was just written and the token that will be written. | |
644 bool _needsSpaceAfterLastComment(List<SourceComment> comments, String token) { | |
645 // Not if there are no comments. | |
646 if (comments.isEmpty) return false; | |
647 | |
648 // Not at the beginning of a line. | |
649 if (!_chunks.last.canAddText) return false; | |
650 | |
651 // Otherwise, it gets a space if the following token is not a delimiter or | |
652 // the empty string, for EOF. | |
653 return token != ")" && | |
654 token != "]" && | |
655 token != "}" && | |
656 token != "," && | |
657 token != ";" && | |
658 token != ""; | |
659 } | |
660 | |
661 /// Appends a hard split with the current indentation and nesting (the latter | |
662 /// only if [nest] is `true`). | |
663 /// | |
664 /// If [double] is `true` or `false`, forces a since or double line to be | |
665 /// output. Otherwise, it is left indeterminate. | |
666 /// | |
667 /// If [flushLeft] is `true`, then the split will always cause the next line | |
668 /// to be at column zero. Otherwise, it uses the normal indentation and | |
669 /// nesting behavior. | |
670 void _writeHardSplit({bool nest: false, bool double, bool flushLeft}) { | |
671 // A hard split overrides any other whitespace. | |
672 _pendingWhitespace = null; | |
673 _writeSplit(new HardSplitRule(), nest ? null : _nesting.blockNesting, | |
674 flushLeft: flushLeft, isDouble: double); | |
675 } | |
676 | |
677 /// Ends the current chunk (if any) with the given split information. | |
678 /// | |
679 /// Returns the chunk. | |
680 Chunk _writeSplit(Rule rule, NestingLevel nesting, | |
681 {bool flushLeft, bool isDouble, bool spaceWhenUnsplit}) { | |
682 if (_chunks.isEmpty) { | |
683 if (flushLeft != null) _firstFlushLeft = flushLeft; | |
684 | |
685 return null; | |
686 } | |
687 | |
688 if (nesting == null) nesting = _nesting.nesting; | |
689 | |
690 var chunk = _chunks.last; | |
691 chunk.applySplit(rule, _nesting.indentation, nesting, | |
692 flushLeft: flushLeft, | |
693 isDouble: isDouble, | |
694 spaceWhenUnsplit: spaceWhenUnsplit); | |
695 | |
696 // Keep track of which chunks are owned by the rule. | |
697 if (rule is! HardSplitRule) { | |
698 _ruleChunks.putIfAbsent(rule, () => []).add(_chunks.length - 1); | |
699 } | |
700 | |
701 if (chunk.isHardSplit) _handleHardSplit(); | |
702 | |
703 return chunk; | |
704 } | |
705 | |
706 /// Writes [text] to either the current chunk or a new one if the current | |
707 /// chunk is complete. | |
708 void _writeText(String text) { | |
709 if (_chunks.isNotEmpty && _chunks.last.canAddText) { | |
710 _chunks.last.appendText(text); | |
711 } else { | |
712 _chunks.add(new Chunk(text)); | |
713 } | |
714 } | |
715 | |
716 /// Returns true if we can divide the chunks at [index] and line split the | |
717 /// ones before and after that separately. | |
718 bool _canDivideAt(int i) { | |
719 var chunk = _chunks[i]; | |
720 if (!chunk.isHardSplit) return false; | |
721 if (chunk.nesting.isNested) return false; | |
722 if (chunk.blockChunks.isNotEmpty) return false; | |
723 | |
724 // Make sure we don't split the line in the middle of a rule. | |
725 var chunks = _ruleChunks[chunk.rule]; | |
726 if (chunks != null && chunks.any((other) => other > i)) return false; | |
727 | |
728 return true; | |
729 } | |
730 | |
731 /// Pre-processes the chunks after they are done being written by the visitor | |
732 /// but before they are run through the line splitter. | |
733 /// | |
734 /// Marks ranges of chunks that can be line split independently to keep the | |
735 /// batches we send to [LineSplitter] small. | |
736 void _divideChunks() { | |
737 // Harden all of the rules that we know get forced by containing hard | |
738 // splits, along with all of the other rules they constrain. | |
739 _hardenRules(); | |
740 | |
741 // Now that we know where all of the divided chunk sections are, mark the | |
742 // chunks. | |
743 for (var i = 0; i < _chunks.length; i++) { | |
744 _chunks[i].markDivide(_canDivideAt(i)); | |
745 } | |
746 } | |
747 | |
748 /// Hardens the active rules when a hard split occurs within them. | |
749 void _handleHardSplit() { | |
750 if (_rules.isEmpty) return; | |
751 | |
752 // If the current rule doesn't care, it will "eat" the hard split and no | |
753 // others will care either. | |
754 if (!_rules.last.splitsOnInnerRules) return; | |
755 | |
756 // Start with the innermost rule. This will traverse the other rules it | |
757 // constrains. | |
758 _hardSplitRules.add(_rules.last); | |
759 } | |
760 | |
761 /// Replaces all of the previously hardened rules with hard splits, along | |
762 /// with every rule that those constrain to also split. | |
763 /// | |
764 /// This should only be called after all chunks have been written. | |
765 void _hardenRules() { | |
766 if (_hardSplitRules.isEmpty) return; | |
767 | |
768 // Harden all of the rules that are constrained by [rules] as well. | |
769 var hardenedRules = new Set(); | |
770 walkConstraints(rule) { | |
771 if (hardenedRules.contains(rule)) return; | |
772 hardenedRules.add(rule); | |
773 | |
774 // Follow this rule's constraints, recursively. | |
775 for (var other in _ruleChunks.keys) { | |
776 if (other == rule) continue; | |
777 | |
778 if (rule.constrain(rule.fullySplitValue, other) == | |
779 other.fullySplitValue) { | |
780 walkConstraints(other); | |
781 } | |
782 } | |
783 } | |
784 | |
785 for (var rule in _hardSplitRules) { | |
786 walkConstraints(rule); | |
787 } | |
788 | |
789 // Harden every chunk that uses one of these rules. | |
790 for (var chunk in _chunks) { | |
791 if (hardenedRules.contains(chunk.rule)) { | |
792 chunk.harden(); | |
793 } | |
794 } | |
795 } | |
796 } | |
OLD | NEW |