Index: observatory_pub_packages/analyzer/src/services/writer.dart |
=================================================================== |
--- observatory_pub_packages/analyzer/src/services/writer.dart (revision 0) |
+++ observatory_pub_packages/analyzer/src/services/writer.dart (working copy) |
@@ -0,0 +1,518 @@ |
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
+// for details. All rights reserved. Use of this source code is governed by a |
+// BSD-style license that can be found in the LICENSE file. |
+ |
+library source_writer; |
+ |
+import 'dart:math' as math; |
+ |
+class Line { |
+ |
+ final List<LineToken> tokens = <LineToken>[]; |
+ final bool useTabs; |
+ final int spacesPerIndent; |
+ final int indentLevel; |
+ final LinePrinter printer; |
+ |
+ Line({this.indentLevel: 0, this.useTabs: false, this.spacesPerIndent: 2, |
+ this.printer: const SimpleLinePrinter()}) { |
+ if (indentLevel > 0) { |
+ indent(indentLevel); |
+ } |
+ } |
+ |
+ void addSpace() { |
+ addSpaces(1); |
+ } |
+ |
+ void addSpaces(int n, {breakWeight: DEFAULT_SPACE_WEIGHT}) { |
+ tokens.add(new SpaceToken(n, breakWeight: breakWeight)); |
+ } |
+ |
+ void addToken(LineToken token) { |
+ tokens.add(token); |
+ } |
+ |
+ void clear() { |
+ tokens.clear(); |
+ } |
+ |
+ bool isEmpty() => tokens.isEmpty; |
+ |
+ bool isWhitespace() => tokens.every( |
+ (tok) => tok is SpaceToken || tok is TabToken); |
+ |
+ void indent(int n) { |
+ tokens.insert(0, |
+ useTabs ? new TabToken(n) : new SpaceToken(n * spacesPerIndent)); |
+ } |
+ |
+ String toString() => printer.printLine(this); |
+ |
+} |
+ |
+ |
+/// Base class for line printers |
+abstract class LinePrinter { |
+ |
+ const LinePrinter(); |
+ |
+ /// Convert this [line] to a [String] representation. |
+ String printLine(Line line); |
+} |
+ |
+ |
+typedef String Indenter(int n); |
+ |
+ |
+/// A simple line breaking [LinePrinter] |
+class SimpleLineBreaker extends LinePrinter { |
+ |
+ static final NO_OP_INDENTER = (n) => ''; |
+ |
+ final chunks = <Chunk>[]; |
+ final int maxLength; |
+ Indenter indenter; |
+ |
+ SimpleLineBreaker(this.maxLength, [this.indenter]) { |
+ if (indenter == null) { |
+ indenter = NO_OP_INDENTER; |
+ } |
+ } |
+ |
+ String printLine(Line line) { |
+ var buf = new StringBuffer(); |
+ var chunks = breakLine(line); |
+ for (var i = 0; i < chunks.length; ++i) { |
+ var chunk = chunks[i]; |
+ if (i > 0) { |
+ buf.write(indent(chunk, chunk.indent)); |
+ } else { |
+ buf.write(chunk); |
+ } |
+ } |
+ return buf.toString(); |
+ } |
+ |
+ String indent(Chunk chunk, int level) { |
+ return '\n' + indenter(level) + chunk.toString(); |
+ } |
+ |
+ List<Chunk> breakLine(Line line) { |
+ List<LineToken> tokens = preprocess(line.tokens); |
+ List<Chunk> chunks = <Chunk>[new Chunk(line.indentLevel, maxLength, tokens)]; |
+ // try SINGLE_SPACE_WEIGHT |
+ { |
+ Chunk chunk = chunks[0]; |
+ if (chunk.length > maxLength) { |
+ for (int i = 0; i < tokens.length; i++) { |
+ LineToken token = tokens[i]; |
+ if (token is SpaceToken && token.breakWeight == SINGLE_SPACE_WEIGHT) { |
+ var beforeChunk = chunk.subChunk(chunk.indent, 0, i); |
+ var restChunk = chunk.subChunk(chunk.indent + 2, i + 1); |
+ // check if 'init' in 'var v = init;' fits a line |
+ if (restChunk.length < maxLength) { |
+ return [beforeChunk, restChunk]; |
+ } |
+ // check if 'var v = method(' in 'var v = method(args)' does not fit |
+ int weight = chunk.findMinSpaceWeight(); |
+ if (chunk.getLengthToSpaceWithWeight(weight) > maxLength) { |
+ chunks = [beforeChunk, restChunk]; |
+ } |
+ // done anyway |
+ break; |
+ } |
+ } |
+ } |
+ } |
+ // other spaces |
+ while (true) { |
+ List<Chunk> newChunks = <Chunk>[]; |
+ bool hasChanges = false; |
+ for (Chunk chunk in chunks) { |
+ tokens = chunk.tokens; |
+ if (chunk.length > maxLength) { |
+ if (chunk.hasAnySpace()) { |
+ int weight = chunk.findMinSpaceWeight(); |
+ int newIndent = chunk.indent; |
+ if (weight == DEFAULT_SPACE_WEIGHT) { |
+ int start = 0; |
+ int length = 0; |
+ for (int i = 0; i < tokens.length; i++) { |
+ LineToken token = tokens[i]; |
+ if (token is SpaceToken && token.breakWeight == weight && |
+ i < tokens.length - 1) { |
+ LineToken nextToken = tokens[i + 1]; |
+ if (length + token.length + nextToken.length > maxLength) { |
+ newChunks.add(chunk.subChunk(newIndent, start, i)); |
+ newIndent = chunk.indent + 2; |
+ start = i + 1; |
+ length = 0; |
+ continue; |
+ } |
+ } |
+ length += token.length; |
+ } |
+ if (start < tokens.length) { |
+ newChunks.add(chunk.subChunk(newIndent, start)); |
+ } |
+ } else { |
+ List<LineToken> part = []; |
+ int start = 0; |
+ for (int i = 0; i < tokens.length; i++) { |
+ LineToken token = tokens[i]; |
+ if (token is SpaceToken && token.breakWeight == weight) { |
+ newChunks.add(chunk.subChunk(newIndent, start, i)); |
+ newIndent = chunk.indent + 2; |
+ start = i + 1; |
+ } |
+ } |
+ if (start < tokens.length) { |
+ newChunks.add(chunk.subChunk(newIndent, start)); |
+ } |
+ } |
+ } else { |
+ newChunks.add(chunk); |
+ } |
+ } else { |
+ newChunks.add(chunk); |
+ } |
+ if (newChunks.length > chunks.length) { |
+ hasChanges = true; |
+ } |
+ } |
+ if (!hasChanges) { |
+ break; |
+ } |
+ chunks = newChunks; |
+ } |
+ return chunks; |
+ } |
+ |
+ static List<LineToken> preprocess(List<LineToken> tok) { |
+ |
+ var tokens = <LineToken>[]; |
+ var curr; |
+ |
+ tok.forEach((token) { |
+ if (token is! SpaceToken) { |
+ if (curr == null) { |
+ curr = token; |
+ } else { |
+ curr = merge(curr, token); |
+ } |
+ } else { |
+ if (isNonbreaking(token)) { |
+ curr = merge(curr, token); |
+ } else { |
+ if (curr != null) { |
+ tokens.add(curr); |
+ curr = null; |
+ } |
+ tokens.add(token); |
+ } |
+ } |
+ }); |
+ |
+ if (curr != null) { |
+ tokens.add(curr); |
+ } |
+ |
+ return tokens; |
+ } |
+ |
+ static bool isNonbreaking(SpaceToken token) => |
+ token.breakWeight == UNBREAKABLE_SPACE_WEIGHT; |
+ |
+ static LineToken merge(LineToken first, LineToken second) => |
+ new LineToken(first.value + second.value); |
+} |
+ |
+/// Test if this [string] contains only whitespace characters |
+bool isWhitespace(String string) => string.codeUnits.every( |
+ (c) => c == 0x09 || c == 0x20 || c == 0x0A || c == 0x0D); |
+ |
+/// Special token indicating a line start |
+final LINE_START = new SpaceToken(0); |
+ |
+const DEFAULT_SPACE_WEIGHT = UNBREAKABLE_SPACE_WEIGHT - 1; |
+/// The weight of a space after '=' in variable declaration or assignment |
+const SINGLE_SPACE_WEIGHT = UNBREAKABLE_SPACE_WEIGHT - 2; |
+const UNBREAKABLE_SPACE_WEIGHT = 100000000; |
+ |
+/// Simple non-breaking printer |
+class SimpleLinePrinter extends LinePrinter { |
+ |
+ const SimpleLinePrinter(); |
+ |
+ String printLine(Line line) { |
+ var buffer = new StringBuffer(); |
+ line.tokens.forEach((tok) => buffer.write(tok.toString())); |
+ return buffer.toString(); |
+ } |
+ |
+} |
+ |
+ |
+/// Describes a piece of text in a [Line]. |
+abstract class LineText { |
+ int get length; |
+} |
+ |
+ |
+/// A working piece of text used in calculating line breaks |
+class Chunk { |
+ final int indent; |
+ final int maxLength; |
+ final List<LineToken> tokens = <LineToken>[]; |
+ |
+ Chunk(this.indent, this.maxLength, [List<LineToken> tokens]) { |
+ this.tokens.addAll(tokens); |
+ } |
+ |
+ int get length { |
+ return tokens.fold(0, (len, token) => len + token.length); |
+ } |
+ |
+ int getLengthToSpaceWithWeight(int weight) { |
+ int length = 0; |
+ for (LineToken token in tokens) { |
+ if (token is SpaceToken && token.breakWeight == weight) { |
+ break; |
+ } |
+ length += token.length; |
+ } |
+ return length; |
+ } |
+ |
+ bool fits(LineToken a, LineToken b) { |
+ return length + a.length + a.length <= maxLength; |
+ } |
+ |
+ void add(LineToken token) { |
+ tokens.add(token); |
+ } |
+ |
+ bool hasInitializerSpace() { |
+ return tokens.any((token) { |
+ return token is SpaceToken && token.breakWeight == SINGLE_SPACE_WEIGHT; |
+ }); |
+ } |
+ |
+ bool hasAnySpace() { |
+ return tokens.any((token) => token is SpaceToken); |
+ } |
+ |
+ int findMinSpaceWeight() { |
+ int minWeight = UNBREAKABLE_SPACE_WEIGHT; |
+ for (var token in tokens) { |
+ if (token is SpaceToken) { |
+ minWeight = math.min(minWeight, token.breakWeight); |
+ } |
+ } |
+ return minWeight; |
+ } |
+ |
+ Chunk subChunk(int indentLevel, int start, [int end]) { |
+ List<LineToken> subTokens = tokens.sublist(start, end); |
+ return new Chunk(indentLevel, maxLength, subTokens); |
+ } |
+ |
+ String toString() => tokens.join(); |
+} |
+ |
+ |
+class LineToken implements LineText { |
+ |
+ final String value; |
+ |
+ LineToken(this.value); |
+ |
+ String toString() => value; |
+ |
+ int get length => lengthLessNewlines(value); |
+ |
+ int lengthLessNewlines(String str) => |
+ str.endsWith('\n') ? str.length - 1 : str.length; |
+ |
+} |
+ |
+ |
+class SpaceToken extends LineToken { |
+ |
+ final int breakWeight; |
+ |
+ SpaceToken(int n, {this.breakWeight: DEFAULT_SPACE_WEIGHT}) : |
+ super(getSpaces(n)); |
+} |
+ |
+ |
+class TabToken extends LineToken { |
+ |
+ TabToken(int n) : super(getTabs(n)); |
+} |
+ |
+ |
+class NewlineToken extends LineToken { |
+ |
+ NewlineToken(String value) : super(value); |
+} |
+ |
+ |
+class SourceWriter { |
+ |
+ final StringBuffer buffer = new StringBuffer(); |
+ Line currentLine; |
+ |
+ final String lineSeparator; |
+ int indentCount = 0; |
+ final int spacesPerIndent; |
+ final bool useTabs; |
+ |
+ LinePrinter linePrinter; |
+ LineToken _lastToken; |
+ |
+ SourceWriter({this.indentCount: 0, this.lineSeparator: NEW_LINE, |
+ this.useTabs: false, this.spacesPerIndent: 2, int maxLineLength: 80}) { |
+ if (maxLineLength > 0) { |
+ linePrinter = new SimpleLineBreaker(maxLineLength, (n) => |
+ getIndentString(n, useTabs: useTabs, spacesPerIndent: spacesPerIndent)); |
+ } else { |
+ linePrinter = new SimpleLinePrinter(); |
+ } |
+ currentLine = newLine(); |
+ } |
+ |
+ LineToken get lastToken => _lastToken; |
+ |
+ _addToken(LineToken token) { |
+ _lastToken = token; |
+ currentLine.addToken(token); |
+ } |
+ |
+ void indent() { |
+ ++indentCount; |
+ // Rather than fiddle with deletions/insertions just start fresh |
+ if (currentLine.isWhitespace()) { |
+ currentLine = newLine(); |
+ } |
+ } |
+ |
+ void newline() { |
+ if (currentLine.isWhitespace()) { |
+ currentLine.tokens.clear(); |
+ } |
+ _addToken(new NewlineToken(this.lineSeparator)); |
+ buffer.write(currentLine.toString()); |
+ currentLine = newLine(); |
+ } |
+ |
+ void newlines(int num) { |
+ while (num-- > 0) { |
+ newline(); |
+ } |
+ } |
+ |
+ void write(String string) { |
+ var lines = string.split(lineSeparator); |
+ var length = lines.length; |
+ for (int i = 0; i < length; i++) { |
+ var line = lines[i]; |
+ _addToken(new LineToken(line)); |
+ if (i != length - 1) { |
+ newline(); |
+ // no indentation for multi-line strings |
+ currentLine.clear(); |
+ } |
+ } |
+ } |
+ |
+ void writeln(String s) { |
+ write(s); |
+ newline(); |
+ } |
+ |
+ void space() { |
+ spaces(1); |
+ } |
+ |
+ void spaces(n, {breakWeight: DEFAULT_SPACE_WEIGHT}) { |
+ currentLine.addSpaces(n, breakWeight: breakWeight); |
+ } |
+ |
+ void unindent() { |
+ --indentCount; |
+ // Rather than fiddle with deletions/insertions just start fresh |
+ if (currentLine.isWhitespace()) { |
+ currentLine = newLine(); |
+ } |
+ } |
+ |
+ Line newLine() => new Line(indentLevel: indentCount, useTabs: useTabs, |
+ spacesPerIndent: spacesPerIndent, printer: linePrinter); |
+ |
+ String toString() { |
+ var source = new StringBuffer(buffer.toString()); |
+ if (!currentLine.isWhitespace()) { |
+ source.write(currentLine); |
+ } |
+ return source.toString(); |
+ } |
+ |
+} |
+ |
+const NEW_LINE = '\n'; |
+const SPACE = ' '; |
+const SPACES = const [ |
+ '', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+ ' ', |
+]; |
+const TABS = const [ |
+ '', |
+ '\t', |
+ '\t\t', |
+ '\t\t\t', |
+ '\t\t\t\t', |
+ '\t\t\t\t\t', |
+ '\t\t\t\t\t\t', |
+ '\t\t\t\t\t\t\t', |
+ '\t\t\t\t\t\t\t\t', |
+ '\t\t\t\t\t\t\t\t\t', |
+ '\t\t\t\t\t\t\t\t\t\t', |
+ '\t\t\t\t\t\t\t\t\t\t\t', |
+ '\t\t\t\t\t\t\t\t\t\t\t\t', |
+ '\t\t\t\t\t\t\t\t\t\t\t\t\t', |
+ '\t\t\t\t\t\t\t\t\t\t\t\t\t\t', |
+]; |
+ |
+ |
+String getIndentString(int indentWidth, {bool useTabs: false, |
+ int spacesPerIndent: 2}) => useTabs ? getTabs(indentWidth) : |
+ getSpaces(indentWidth * spacesPerIndent); |
+ |
+String getSpaces(int n) => n < SPACES.length ? SPACES[n] : repeat(' ', n); |
+ |
+String getTabs(int n) => n < TABS.length ? TABS[n] : repeat('\t', n); |
+ |
+String repeat(String ch, int times) { |
+ var sb = new StringBuffer(); |
+ for (var i = 0; i < times; ++i) { |
+ sb.write(ch); |
+ } |
+ return sb.toString(); |
+} |