You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@beam.apache.org by pa...@apache.org on 2022/12/21 20:06:25 UTC

[beam] branch master updated: Refactor focusing to contextLine (#24674)

This is an automated email from the ASF dual-hosted git repository.

pabloem pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 0917978677f Refactor focusing to contextLine (#24674)
0917978677f is described below

commit 0917978677f05385eb2ef0d89a016f4a2aa5c6c1
Author: alexeyinkin <al...@akvelon.com>
AuthorDate: Thu Dec 22 00:06:18 2022 +0400

    Refactor focusing to contextLine (#24674)
    
    * Refactor focusing to contextLine (#24613)
    
    * Rename a widget (#24613)
    
    * Minor reordering (#24613)
    
    * Fix after review (#24613)
---
 .../widgets/embedded_editor.dart                   |   1 -
 .../widgets/editor_textarea_wrapper.dart           |   1 -
 .../controllers/snippet_editing_controller.dart    |  28 ++++
 .../lib/src/models/example_base.dart               |   2 +
 .../lib/src/widgets/editor_textarea.dart           | 178 ---------------------
 .../lib/src/widgets/snippet_editor.dart            | 117 ++++++++++++--
 6 files changed, 133 insertions(+), 194 deletions(-)

diff --git a/playground/frontend/lib/pages/embedded_playground/widgets/embedded_editor.dart b/playground/frontend/lib/pages/embedded_playground/widgets/embedded_editor.dart
index 94000034f29..ac319426c79 100644
--- a/playground/frontend/lib/pages/embedded_playground/widgets/embedded_editor.dart
+++ b/playground/frontend/lib/pages/embedded_playground/widgets/embedded_editor.dart
@@ -37,7 +37,6 @@ class EmbeddedEditor extends StatelessWidget {
     return SnippetEditor(
       controller: snippetController,
       isEditable: isEditable,
-      goToContextLine: false,
     );
   }
 }
diff --git a/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart b/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart
index 65c6e4177c6..57c1ba3fa70 100644
--- a/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart
+++ b/playground/frontend/lib/pages/standalone_playground/widgets/editor_textarea_wrapper.dart
@@ -56,7 +56,6 @@ class CodeTextAreaWrapper extends StatelessWidget {
                   child: SnippetEditor(
                     controller: snippetController,
                     isEditable: true,
-                    goToContextLine: true,
                   ),
                 ),
                 Positioned(
diff --git a/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart b/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart
index 195c0bd1e10..5963837f8c5 100644
--- a/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart
+++ b/playground/frontend/playground_components/lib/src/controllers/snippet_editing_controller.dart
@@ -16,6 +16,8 @@
  * limitations under the License.
  */
 
+import 'dart:math';
+
 import 'package:flutter/widgets.dart';
 import 'package:flutter_code_editor/flutter_code_editor.dart';
 import 'package:get_it/get_it.dart';
@@ -77,6 +79,7 @@ class SnippetEditingController extends ChangeNotifier {
     codeController.removeListener(_onCodeControllerChanged);
     setSource(example.source);
     _applyViewOptions(viewOptions);
+    _toStartOfContextLineIfAny();
     codeController.addListener(_onCodeControllerChanged);
 
     notifyListeners();
@@ -100,6 +103,31 @@ class SnippetEditingController extends ChangeNotifier {
     }
   }
 
+  void _toStartOfContextLineIfAny() {
+    final contextLine1Based = selectedExample?.contextLine;
+
+    if (contextLine1Based == null) {
+      return;
+    }
+
+    _toStartOfFullLine(max(contextLine1Based - 1, 0));
+  }
+
+  void _toStartOfFullLine(int line) {
+    if (line >= codeController.code.lines.length) {
+      return;
+    }
+
+    final fullPosition = codeController.code.lines.lines[line].textRange.start;
+    final visiblePosition = codeController.code.hiddenRanges.cutPosition(
+      fullPosition,
+    );
+
+    codeController.selection = TextSelection.collapsed(
+      offset: visiblePosition,
+    );
+  }
+
   Example? get selectedExample => _selectedExample;
 
   ExampleLoadingDescriptor? get descriptor => _descriptor;
diff --git a/playground/frontend/playground_components/lib/src/models/example_base.dart b/playground/frontend/playground_components/lib/src/models/example_base.dart
index dd9b7fcb34c..2ca76d107b4 100644
--- a/playground/frontend/playground_components/lib/src/models/example_base.dart
+++ b/playground/frontend/playground_components/lib/src/models/example_base.dart
@@ -50,6 +50,8 @@ extension ExampleTypeToString on ExampleType {
 /// These objects are fetched as lists from [ExampleRepository].
 class ExampleBase with Comparable<ExampleBase>, EquatableMixin {
   final Complexity? complexity;
+
+  /// Index of the line to focus, 1-based.
   final int contextLine;
   final String description;
   final bool isMultiFile;
diff --git a/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart b/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart
deleted file mode 100644
index 571415a1e71..00000000000
--- a/playground/frontend/playground_components/lib/src/widgets/editor_textarea.dart
+++ /dev/null
@@ -1,178 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-// TODO(alexeyinkin): Refactor this, merge into snippet_editor.dart
-
-import 'package:flutter/material.dart';
-import 'package:flutter_code_editor/flutter_code_editor.dart';
-
-import '../models/example.dart';
-import '../models/sdk.dart';
-import '../theme/theme.dart';
-
-const kJavaRegExp = r'import\s[A-z.0-9]*\;\n\n[(\/\*\*)|(public)|(class)]';
-const kPythonRegExp = r'[^\S\r\n](import|as)[^\S\r\n][A-z]*\n\n';
-const kGoRegExp = r'[^\S\r\n]+\'
-    r'"'
-    r'.*'
-    r'"'
-    r'\n\)\n\n';
-const kAdditionalLinesForScrolling = 4;
-
-class EditorTextArea extends StatefulWidget {
-  final CodeController codeController;
-  final Sdk sdk;
-  final Example? example;
-  final bool enabled;
-  final bool isEditable;
-  final bool goToContextLine;
-
-  const EditorTextArea({
-    super.key,
-    required this.codeController,
-    required this.sdk,
-    this.example,
-    required this.enabled,
-    required this.isEditable,
-    required this.goToContextLine,
-  });
-
-  @override
-  State<EditorTextArea> createState() => _EditorTextAreaState();
-}
-
-class _EditorTextAreaState extends State<EditorTextArea> {
-  var focusNode = FocusNode();
-  final GlobalKey _sizeKey = LabeledGlobalKey('CodeFieldKey');
-
-  @override
-  void dispose() {
-    super.dispose();
-    focusNode.dispose();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    if (widget.goToContextLine) {
-      WidgetsBinding.instance.addPostFrameCallback((_) => _setTextScrolling());
-    }
-
-    final ext = Theme.of(context).extension<BeamThemeExtension>()!;
-
-    return Semantics(
-      container: true,
-      textField: true,
-      multiline: true,
-      enabled: widget.enabled,
-      readOnly: widget.enabled,
-      label: 'widgets.codeEditor.label',
-      child: FocusScope(
-        key: _sizeKey,
-        node: FocusScopeNode(canRequestFocus: widget.isEditable),
-        child: CodeTheme(
-          data: ext.codeTheme,
-          child: Container(
-            color: ext.codeTheme.styles['root']?.backgroundColor,
-            child: SingleChildScrollView(
-              child: CodeField(
-                key: ValueKey(widget.codeController),
-                focusNode: focusNode,
-                enabled: widget.enabled,
-                controller: widget.codeController,
-                textStyle: ext.codeRootStyle,
-              ),
-            ),
-          ),
-        ),
-      ),
-    );
-  }
-
-  void _setTextScrolling() {
-    focusNode.requestFocus();
-    if (widget.codeController.text.isNotEmpty) {
-      widget.codeController.selection = TextSelection.fromPosition(
-        TextPosition(
-          offset: _getOffset(),
-        ),
-      );
-    }
-  }
-
-  int _getOffset() {
-    int contextLine = _getIndexOfContextLine();
-    String pattern = _getPattern(_getQntOfStringsOnScreen());
-    if (pattern == '' || pattern == '}') {
-      return widget.codeController.text.lastIndexOf(pattern);
-    }
-
-    return widget.codeController.text.indexOf(
-      pattern,
-      contextLine,
-    );
-  }
-
-  String _getPattern(int qntOfStrings) {
-    int contextLineIndex = _getIndexOfContextLine();
-    List<String> stringsAfterContextLine =
-        widget.codeController.text.substring(contextLineIndex).split('\n');
-
-    String result =
-        stringsAfterContextLine.length + kAdditionalLinesForScrolling >
-                qntOfStrings
-            ? _getResultSubstring(stringsAfterContextLine, qntOfStrings)
-            : stringsAfterContextLine.last;
-
-    return result;
-  }
-
-  int _getQntOfStringsOnScreen() {
-    final renderBox = _sizeKey.currentContext!.findRenderObject()! as RenderBox;
-    final height = renderBox.size.height * .75;
-
-    return height ~/ codeFontSize;
-  }
-
-  int _getIndexOfContextLine() {
-    final contextLine = widget.example!.contextLine;
-    final code = widget.codeController.code;
-    final fullCharIndex = code.lines.lines[contextLine].textRange.start;
-    final visibleCharIndex = code.hiddenRanges.cutPosition(fullCharIndex);
-
-    return visibleCharIndex;
-  }
-
-  // This function made for more accuracy in the process of finding an exact line.
-  String _getResultSubstring(
-    List<String> stringsAfterContextLine,
-    int qntOfStrings,
-  ) {
-    StringBuffer result = StringBuffer();
-
-    for (int i = qntOfStrings - kAdditionalLinesForScrolling;
-        i < qntOfStrings + kAdditionalLinesForScrolling;
-        i++) {
-      if (i == stringsAfterContextLine.length - 1) {
-        return result.toString();
-      }
-      result.write(stringsAfterContextLine[i] + '\n');
-    }
-
-    return result.toString();
-  }
-}
diff --git a/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart b/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart
index fe7ecc4e603..ddcba180b89 100644
--- a/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart
+++ b/playground/frontend/playground_components/lib/src/widgets/snippet_editor.dart
@@ -16,31 +16,120 @@
  * limitations under the License.
  */
 
-import 'package:flutter/widgets.dart';
+import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
+import 'package:flutter_code_editor/flutter_code_editor.dart';
 
 import '../controllers/snippet_editing_controller.dart';
-import 'editor_textarea.dart';
+import '../theme/theme.dart';
 
-class SnippetEditor extends StatelessWidget {
+class SnippetEditor extends StatefulWidget {
   final SnippetEditingController controller;
   final bool isEditable;
-  final bool goToContextLine;
 
-  const SnippetEditor({
+  SnippetEditor({
     required this.controller,
     required this.isEditable,
-    required this.goToContextLine,
-  });
+  }) : super(
+    // When the example is changed, will scroll to the context line again.
+    key: ValueKey(controller.selectedExample),
+  );
+
+  @override
+  State<SnippetEditor> createState() => _SnippetEditorState();
+}
+
+class _SnippetEditorState extends State<SnippetEditor> {
+  bool _didAutoFocus = false;
+  final _focusNode = FocusNode();
+  final _scrollController = ScrollController();
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+
+    if (!_didAutoFocus) {
+      _didAutoFocus = true;
+      SchedulerBinding.instance.addPostFrameCallback((_) {
+        if (mounted) {
+          _scrollSoCursorIsOnTop();
+        }
+      });
+    }
+  }
+
+  void _scrollSoCursorIsOnTop() {
+    _focusNode.requestFocus();
+
+    final position = max(widget.controller.codeController.selection.start, 0);
+    final characterOffset = _getLastCharacterOffset(
+      text: widget.controller.codeController.text.substring(0, position),
+      style: kLightTheme.extension<BeamThemeExtension>()!.codeRootStyle,
+    );
+
+    _scrollController.jumpTo(
+      min(
+        characterOffset.dy,
+        _scrollController.position.maxScrollExtent,
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    _focusNode.dispose();
+    super.dispose();
+  }
 
   @override
   Widget build(BuildContext context) {
-    return EditorTextArea(
-      codeController: controller.codeController,
-      sdk: controller.sdk,
-      enabled: !(controller.selectedExample?.isMultiFile ?? false),
-      example: controller.selectedExample,
-      isEditable: isEditable,
-      goToContextLine: goToContextLine,
+    final ext = Theme.of(context).extension<BeamThemeExtension>()!;
+    final isMultiFile = widget.controller.selectedExample?.isMultiFile ?? false;
+    final isEnabled = widget.isEditable && !isMultiFile;
+
+    return Semantics(
+      container: true,
+      enabled: isEnabled,
+      label: 'widgets.codeEditor.label',
+      multiline: true,
+      readOnly: isEnabled,
+      textField: true,
+      child: FocusScope(
+        node: FocusScopeNode(canRequestFocus: isEnabled),
+        child: CodeTheme(
+          data: ext.codeTheme,
+          child: Container(
+            color: ext.codeTheme.styles['root']?.backgroundColor,
+            child: SingleChildScrollView(
+              controller: _scrollController,
+              child: CodeField(
+                key: ValueKey(widget.controller.codeController),
+                controller: widget.controller.codeController,
+                enabled: isEnabled,
+                focusNode: _focusNode,
+                textStyle: ext.codeRootStyle,
+              ),
+            ),
+          ),
+        ),
+      ),
     );
   }
 }
+
+Offset _getLastCharacterOffset({
+  required String text,
+  required TextStyle style,
+}) {
+  final textPainter = TextPainter(
+    textDirection: TextDirection.ltr,
+    text: TextSpan(text: text, style: style),
+  )..layout();
+
+  return textPainter.getOffsetForCaret(
+    TextPosition(offset: text.length),
+    Rect.zero,
+  );
+}