Support extracting selected statements into a new Flutter widget.
R=brianwilkerson@google.com
Bug: https://github.com/flutter/flutter-intellij/issues/2283
Change-Id: I778f5b06e0f995026219bee32c4a2253aa6595c5
Reviewed-on: https://dart-review.googlesource.com/57704
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
diff --git a/pkg/analysis_server/lib/src/edit/edit_domain.dart b/pkg/analysis_server/lib/src/edit/edit_domain.dart
index 20397a1..1fd145f 100644
--- a/pkg/analysis_server/lib/src/edit/edit_domain.dart
+++ b/pkg/analysis_server/lib/src/edit/edit_domain.dart
@@ -586,7 +586,7 @@
}
// Try EXTRACT_WIDGETS.
if (new ExtractWidgetRefactoring(
- searchEngine, analysisSession, unit, offset)
+ searchEngine, analysisSession, unit, offset, length)
.isAvailable()) {
kinds.add(RefactoringKind.EXTRACT_WIDGET);
}
@@ -936,7 +936,7 @@
if (unit != null) {
var analysisSession = server.getAnalysisDriver(file).currentSession;
refactoring = new ExtractWidgetRefactoring(
- searchEngine, analysisSession, unit, offset);
+ searchEngine, analysisSession, unit, offset, length);
feedback = new ExtractWidgetFeedback();
}
}
diff --git a/pkg/analysis_server/lib/src/services/refactoring/extract_widget.dart b/pkg/analysis_server/lib/src/services/refactoring/extract_widget.dart
index 9e1b9c8..1ad85d0 100644
--- a/pkg/analysis_server/lib/src/services/refactoring/extract_widget.dart
+++ b/pkg/analysis_server/lib/src/services/refactoring/extract_widget.dart
@@ -31,6 +31,7 @@
final AnalysisSessionHelper sessionHelper;
final CompilationUnit unit;
final int offset;
+ final int length;
CompilationUnitElement unitElement;
LibraryElement libraryElement;
@@ -58,6 +59,12 @@
/// The widget creation expression to extract.
InstanceCreationExpression _expression;
+ /// The statements covered by [offset] and [length] to extract.
+ List<Statement> _statements;
+
+ /// The [SourceRange] that covers [_statements].
+ SourceRange _statementsRange;
+
/// The method returning widget to extract.
MethodDeclaration _method;
@@ -66,8 +73,8 @@
/// and [_method] parameters.
List<_Parameter> _parameters = [];
- ExtractWidgetRefactoringImpl(
- this.searchEngine, AnalysisSession session, this.unit, this.offset)
+ ExtractWidgetRefactoringImpl(this.searchEngine, AnalysisSession session,
+ this.unit, this.offset, this.length)
: sessionHelper = new AnalysisSessionHelper(session) {
unitElement = unit.element;
libraryElement = unitElement.library;
@@ -95,8 +102,10 @@
return result;
}
- _enclosingUnitMember = (_expression ?? _method).getAncestor(
- (n) => n is CompilationUnitMember && n.parent is CompilationUnit);
+ AstNode astNode = _expression ?? _method ?? _statements.first;
+ _enclosingUnitMember = astNode.getAncestor((n) {
+ return n is CompilationUnitMember && n.parent is CompilationUnit;
+ });
result.addStatus(await _initializeClasses());
result.addStatus(await _initializeParameters());
@@ -136,6 +145,12 @@
builder.addReplacement(range.node(_expression), (builder) {
_writeWidgetInstantiation(builder);
});
+ } else if (_statements != null) {
+ builder.addReplacement(_statementsRange, (builder) {
+ builder.write('return ');
+ _writeWidgetInstantiation(builder);
+ builder.write(';');
+ });
} else {
_removeMethodDeclaration(builder);
_replaceInvocationsWithInstantiations(builder);
@@ -156,7 +171,7 @@
/// Checks if [offset] is a widget creation expression that can be extracted.
RefactoringStatus _checkSelection() {
- AstNode node = new NodeLocator2(offset, offset).searchWithin(unit);
+ AstNode node = new NodeLocator2(offset, offset + length).searchWithin(unit);
// Find the enclosing class.
_enclosingClassNode = node?.getAncestor((n) => n is ClassDeclaration);
@@ -169,6 +184,30 @@
return new RefactoringStatus();
}
+ // Block with selected statements.
+ if (node is Block) {
+ var selectionRange = new SourceRange(offset, length);
+ var statements = <Statement>[];
+ for (var statement in node.statements) {
+ var statementRange = range.node(statement);
+ if (statementRange.intersects(selectionRange)) {
+ statements.add(statement);
+ }
+ }
+ if (statements.isNotEmpty) {
+ var lastStatement = statements.last;
+ if (lastStatement is ReturnStatement &&
+ isWidgetExpression(lastStatement.expression)) {
+ _statements = statements;
+ _statementsRange = range.startEnd(statements.first, statements.last);
+ return new RefactoringStatus();
+ } else {
+ return new RefactoringStatus.fatal(
+ 'The last selected statement must return a widget.');
+ }
+ }
+ }
+
// Widget myMethod(...) { ... }
for (; node != null; node = node.parent) {
if (node is FunctionBody) {
@@ -229,6 +268,13 @@
collector = new _ParametersCollector(_enclosingClassElement, localRange);
_expression.accept(collector);
}
+ if (_statements != null) {
+ collector =
+ new _ParametersCollector(_enclosingClassElement, _statementsRange);
+ for (var statement in _statements) {
+ statement.accept(collector);
+ }
+ }
if (_method != null) {
SourceRange localRange = range.node(_method);
collector = new _ParametersCollector(_enclosingClassElement, localRange);
@@ -300,6 +346,11 @@
builder.addDeletion(linesRange);
}
+ String _replaceIndent(String code, String indentOld, String indentNew) {
+ var regExp = new RegExp('^$indentOld', multiLine: true);
+ return code.replaceAll(regExp, indentNew);
+ }
+
/// Replace invocations of the [_method] with instantiations of the new
/// widget class.
void _replaceInvocationsWithInstantiations(DartFileEditBuilder builder) {
@@ -416,8 +467,7 @@
String indentNew = ' ';
String code = utils.getNodeText(_expression);
- code = code.replaceAll(
- new RegExp('^$indentOld', multiLine: true), indentNew);
+ code = _replaceIndent(code, indentOld, indentNew);
builder.writeln('{');
@@ -426,6 +476,20 @@
builder.writeln(';');
builder.writeln(' }');
+ } else if (_statements != null) {
+ String indentOld = utils.getLinePrefix(_statementsRange.offset);
+ String indentNew = ' ';
+
+ String code = utils.getRangeText(_statementsRange);
+ code = _replaceIndent(code, indentOld, indentNew);
+
+ builder.writeln('{');
+
+ builder.write(indentNew);
+ builder.write(code);
+ builder.writeln();
+
+ builder.writeln(' }');
} else {
String code = utils.getNodeText(_method.body);
builder.writeln(code);
@@ -531,6 +595,7 @@
}
}
}
+ // TODO(scheglov) support for ParameterElement
if (type != null && uniqueElements.add(element)) {
parameters.add(new _Parameter(elementName, type));
diff --git a/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart b/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
index 8808121..f99d9db 100644
--- a/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
+++ b/pkg/analysis_server/lib/src/services/refactoring/refactoring.dart
@@ -243,9 +243,9 @@
* Returns a new [ExtractWidgetRefactoring] instance.
*/
factory ExtractWidgetRefactoring(SearchEngine searchEngine,
- AnalysisSession session, CompilationUnit unit, int offset) {
+ AnalysisSession session, CompilationUnit unit, int offset, int length) {
return new ExtractWidgetRefactoringImpl(
- searchEngine, session, unit, offset);
+ searchEngine, session, unit, offset, length);
}
/**
diff --git a/pkg/analysis_server/test/services/refactoring/extract_widget_test.dart b/pkg/analysis_server/test/services/refactoring/extract_widget_test.dart
index 08e01e3..0573853 100644
--- a/pkg/analysis_server/test/services/refactoring/extract_widget_test.dart
+++ b/pkg/analysis_server/test/services/refactoring/extract_widget_test.dart
@@ -1081,6 +1081,95 @@
expect(refactoring.refactoringName, 'Extract Widget');
}
+ test_statements() async {
+ addFlutterPackage();
+ await indexTestUnit(r'''
+import 'package:flutter/material.dart';
+
+Widget main() {
+ var index = 0;
+ var a = 'a $index';
+// start
+ var b = 'b $index';
+ return new Row(
+ children: <Widget>[
+ new Text(a),
+ new Text(b),
+ ],
+ );
+// end
+}
+''');
+ _createRefactoringForStartEnd();
+
+ await _assertSuccessfulRefactoring(r'''
+import 'package:flutter/material.dart';
+
+Widget main() {
+ var index = 0;
+ var a = 'a $index';
+// start
+ return new Test(index: index, a: a);
+// end
+}
+
+class Test extends StatelessWidget {
+ const Test({
+ Key key,
+ @required this.index,
+ @required this.a,
+ }) : super(key: key);
+
+ final int index;
+ final String a;
+
+ @override
+ Widget build(BuildContext context) {
+ var b = 'b $index';
+ return new Row(
+ children: <Widget>[
+ new Text(a),
+ new Text(b),
+ ],
+ );
+ }
+}
+''');
+ }
+
+ test_statements_BAD_emptySelection() async {
+ addFlutterPackage();
+ await indexTestUnit(r'''
+import 'package:flutter/material.dart';
+
+void main() {
+// start
+// end
+}
+''');
+ _createRefactoringForStartEnd();
+
+ assertRefactoringStatus(await refactoring.checkInitialConditions(),
+ RefactoringProblemSeverity.FATAL);
+ }
+
+ test_statements_BAD_notReturnStatement() async {
+ addFlutterPackage();
+ await indexTestUnit(r'''
+import 'package:flutter/material.dart';
+
+void main() {
+// start
+ new Text('text');
+// end
+}
+''');
+ _createRefactoringForStartEnd();
+
+ assertRefactoringStatus(await refactoring.checkInitialConditions(),
+ RefactoringProblemSeverity.FATAL);
+ }
+
Future<void> _assertRefactoringChange(String expectedCode) async {
SourceChange refactoringChange = await refactoring.createChange();
this.refactoringChange = refactoringChange;
@@ -1096,18 +1185,24 @@
await _assertRefactoringChange(expectedCode);
}
- void _createRefactoring(int offset) {
+ void _createRefactoring(int offset, int length) {
refactoring = new ExtractWidgetRefactoring(
- searchEngine, driver.currentSession, testUnit, offset);
+ searchEngine, driver.currentSession, testUnit, offset, length);
refactoring.name = 'Test';
}
+ void _createRefactoringForStartEnd() {
+ int offset = findOffset('// start\n') + '// start\n'.length;
+ int length = findOffset('// end') - offset;
+ _createRefactoring(offset, length);
+ }
+
/**
* Creates a new refactoring in [refactoring] at the offset of the given
* [search] pattern.
*/
void _createRefactoringForStringOffset(String search) {
int offset = findOffset(search);
- _createRefactoring(offset);
+ _createRefactoring(offset, 0);
}
}