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.line_writer;
|
6 | |
|
7 | | import 'chunk.dart';
|
8 | | import 'dart_formatter.dart';
|
9 | | import 'debug.dart' as debug;
|
10 | | import 'line_splitting/line_splitter.dart';
|
11 | | import 'whitespace.dart';
|
12 | |
|
13 | | /// Given a series of chunks, splits them into lines and writes the result to
|
14 | | /// a buffer.
|
15 | | class LineWriter {
|
16 | | final _buffer = new StringBuffer();
|
17 | |
|
18 | | final List<Chunk> _chunks;
|
19 | |
|
20 | | final String _lineEnding;
|
21 | |
|
22 | | /// The number of characters allowed in a single line.
|
23 | | final int pageWidth;
|
24 | |
|
25 | | /// The number of characters of additional indentation to apply to each line.
|
26 | | ///
|
27 | | /// This is used when formatting blocks to get the output into the right
|
28 | | /// column based on where the block appears.
|
29 | | final int _blockIndentation;
|
30 | |
|
31 | | /// The cache of blocks that have already been formatted.
|
32 | | final Map<_BlockKey, FormatResult> _blockCache;
|
33 | |
|
34 | | /// The offset in [_buffer] where the selection starts in the formatted code.
|
35 | | ///
|
36 | | /// This will be `null` if there is no selection or the writer hasn't reached
|
37 | | /// the beginning of the selection yet.
|
38 | | int _selectionStart;
|
39 | |
|
40 | | /// The offset in [_buffer] where the selection ends in the formatted code.
|
41 | | ///
|
42 | | /// This will be `null` if there is no selection or the writer hasn't reached
|
43 | | /// the end of the selection yet.
|
44 | | int _selectionEnd;
|
45 | |
|
46 | | /// The number of characters that have been written to the output.
|
47 | 3 | int get length => _buffer.length;
|
48 | |
|
49 | 3 | LineWriter(DartFormatter formatter, this._chunks)
|
50 | 3 | : _lineEnding = formatter.lineEnding,
|
51 | 3 | pageWidth = formatter.pageWidth,
|
52 | | _blockIndentation = 0,
|
53 | 3 | _blockCache = {};
|
54 | |
|
55 | | /// Creates a line writer for a block.
|
56 | 2 | LineWriter._(this._chunks, this._lineEnding, this.pageWidth,
|
57 | | this._blockIndentation, this._blockCache) {
|
58 | | // There is always a newline after the opening delimiter.
|
59 | 6 | _buffer.write(_lineEnding);
|
60 | | }
|
61 | |
|
62 | | /// Gets the results of formatting the child block of [chunk] at with
|
63 | | /// starting [column].
|
64 | | ///
|
65 | | /// If that block has already been formatted, reuses the results.
|
66 | | ///
|
67 | | /// The column is the column for the delimiters. The contents of the block
|
68 | | /// are always implicitly one level deeper than that.
|
69 | | ///
|
70 | | /// main() {
|
71 | | /// function(() {
|
72 | | /// block;
|
73 | | /// });
|
74 | | /// }
|
75 | | ///
|
76 | | /// When we format the anonymous lambda, [column] will be 2, not 4.
|
77 | 2 | FormatResult formatBlock(Chunk chunk, int column) {
|
78 | 2 | var key = new _BlockKey(chunk, column);
|
79 | |
|
80 | | // Use the cached one if we have it.
|
81 | 4 | var cached = _blockCache[key];
|
82 | | if (cached != null) return cached;
|
83 | |
|
84 | 2 | var writer = new LineWriter._(
|
85 | 10 | chunk.block.chunks, _lineEnding, pageWidth, column, _blockCache);
|
86 | |
|
87 | | // TODO(rnystrom): Passing in an initial indent here is hacky. The
|
88 | | // LineWriter ensures all but the first chunk have a block indent, and this
|
89 | | // handles the first chunk. Do something cleaner.
|
90 | 4 | var result = writer.writeLines(Indent.block, flushLeft: chunk.flushLeft);
|
91 | 4 | return _blockCache[key] = result;
|
92 | | }
|
93 | |
|
94 | | /// Takes all of the chunks and divides them into sublists and line splits
|
95 | | /// each list.
|
96 | | ///
|
97 | | /// Since this is linear and line splitting is worse it's good to feed the
|
98 | | /// line splitter smaller lists of chunks when possible.
|
99 | 3 | FormatResult writeLines(int firstLineIndent,
|
100 | | {bool isCompilationUnit: false, bool flushLeft: false}) {
|
101 | | // Now that we know what hard splits there will be, break the chunks into
|
102 | | // independently splittable lines.
|
103 | | var newlines = 0;
|
104 | | var indent = firstLineIndent;
|
105 | | var totalCost = 0;
|
106 | | var start = 0;
|
107 | |
|
108 | 12 | for (var i = 0; i < _chunks.length; i++) {
|
109 | 6 | var chunk = _chunks[i];
|
110 | 3 | if (!chunk.canDivide) continue;
|
111 | |
|
112 | 2 | totalCost +=
|
113 | 4 | _completeLine(newlines, indent, start, i + 1, flushLeft: flushLeft);
|
114 | |
|
115 | | // Get ready for the next line.
|
116 | 2 | newlines = chunk.isDouble ? 2 : 1;
|
117 | 2 | indent = chunk.indent;
|
118 | 2 | flushLeft = chunk.flushLeft;
|
119 | 2 | start = i + 1;
|
120 | | }
|
121 | |
|
122 | 9 | if (start < _chunks.length) {
|
123 | 12 | totalCost += _completeLine(newlines, indent, start, _chunks.length,
|
124 | | flushLeft: flushLeft);
|
125 | | }
|
126 | |
|
127 | | // Be a good citizen, end with a newline.
|
128 | 9 | if (isCompilationUnit) _buffer.write(_lineEnding);
|
129 | |
|
130 | 3 | return new FormatResult(
|
131 | 12 | _buffer.toString(), totalCost, _selectionStart, _selectionEnd);
|
132 | | }
|
133 | |
|
134 | | /// Takes the chunks from [start] to [end] with leading [indent], removes
|
135 | | /// them, and runs the [LineSplitter] on them.
|
136 | 3 | int _completeLine(int newlines, int indent, int start, int end,
|
137 | | {bool flushLeft}) {
|
138 | | // Write the newlines required by the previous line.
|
139 | 5 | for (var j = 0; j < newlines; j++) {
|
140 | 6 | _buffer.write(_lineEnding);
|
141 | | }
|
142 | |
|
143 | 6 | var chunks = _chunks.sublist(start, end);
|
144 | |
|
145 | | if (debug.traceLineWriter) {
|
146 | 0 | debug.log(debug.green("\nWriting:"));
|
147 | 0 | debug.dumpChunks(0, chunks);
|
148 | 0 | debug.log();
|
149 | | }
|
150 | |
|
151 | | // Run the line splitter.
|
152 | 6 | var splitter = new LineSplitter(this, chunks, _blockIndentation, indent,
|
153 | | flushLeft: flushLeft);
|
154 | 3 | var splits = splitter.apply();
|
155 | |
|
156 | | // Write the indentation of the first line.
|
157 | | if (!flushLeft) {
|
158 | 15 | _buffer.write(" " * (indent + _blockIndentation));
|
159 | | }
|
160 | |
|
161 | | // Write each chunk with the appropriate splits between them.
|
162 | 9 | for (var i = 0; i < chunks.length; i++) {
|
163 | 3 | var chunk = chunks[i];
|
164 | 3 | _writeChunk(chunk);
|
165 | |
|
166 | 3 | if (chunk.isBlock) {
|
167 | 2 | if (!splits.shouldSplitAt(i)) {
|
168 | | // This block didn't split (which implies none of the child blocks
|
169 | | // of that block split either, recursively), so write them all inline.
|
170 | 2 | _writeChunksUnsplit(chunk);
|
171 | | } else {
|
172 | | // Include the formatted block contents.
|
173 | 4 | var block = formatBlock(chunk, splits.getColumn(i));
|
174 | |
|
175 | | // If this block contains one of the selection markers, tell the
|
176 | | // writer where it ended up in the final output.
|
177 | 2 | if (block.selectionStart != null) {
|
178 | 4 | _selectionStart = length + block.selectionStart;
|
179 | | }
|
180 | |
|
181 | 2 | if (block.selectionEnd != null) {
|
182 | 4 | _selectionEnd = length + block.selectionEnd;
|
183 | | }
|
184 | |
|
185 | 6 | _buffer.write(block.text);
|
186 | | }
|
187 | | }
|
188 | |
|
189 | 9 | if (i == chunks.length - 1) {
|
190 | | // Don't write trailing whitespace after the last chunk.
|
191 | 3 | } else if (splits.shouldSplitAt(i)) {
|
192 | 6 | _buffer.write(_lineEnding);
|
193 | 8 | if (chunk.isDouble) _buffer.write(_lineEnding);
|
194 | |
|
195 | 8 | _buffer.write(" " * (splits.getColumn(i)));
|
196 | | } else {
|
197 | 9 | if (chunk.spaceWhenUnsplit) _buffer.write(" ");
|
198 | | }
|
199 | | }
|
200 | |
|
201 | 3 | return splits.cost;
|
202 | | }
|
203 | |
|
204 | | /// Writes the block chunks of [chunk] (and any child chunks of them,
|
205 | | /// recursively) without any splitting.
|
206 | 2 | void _writeChunksUnsplit(Chunk chunk) {
|
207 | 2 | if (!chunk.isBlock) return;
|
208 | |
|
209 | 6 | for (var blockChunk in chunk.block.chunks) {
|
210 | 2 | _writeChunk(blockChunk);
|
211 | |
|
212 | 6 | if (blockChunk.spaceWhenUnsplit) _buffer.write(" ");
|
213 | |
|
214 | | // Recurse into the block.
|
215 | 2 | _writeChunksUnsplit(blockChunk);
|
216 | | }
|
217 | | }
|
218 | |
|
219 | | /// Writes [chunk] to the output and updates the selection if the chunk
|
220 | | /// contains a selection marker.
|
221 | 3 | void _writeChunk(Chunk chunk) {
|
222 | 3 | if (chunk.selectionStart != null) {
|
223 | 4 | _selectionStart = length + chunk.selectionStart;
|
224 | | }
|
225 | |
|
226 | 3 | if (chunk.selectionEnd != null) {
|
227 | 4 | _selectionEnd = length + chunk.selectionEnd;
|
228 | | }
|
229 | |
|
230 | 9 | _buffer.write(chunk.text);
|
231 | | }
|
232 | | }
|
233 | |
|
234 | | /// Key type for the formatted block cache.
|
235 | | ///
|
236 | | /// To cache formatted blocks, we just need to know which block it is (the
|
237 | | /// index of its parent chunk) and how far it was indented when we formatted it
|
238 | | /// (the starting column).
|
239 | | class _BlockKey {
|
240 | | /// The index of the chunk in the surrounding chunk list that contains this
|
241 | | /// block.
|
242 | | final Chunk chunk;
|
243 | |
|
244 | | /// The absolute zero-based column number where the block starts.
|
245 | | final int column;
|
246 | |
|
247 | 2 | _BlockKey(this.chunk, this.column);
|
248 | |
|
249 | 2 | bool operator ==(other) {
|
250 | 2 | if (other is! _BlockKey) return false;
|
251 | 12 | return chunk == other.chunk && column == other.column;
|
252 | | }
|
253 | |
|
254 | 12 | int get hashCode => chunk.hashCode ^ column.hashCode;
|
255 | | }
|
256 | |
|
257 | | /// The result of formatting a series of chunks.
|
258 | | class FormatResult {
|
259 | | /// The resulting formatted text, including newlines and leading whitespace
|
260 | | /// to reach the proper column.
|
261 | | final String text;
|
262 | |
|
263 | | /// The numeric cost of the chosen solution.
|
264 | | final int cost;
|
265 | |
|
266 | | /// Where in the resulting buffer the selection starting point should appear
|
267 | | /// if it was contained within this split list of chunks.
|
268 | | ///
|
269 | | /// Otherwise, this is `null`.
|
270 | | final int selectionStart;
|
271 | |
|
272 | | /// Where in the resulting buffer the selection end point should appear if it
|
273 | | /// was contained within this split list of chunks.
|
274 | | ///
|
275 | | /// Otherwise, this is `null`.
|
276 | | final int selectionEnd;
|
277 | |
|
278 | 3 | FormatResult(this.text, this.cost, this.selectionStart, this.selectionEnd);
|
279 | | }
|