Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(440)

Unified Diff: pkg/analyzer_experimental/lib/src/services/formatter_impl.dart

Issue 17470004: Dart formatter checkpoint. (Closed) Base URL: http://dart.googlecode.com/svn/branches/bleeding_edge/dart/
Patch Set: Created 7 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: pkg/analyzer_experimental/lib/src/services/formatter_impl.dart
===================================================================
--- pkg/analyzer_experimental/lib/src/services/formatter_impl.dart (revision 24194)
+++ pkg/analyzer_experimental/lib/src/services/formatter_impl.dart (working copy)
@@ -84,21 +84,22 @@
final FormatterOptions options;
final List<AnalysisError> errors = <AnalysisError>[];
scheglov 2013/06/19 22:12:05 Remove type annotation for the final field.
pquitslund 2013/06/20 22:34:52 Done.
+ final EditRecorder recorder;
- CodeFormatterImpl(this.options);
+ CodeFormatterImpl(FormatterOptions options) : this.options = options,
scheglov 2013/06/19 22:12:05 May be "this.options" and remove initializer?
pquitslund 2013/06/20 22:34:52 The trouble is that I'd like to use options in my
+ recorder = new EditRecorder(options);
String format(CodeKind kind, String source, {int offset, int end,
int indentationLevel:0}) {
var start = tokenize(source);
- _checkForErrors();
+ checkForErrors();
var node = parse(kind, start);
- _checkForErrors();
+ checkForErrors();
- // To be continued...
-
- return source;
+ var formatter = new FormattingEngine(options);
+ return formatter.format(source, node, start, kind, recorder);
}
ASTNode parse(CodeKind kind, Token start) {
@@ -115,7 +116,7 @@
throw new FormatterException('Unsupported format kind: $kind');
}
- _checkForErrors() {
+ checkForErrors() {
if (errors.length > 0) {
throw new FormatterException.forError(errors);
}
@@ -132,18 +133,13 @@
}
-/// Placeholder class to hold a reference to the Class object representing
-/// the Dart keyword void.
-class Void extends Object {
-}
-
-
/// Records a sequence of edits to a source string that will cause the string
/// to be formatted when applied.
class EditRecorder {
final FormatterOptions options;
+ final EditStore editStore;
int column = 0;
@@ -152,13 +148,108 @@
Token currentToken;
- int indentationLevel = 0;
int numberOfIndentations = 0;
- bool isIndentNeeded = false;
+ bool needsIndent = false;
- EditRecorder(this.options);
+ EditRecorder(this.options): editStore = new EditStore();
Brian Wilkerson 2013/06/19 21:50:52 Just curiosity, but why did you move away from usi
pquitslund 2013/06/20 22:34:52 Discussed (exhaustively) in person!
+ EditRecorder.forStore(this.options, this.editStore);
+
+ /// Add an [Edit] that describes a textual [replacement] of a text
+ /// interval starting at the given [offset] spanning the given [length].
+ void addEdit(int offset, int length, String replacement) {
+ editStore.addEdit(offset, length, replacement);
+ }
+
+ /// Advance past the given expected [token] (or fail if not matched).
+ void advance(Token token) {
+ if (currentToken.lexeme == token.lexeme) {
+
+ // TODO(pquitslund) emit comments
+// if (needsIndent) {
+// advanceIndent();
+// needsIndent = false;
+// }
+ // Record writing a token at the current edit location
+ advanceChars(token.length);
+ currentToken = currentToken.next;
+ } else {
+ wrongToken(token.lexeme);
+ }
+ }
+
+ /// Move indices past indent, adding an edit if needed to adjust indentation
+ void advanceIndent() {
+// var indentWidth = options.indentPerLevel * indentationLevel;
+// var indentString = getIndentString(indentWidth);
+// var sourceIndentWidth = 0;
+// for (var i = 0; i < source.length; i++) {
+// if (isIndentChar(source[sourceIndex + i])) {
+// sourceIndentWidth += 1;
+// } else {
+// break;
+// }
+// }
+// var hasSameIndent = sourceIndentWidth == indentWidth;
+// if (hasSameIndent) {
+// for (var i = 0; i < indentWidth; i++) {
+// if (source[sourceIndex + i] != indentString[i]) {
+// hasSameIndent = false;
+// break;
+// }
+// }
+// if (hasSameIndent) {
+// advanceChars(indentWidth);
+// return;
+// }
+// }
+// addEdit(sourceIndex, sourceIndentWidth, indentString);
+// column += indentWidth;
+// sourceIndex += sourceIndentWidth;
+
+ var indent = options.indentPerLevel * numberOfIndentations;
+
+ spaces(indent);
+ }
+
+ String getIndentString(int indentWidth) {
+
+ // TODO(pquitslund) a temporary workaround
+ if (indentWidth < 0) {
+ return '';
+ }
+
+ // TODO(pquitslund) allow indent with tab chars
+
+ // Fetch a precomputed indent string
+ if (indentWidth < SPACES.length) {
+ return SPACES[indentWidth];
+ }
+
+ // Build un-precomputed strings dynamically
+ var sb = new StringBuffer();
+ for (var i=0; i < indentWidth; ++i) {
scheglov 2013/06/19 22:12:05 Whitespaces before/after '='.
pquitslund 2013/06/20 22:34:52 Done.
+ sb.write(' ');
+ }
+ return sb.toString();
+ }
+
+ /// Advance past the given expected [token] (or fail if not matched).
+ void advanceToken(String token) {
+ if (currentToken.lexeme == token) {
+ advance(currentToken);
+ } else {
+ wrongToken(token);
+ }
+ }
+
+ /// Advance [column] and [sourceIndex] indices by [len] characters.
+ void advanceChars(int len) {
+ column += len;
+ sourceIndex += len;
+ }
+
/// Count the number of whitespace chars beginning at the current
/// [sourceIndex].
int countWhitespace() {
@@ -173,9 +264,8 @@
return count;
}
- /// Indent.
+ /// Update indent indices.
void indent() {
- indentationLevel += options.indentPerLevel;
numberOfIndentations++;
}
@@ -192,16 +282,98 @@
return true;
}
+ /// Newline.
+ void newline() {
+ // TODO(pquitslund) emit comments
+ needsIndent = true;
+ // If there is a newline before the edit location, do nothing.
+ if (isNewlineAt(sourceIndex - NEW_LINE.length)) {
+ return;
+ }
+ // If there is a newline after the edit location, advance over it.
+ if (isNewlineAt(sourceIndex)) {
+ advanceChars(NEW_LINE.length);
+ return;
+ }
+ // Otherwise, replace whitespace with a newline.
+ var charsToReplace = countWhitespace();
+ if (isNewlineAt(sourceIndex + charsToReplace)) {
+ ++charsToReplace;
Brian Wilkerson 2013/06/19 21:50:52 Does this want to be incremented by NEW_LINE.lengt
pquitslund 2013/06/20 22:34:52 Yes! Good catch. :)
+ }
+ addEdit(sourceIndex, charsToReplace, NEW_LINE);
+ advanceChars(charsToReplace);
+ }
+
+
+ /// Un-indent.
+ void unindent() {
+ numberOfIndentations--;
+ }
+
+ /// Space.
+ void space() {
+ // TODO(pquitslund) emit comments
+// // If there is a space before the edit location, do nothing.
+// if (isSpaceAt(sourceIndex - 1)) {
+// return;
+// }
+// // If there is a space after the edit location, advance over it.
+// if (isSpaceAt(sourceIndex)) {
+// advance(1);
+// return;
+// }
+ // Otherwise, replace spaces with a single space.
+ spaces(1);
+ }
+
+ /// Spaces.
+ void spaces(int num) {
+ var charsToReplace = countWhitespace();
+ addEdit(sourceIndex, charsToReplace, SPACES[num]);
+ advanceChars(charsToReplace);
+ }
+
+ wrongToken(String token) {
+ throw new FormatterException('expected token: "${token}", '
+ 'actual: "${currentToken}"');
+ }
+
+ String toString() =>
+ new EditOperation().apply(editStore.edits,
+ source.substring(0, sourceIndex));
+
}
const SPACE = ' ';
+final SPACES = [
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+];
+
bool isIndentChar(String ch) => ch == SPACE; // TODO(pquitslund) also check tab
/// Manages stored [Edit]s.
class EditStore {
+ const EditStore();
Brian Wilkerson 2013/06/19 21:50:52 I wouldn't make this a 'const' constructor because
pquitslund 2013/06/20 22:34:52 Actually, I think I AM! Honestly, this is an arti
+
/// The underlying sequence of [Edit]s.
final edits = <Edit>[];
@@ -239,7 +411,6 @@
}
-
/// Describes a text edit.
class Edit {
@@ -264,11 +435,156 @@
}
+/// Applies a sequence of [edits] to a [document].
+class EditOperation {
+
+ String apply(List<Edit> edits, String document) {
+
+ var edit;
+ for (var i = edits.length - 1; i >= 0; --i) {
+ edit = edits[i];
+ document = replace(document, edit.offset,
+ edit.offset + edit.length, edit.replacement);
+ }
+
+ return document;
+ }
+
+}
+
+
+String replace(String str, int start, int end, String replacement) =>
+ str.substring(0, start) + replacement + str.substring(end);
+
+
/// An AST visitor that drives formatting heuristics.
-class FormattingEngine extends RecursiveASTVisitor<Void> {
+class FormattingEngine extends RecursiveASTVisitor {
final FormatterOptions options;
+ CodeKind kind;
+ EditRecorder recorder;
+
FormattingEngine(this.options);
+ String format(String source, ASTNode node, Token start, CodeKind kind,
+ EditRecorder recorder) {
+
+ this.kind = kind;
+ this.recorder = recorder;
+
+ recorder..source = source
+ ..currentToken = start;
Brian Wilkerson 2013/06/19 21:50:52 This looks weird. I would have expected the state
pquitslund 2013/06/20 22:34:52 Agreed. Still working on how this entry point sho
+
+ node.accept(this);
+
+ var editor = new EditOperation();
+ return editor.apply(recorder.editStore.edits, source);
+ }
+
+
+ visitClassDeclaration(ClassDeclaration node) {
+
+ recorder.advanceIndent();
+
+ if (node.documentationComment != null) {
+ node.documentationComment.accept(this);
+ }
+
+ recorder..advance(node.classKeyword)..space();
+
+ node.name.accept(this);
+
+ if (node.typeParameters != null) {
+ node.typeParameters.accept(this);
+ }
+ recorder.space();
+
+ if (node.extendsClause != null) {
+ node.extendsClause.accept(this);
+ recorder.space();
+ }
+
Brian Wilkerson 2013/06/19 21:50:52 You missed the withClause.
pquitslund 2013/06/20 22:34:52 Coming real soon. (Driven by accompanying tests.)
+ if (node.implementsClause != null) {
+ node.implementsClause.accept(this);
+ recorder.space();
+ }
+
+ recorder..advance(node.leftBracket)
+ ..indent();
+
+ for (var member in node.members) {
+ recorder.newline();
Brian Wilkerson 2013/06/19 21:50:52 Do you want to advanceIndent() after the newline,
pquitslund 2013/06/20 22:34:52 Actually, yes. That is cleaner. Thanks!
+ member.accept(this);
+ }
+
+ recorder..unindent()
+ ..newline()
+ ..advanceIndent()
+ ..advance(node.rightBracket);
+ }
+
+
+ visitBlockFunctionBody(BlockFunctionBody node) {
+ recorder..advance(node.beginToken)
Brian Wilkerson 2013/06/19 21:50:52 The begin token for a BlockFunctionBody is the '{'
+ ..indent()
+ ..newline();
+ node.block.accept(this);
+ recorder..unindent()
+ ..advanceIndent()
+ ..advance(node.endToken);
+ }
+
+
+ visitBlock(Block block) {
+
+ }
+
+
+ visitExpressionFunctionBody(ExpressionFunctionBody node) {
+ recorder..advance(node.beginToken)
Brian Wilkerson 2013/06/19 21:50:52 Here too: use 'functionDefinition' and 'semicolon'
pquitslund 2013/06/20 22:34:52 Thanks!
+ ..indent()
+ ..newline();
+ node.expression.accept(this);
+ recorder..unindent()
+ ..advanceIndent()
+ ..advance(node.endToken);
+ }
+
+
+ visitMethodDeclaration(MethodDeclaration node) {
+
+ recorder.advanceIndent();
Brian Wilkerson 2013/06/19 21:50:52 The rule for AST nodes is that whitespace before a
pquitslund 2013/06/20 22:34:52 Done.
+
+ if (node.modifierKeyword != null) {
+ recorder.advance(node.modifierKeyword);
+ recorder.space();
+ }
+
+ if (node.returnType != null) {
+ recorder.advance(node.returnType.beginToken);
Brian Wilkerson 2013/06/19 21:50:52 I think you want to visit the returnType at this p
pquitslund 2013/06/20 22:34:52 Done.
+ recorder.space();
+ }
+
+ recorder.advance(node.name.beginToken);
+
+ node.parameters.accept(this);
+
+ recorder.space();
+
+ node.body.accept(this);
+ }
+
+
+ visitFormalParameterList(FormalParameterList node) {
+ recorder.advance(node.beginToken);
+ //...
+ recorder.advance(node.endToken);
+ }
+
+
+ visitSimpleIdentifier(SimpleIdentifier node) {
+ recorder.advance(node.token);
+ }
+
}

Powered by Google App Engine
This is Rietveld 408576698