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);
   }
 }