瀏覽代碼

feat: #953 improve arrow keys handler

Lucas.Xu 2 年之前
父節點
當前提交
f098c543e6

+ 2 - 2
frontend/app_flowy/packages/appflowy_editor/lib/src/document/selection.dart

@@ -46,9 +46,9 @@ class Selection {
       (start.path <= end.path && !pathEquals(start.path, end.path)) ||
       (isSingle && start.offset < end.offset);
 
-  Selection normalize() {
+  Selection get normalize {
     if (isForward) {
-      return Selection(start: end, end: start);
+      return reversed;
     }
     return this;
   }

+ 2 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/default_selectable.dart

@@ -32,7 +32,8 @@ mixin DefaultSelectable {
   Selection getSelectionInRange(Offset start, Offset end) =>
       forward.getSelectionInRange(start, end);
 
-  Offset localToGlobal(Offset offset) => forward.localToGlobal(offset);
+  Offset localToGlobal(Offset offset) =>
+      forward.localToGlobal(offset) - baseOffset;
 
   Selection? getWorldBoundaryInOffset(Offset offset) =>
       forward.getWorldBoundaryInOffset(offset);

+ 10 - 3
frontend/app_flowy/packages/appflowy_editor/lib/src/render/rich_text/flowy_rich_text.dart

@@ -132,17 +132,24 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
 
   @override
   List<Rect> getRectsInSelection(Selection selection) {
-    assert(pathEquals(selection.start.path, selection.end.path) &&
+    assert(selection.isSingle &&
         pathEquals(selection.start.path, widget.textNode.path));
 
     final textSelection = TextSelection(
       baseOffset: selection.start.offset,
       extentOffset: selection.end.offset,
     );
-    return _renderParagraph
+    final rects = _renderParagraph
         .getBoxesForSelection(textSelection, boxHeightStyle: BoxHeightStyle.max)
         .map((box) => box.toRect())
-        .toList();
+        .toList(growable: false);
+    if (rects.isEmpty) {
+      // If the rich text widget does not contain any text,
+      // there will be no selection boxes,
+      // so we need to return to the default selection.
+      return [Rect.fromLTWH(0, 0, 0, _renderParagraph.size.height)];
+    }
+    return rects;
   }
 
   @override

+ 236 - 98
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/arrow_keys_handler.dart

@@ -1,153 +1,291 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_editor/src/extensions/node_extensions.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 
-int _endOffsetOfNode(Node node) {
-  if (node is TextNode) {
-    return node.delta.length;
+AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
+  if (!_arrowKeys.contains(event.logicalKey)) {
+    return KeyEventResult.ignored;
   }
-  return 0;
-}
 
-extension on Position {
-  Position? goLeft(EditorState editorState) {
-    final node = editorState.document.nodeAtPath(path)!;
-    if (offset == 0) {
-      final prevNode = node.previous;
-      if (prevNode != null) {
-        return Position(
-            path: prevNode.path, offset: _endOffsetOfNode(prevNode));
-      }
-      return null;
-    }
+  if (event.isMetaPressed && event.isShiftPressed) {
+    return _arrowKeysWithMetaAndShift(editorState, event);
+  } else if (event.isMetaPressed) {
+    return _arrowKeysWithMeta(editorState, event);
+  } else if (event.isShiftPressed) {
+    return _arrowKeysWithShift(editorState, event);
+  } else {
+    return _arrowKeysOnly(editorState, event);
+  }
+};
 
-    if (node is TextNode) {
-      return Position(path: path, offset: node.delta.prevRunePosition(offset));
-    } else {
-      return Position(path: path, offset: offset);
-    }
+final _arrowKeys = [
+  LogicalKeyboardKey.arrowLeft,
+  LogicalKeyboardKey.arrowRight,
+  LogicalKeyboardKey.arrowUp,
+  LogicalKeyboardKey.arrowDown
+];
+
+KeyEventResult _arrowKeysWithMetaAndShift(
+    EditorState editorState, RawKeyEvent event) {
+  if (!event.isMetaPressed ||
+      !event.isShiftPressed ||
+      !_arrowKeys.contains(event.logicalKey)) {
+    assert(false);
+    return KeyEventResult.ignored;
   }
 
-  Position? goRight(EditorState editorState) {
-    final node = editorState.document.nodeAtPath(path)!;
-    final lengthOfNode = _endOffsetOfNode(node);
-    if (offset >= lengthOfNode) {
-      final nextNode = node.next;
-      if (nextNode != null) {
-        return Position(path: nextNode.path, offset: 0);
-      }
-      return null;
-    }
+  final nodes = editorState.service.selectionService.currentSelectedNodes;
+  final selection = editorState.service.selectionService.currentSelection.value;
+  if (nodes.isEmpty || selection == null) {
+    return KeyEventResult.ignored;
+  }
 
-    if (node is TextNode) {
-      return Position(path: path, offset: node.delta.nextRunePosition(offset));
-    } else {
-      return Position(path: path, offset: offset);
+  var start = selection.start;
+  var end = selection.end;
+  if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+    final position = nodes.first.selectable?.start();
+    if (position != null) {
+      end = position;
+    }
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
+    final position = nodes.first.selectable?.end();
+    if (position != null) {
+      end = position;
+    }
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
+    final position = editorState.document.root.children
+        .whereType<TextNode>()
+        .first
+        .selectable
+        ?.start();
+    if (position != null) {
+      end = position;
+    }
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
+    final position = editorState.document.root.children
+        .whereType<TextNode>()
+        .last
+        .selectable
+        ?.end();
+    if (position != null) {
+      end = position;
     }
   }
+  editorState.service.selectionService.updateSelection(
+    selection.copyWith(start: start, end: end),
+  );
+  return KeyEventResult.handled;
 }
 
-Position? _goUp(EditorState editorState) {
-  final rects = editorState.service.selectionService.selectionRects;
-  if (rects.isEmpty) {
-    return null;
+// Move the cursor to top, bottom, left and right of the document.
+KeyEventResult _arrowKeysWithMeta(EditorState editorState, RawKeyEvent event) {
+  if (!event.isMetaPressed ||
+      event.isShiftPressed ||
+      !_arrowKeys.contains(event.logicalKey)) {
+    assert(false);
+    return KeyEventResult.ignored;
   }
-  final first = rects.first;
-  final firstOffset = Offset(first.left, first.top);
-  final hitOffset = firstOffset - Offset(0, first.height * 0.5);
-  return editorState.service.selectionService.getPositionInOffset(hitOffset);
-}
 
-Position? _goDown(EditorState editorState) {
-  final rects = editorState.service.selectionService.selectionRects;
-  if (rects.isEmpty) {
-    return null;
+  final nodes = editorState.service.selectionService.currentSelectedNodes;
+  if (nodes.isEmpty) {
+    return KeyEventResult.ignored;
+  }
+  Position? position;
+  if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+    position = nodes.first.selectable?.start();
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
+    position = nodes.last.selectable?.end();
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
+    position = editorState.document.root.children
+        .whereType<TextNode>()
+        .first
+        .selectable
+        ?.start();
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
+    position = editorState.document.root.children
+        .whereType<TextNode>()
+        .last
+        .selectable
+        ?.end();
   }
-  final first = rects.last;
-  final firstOffset = Offset(first.right, first.bottom);
-  final hitOffset = firstOffset + Offset(0, first.height * 0.5);
-  return editorState.service.selectionService.getPositionInOffset(hitOffset);
+  if (position == null) {
+    return KeyEventResult.ignored;
+  }
+  editorState.service.selectionService.updateSelection(
+    Selection.collapsed(position),
+  );
+  return KeyEventResult.handled;
 }
 
-KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {
-  final currentSelection = editorState.cursorSelection;
-  if (currentSelection == null) {
+KeyEventResult _arrowKeysWithShift(EditorState editorState, RawKeyEvent event) {
+  if (event.isMetaPressed ||
+      !event.isShiftPressed ||
+      !_arrowKeys.contains(event.logicalKey)) {
+    assert(false);
     return KeyEventResult.ignored;
   }
 
+  final nodes = editorState.service.selectionService.currentSelectedNodes;
+  final selection = editorState.service.selectionService.currentSelection.value;
+  if (nodes.isEmpty || selection == null) {
+    return KeyEventResult.ignored;
+  }
+  Position? end;
   if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
-    final leftPosition = currentSelection.end.goLeft(editorState);
-    editorState.updateCursorSelection(leftPosition == null
-        ? null
-        : Selection(start: currentSelection.start, end: leftPosition));
-    return KeyEventResult.handled;
+    end = selection.end.goLeft(editorState);
   } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
-    final rightPosition = currentSelection.start.goRight(editorState);
-    editorState.updateCursorSelection(rightPosition == null
-        ? null
-        : Selection(start: rightPosition, end: currentSelection.end));
-    return KeyEventResult.handled;
+    end = selection.end.goRight(editorState);
   } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
-    final position = _goUp(editorState);
-    editorState.updateCursorSelection(position == null
-        ? null
-        : Selection(start: position, end: currentSelection.end));
-    return KeyEventResult.handled;
+    end = _goUp(editorState);
   } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
-    final position = _goDown(editorState);
-    editorState.updateCursorSelection(position == null
-        ? null
-        : Selection(start: currentSelection.start, end: position));
-    return KeyEventResult.handled;
+    end = _goDown(editorState);
   }
-  return KeyEventResult.ignored;
+  if (end == null) {
+    return KeyEventResult.ignored;
+  }
+  editorState.service.selectionService
+      .updateSelection(selection.copyWith(end: end));
+  return KeyEventResult.handled;
 }
 
-AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
-  if (event.isShiftPressed) {
-    return _handleShiftKey(editorState, event);
+KeyEventResult _arrowKeysOnly(EditorState editorState, RawKeyEvent event) {
+  if (event.isMetaPressed ||
+      event.isShiftPressed ||
+      !_arrowKeys.contains(event.logicalKey)) {
+    assert(false);
+    return KeyEventResult.ignored;
   }
 
-  final currentSelection = editorState.cursorSelection;
-  if (currentSelection == null) {
+  final nodes = editorState.service.selectionService.currentSelectedNodes;
+  final selection =
+      editorState.service.selectionService.currentSelection.value?.normalize;
+  if (nodes.isEmpty || selection == null) {
     return KeyEventResult.ignored;
   }
-
   if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
-    if (currentSelection.isCollapsed) {
-      final leftPosition = currentSelection.start.goLeft(editorState);
+    if (selection.isCollapsed) {
+      final leftPosition = selection.start.goLeft(editorState);
       if (leftPosition != null) {
-        editorState.updateCursorSelection(Selection.collapsed(leftPosition));
+        editorState.service.selectionService.updateSelection(
+          Selection.collapsed(leftPosition),
+        );
       }
     } else {
-      editorState.updateCursorSelection(
-        currentSelection.collapse(atStart: currentSelection.isBackward),
+      editorState.service.selectionService.updateSelection(
+        Selection.collapsed(selection.start),
       );
     }
     return KeyEventResult.handled;
   } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
-    if (currentSelection.isCollapsed) {
-      final rightPosition = currentSelection.end.goRight(editorState);
+    if (selection.isCollapsed) {
+      final rightPosition = selection.start.goRight(editorState);
       if (rightPosition != null) {
-        editorState.updateCursorSelection(Selection.collapsed(rightPosition));
+        editorState.service.selectionService.updateSelection(
+          Selection.collapsed(rightPosition),
+        );
       }
     } else {
-      editorState.updateCursorSelection(
-        currentSelection.collapse(atStart: !currentSelection.isBackward),
+      editorState.service.selectionService.updateSelection(
+        Selection.collapsed(selection.end),
       );
     }
     return KeyEventResult.handled;
   } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
-    final position = _goUp(editorState);
+    final upPosition = _goUp(editorState);
     editorState.updateCursorSelection(
-        position == null ? null : Selection.collapsed(position));
+      upPosition == null ? null : Selection.collapsed(upPosition),
+    );
     return KeyEventResult.handled;
   } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
-    final position = _goDown(editorState);
+    final downPosition = _goDown(editorState);
     editorState.updateCursorSelection(
-        position == null ? null : Selection.collapsed(position));
+      downPosition == null ? null : Selection.collapsed(downPosition),
+    );
     return KeyEventResult.handled;
   }
-
   return KeyEventResult.ignored;
-};
+}
+
+extension on Position {
+  Position? goLeft(EditorState editorState) {
+    final node = editorState.document.nodeAtPath(path);
+    if (node == null) {
+      return null;
+    }
+    if (offset == 0) {
+      final previousEnd = node.previous?.selectable?.end();
+      if (previousEnd != null) {
+        return previousEnd;
+      }
+      return null;
+    }
+    if (node is TextNode) {
+      return Position(path: path, offset: node.delta.prevRunePosition(offset));
+    } else {
+      return Position(path: path, offset: offset);
+    }
+  }
+
+  Position? goRight(EditorState editorState) {
+    final node = editorState.document.nodeAtPath(path);
+    if (node == null) {
+      return null;
+    }
+    final end = node.selectable?.end();
+    if (end != null && offset >= end.offset) {
+      final nextStart = node.next?.selectable?.start();
+      if (nextStart != null) {
+        return nextStart;
+      }
+      return null;
+    }
+    if (node is TextNode) {
+      return Position(path: path, offset: node.delta.nextRunePosition(offset));
+    } else {
+      return Position(path: path, offset: offset);
+    }
+  }
+}
+
+Position? _goUp(EditorState editorState) {
+  final selection = editorState.service.selectionService.currentSelection.value;
+  final rects = editorState.service.selectionService.selectionRects;
+  if (rects.isEmpty || selection == null) {
+    return null;
+  }
+  Offset offset;
+  if (selection.isBackward) {
+    final rect = rects.reduce(
+      (current, next) => current.bottom >= next.bottom ? current : next,
+    );
+    offset = rect.topRight.translate(0, -rect.height);
+  } else {
+    final rect = rects.reduce(
+      (current, next) => current.top <= next.top ? current : next,
+    );
+    offset = rect.topLeft.translate(0, -rect.height);
+  }
+  return editorState.service.selectionService.getPositionInOffset(offset);
+}
+
+Position? _goDown(EditorState editorState) {
+  final selection = editorState.service.selectionService.currentSelection.value;
+  final rects = editorState.service.selectionService.selectionRects;
+  if (rects.isEmpty || selection == null) {
+    return null;
+  }
+  Offset offset;
+  if (selection.isBackward) {
+    final rect = rects.reduce(
+      (current, next) => current.bottom >= next.bottom ? current : next,
+    );
+    offset = rect.bottomRight.translate(0, rect.height);
+  } else {
+    final rect = rects.reduce(
+      (current, next) => current.top <= next.top ? current : next,
+    );
+    offset = rect.bottomLeft.translate(0, rect.height);
+  }
+  return editorState.service.selectionService.getPositionInOffset(offset);
+}

+ 4 - 5
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/copy_paste_handler.dart

@@ -1,13 +1,12 @@
 import 'package:appflowy_editor/appflowy_editor.dart';
 import 'package:appflowy_editor/src/infra/html_converter.dart';
 import 'package:appflowy_editor/src/document/node_iterator.dart';
-import 'package:appflowy_editor/src/infra/log.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:rich_clipboard/rich_clipboard.dart';
 
 _handleCopy(EditorState editorState) async {
-  final selection = editorState.cursorSelection?.normalize();
+  final selection = editorState.cursorSelection?.normalize;
   if (selection == null || selection.isCollapsed) {
     return;
   }
@@ -43,7 +42,7 @@ _handleCopy(EditorState editorState) async {
 }
 
 _pasteHTML(EditorState editorState, String html) {
-  final selection = editorState.cursorSelection?.normalize();
+  final selection = editorState.cursorSelection?.normalize;
   if (selection == null) {
     return;
   }
@@ -191,7 +190,7 @@ Delta _lineContentToDelta(String lineContent) {
 }
 
 _handlePastePlainText(EditorState editorState, String plainText) {
-  final selection = editorState.cursorSelection?.normalize();
+  final selection = editorState.cursorSelection?.normalize;
   if (selection == null) {
     return;
   }
@@ -256,7 +255,7 @@ _handleCut(EditorState editorState) {
 }
 
 _deleteSelectedContent(EditorState editorState) {
-  final selection = editorState.cursorSelection?.normalize();
+  final selection = editorState.cursorSelection?.normalize;
   if (selection == null || selection.isCollapsed) {
     return;
   }

+ 2 - 1
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/default_key_event_handlers.dart

@@ -1,7 +1,7 @@
-import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/copy_paste_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/backspace_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/enter_without_shift_in_text_node_handler.dart';
+import 'package:appflowy_editor/src/service/internal_key_event_handlers/arrow_keys_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/redo_undo_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/slash_handler.dart';
 import 'package:appflowy_editor/src/service/internal_key_event_handlers/update_text_style_by_command_x_handler.dart';
@@ -13,6 +13,7 @@ import 'package:appflowy_editor/src/service/keyboard_service.dart';
 List<AppFlowyKeyEventHandler> defaultKeyEventHandlers = [
   deleteTextHandler,
   slashShortcutHandler,
+  // arrowKeysHandler,
   arrowKeysHandler,
   copyPasteKeysHandler,
   redoUndoKeysHandler,

+ 153 - 0
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/legacy/arrow_keys_handler.dart

@@ -0,0 +1,153 @@
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+int _endOffsetOfNode(Node node) {
+  if (node is TextNode) {
+    return node.delta.length;
+  }
+  return 0;
+}
+
+extension on Position {
+  Position? goLeft(EditorState editorState) {
+    final node = editorState.document.nodeAtPath(path)!;
+    if (offset == 0) {
+      final prevNode = node.previous;
+      if (prevNode != null) {
+        return Position(
+            path: prevNode.path, offset: _endOffsetOfNode(prevNode));
+      }
+      return null;
+    }
+
+    if (node is TextNode) {
+      return Position(path: path, offset: node.delta.prevRunePosition(offset));
+    } else {
+      return Position(path: path, offset: offset);
+    }
+  }
+
+  Position? goRight(EditorState editorState) {
+    final node = editorState.document.nodeAtPath(path)!;
+    final lengthOfNode = _endOffsetOfNode(node);
+    if (offset >= lengthOfNode) {
+      final nextNode = node.next;
+      if (nextNode != null) {
+        return Position(path: nextNode.path, offset: 0);
+      }
+      return null;
+    }
+
+    if (node is TextNode) {
+      return Position(path: path, offset: node.delta.nextRunePosition(offset));
+    } else {
+      return Position(path: path, offset: offset);
+    }
+  }
+}
+
+Position? _goUp(EditorState editorState) {
+  final rects = editorState.service.selectionService.selectionRects;
+  if (rects.isEmpty) {
+    return null;
+  }
+  final first = rects.first;
+  final firstOffset = Offset(first.left, first.top);
+  final hitOffset = firstOffset - Offset(0, first.height * 0.5);
+  return editorState.service.selectionService.getPositionInOffset(hitOffset);
+}
+
+Position? _goDown(EditorState editorState) {
+  final rects = editorState.service.selectionService.selectionRects;
+  if (rects.isEmpty) {
+    return null;
+  }
+  final first = rects.last;
+  final firstOffset = Offset(first.right, first.bottom);
+  final hitOffset = firstOffset + Offset(0, first.height * 0.5);
+  return editorState.service.selectionService.getPositionInOffset(hitOffset);
+}
+
+KeyEventResult _handleShiftKey(EditorState editorState, RawKeyEvent event) {
+  final currentSelection = editorState.cursorSelection;
+  if (currentSelection == null) {
+    return KeyEventResult.ignored;
+  }
+
+  if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+    final leftPosition = currentSelection.end.goLeft(editorState);
+    editorState.updateCursorSelection(leftPosition == null
+        ? null
+        : Selection(start: currentSelection.start, end: leftPosition));
+    return KeyEventResult.handled;
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
+    final rightPosition = currentSelection.start.goRight(editorState);
+    editorState.updateCursorSelection(rightPosition == null
+        ? null
+        : Selection(start: rightPosition, end: currentSelection.end));
+    return KeyEventResult.handled;
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
+    final position = _goUp(editorState);
+    editorState.updateCursorSelection(position == null
+        ? null
+        : Selection(start: position, end: currentSelection.end));
+    return KeyEventResult.handled;
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
+    final position = _goDown(editorState);
+    editorState.updateCursorSelection(position == null
+        ? null
+        : Selection(start: currentSelection.start, end: position));
+    return KeyEventResult.handled;
+  }
+  return KeyEventResult.ignored;
+}
+
+AppFlowyKeyEventHandler arrowKeysHandler = (editorState, event) {
+  if (event.isShiftPressed) {
+    return _handleShiftKey(editorState, event);
+  }
+
+  final currentSelection = editorState.cursorSelection;
+  if (currentSelection == null) {
+    return KeyEventResult.ignored;
+  }
+
+  if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
+    if (currentSelection.isCollapsed) {
+      final leftPosition = currentSelection.start.goLeft(editorState);
+      if (leftPosition != null) {
+        editorState.updateCursorSelection(Selection.collapsed(leftPosition));
+      }
+    } else {
+      editorState.updateCursorSelection(
+        currentSelection.collapse(atStart: currentSelection.isBackward),
+      );
+    }
+    return KeyEventResult.handled;
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
+    if (currentSelection.isCollapsed) {
+      final rightPosition = currentSelection.end.goRight(editorState);
+      if (rightPosition != null) {
+        editorState.updateCursorSelection(Selection.collapsed(rightPosition));
+      }
+    } else {
+      editorState.updateCursorSelection(
+        currentSelection.collapse(atStart: !currentSelection.isBackward),
+      );
+    }
+    return KeyEventResult.handled;
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
+    final position = _goUp(editorState);
+    editorState.updateCursorSelection(
+        position == null ? null : Selection.collapsed(position));
+    return KeyEventResult.handled;
+  } else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
+    final position = _goDown(editorState);
+    editorState.updateCursorSelection(
+        position == null ? null : Selection.collapsed(position));
+    return KeyEventResult.handled;
+  }
+
+  return KeyEventResult.ignored;
+};

+ 0 - 4
frontend/app_flowy/packages/appflowy_editor/lib/src/service/internal_key_event_handlers/slash_handler.dart

@@ -25,10 +25,6 @@ AppFlowyKeyEventHandler slashShortcutHandler = (editorState, event) {
   if (selection == null || context == null || selectable == null) {
     return KeyEventResult.ignored;
   }
-  final selectionRects = editorState.service.selectionService.selectionRects;
-  if (selectionRects.isEmpty) {
-    return KeyEventResult.ignored;
-  }
   TransactionBuilder(editorState)
     ..replaceText(textNode, selection.start.offset,
         selection.end.offset - selection.start.offset, event.character ?? '')

+ 4 - 6
frontend/app_flowy/packages/appflowy_editor/lib/src/service/selection_service.dart

@@ -348,10 +348,8 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
 
     final backwardNodes =
         selection.isBackward ? nodes : nodes.reversed.toList(growable: false);
-    final backwardSelection = selection.isBackward
-        ? selection
-        : selection.copyWith(start: selection.end, end: selection.start);
-    assert(backwardSelection.isBackward);
+    final normalizedSelection = selection.normalize;
+    assert(normalizedSelection.isBackward);
 
     for (var i = 0; i < backwardNodes.length; i++) {
       final node = backwardNodes[i];
@@ -360,7 +358,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
         continue;
       }
 
-      var newSelection = backwardSelection.copy();
+      var newSelection = normalizedSelection.copy();
 
       /// In the case of multiple selections,
       ///  we need to return a new selection for each selected node individually.
@@ -370,7 +368,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
       /// text: ghijkl
       /// text: mn>opqr
       ///
-      if (!backwardSelection.isSingle) {
+      if (!normalizedSelection.isSingle) {
         if (i == 0) {
           newSelection = newSelection.copyWith(end: selectable.end());
         } else if (i == nodes.length - 1) {

+ 3 - 0
frontend/app_flowy/packages/appflowy_editor/test/infra/test_raw_key_event.dart

@@ -103,6 +103,9 @@ extension on LogicalKeyboardKey {
     if (this == LogicalKeyboardKey.slash) {
       return PhysicalKeyboardKey.slash;
     }
+    if (this == LogicalKeyboardKey.arrowUp) {
+      return PhysicalKeyboardKey.arrowUp;
+    }
     if (this == LogicalKeyboardKey.arrowDown) {
       return PhysicalKeyboardKey.arrowDown;
     }

+ 282 - 0
frontend/app_flowy/packages/appflowy_editor/test/service/internal_key_event_handlers/arrow_keys_handler_test.dart

@@ -58,6 +58,219 @@ void main() async {
       (tester) async {
     await _testPressArrowKeyInNotCollapsedSelection(tester, false);
   });
+
+  testWidgets('Presses arrow left/right + shift in collapsed selection',
+      (tester) async {
+    const text = 'Welcome to Appflowy';
+    final editor = tester.editor
+      ..insertTextNode(text)
+      ..insertTextNode(text);
+    await editor.startTesting();
+    const offset = 8;
+    final selection = Selection.single(path: [1], startOffset: offset);
+    await editor.updateSelection(selection);
+    for (var i = offset - 1; i >= 0; i--) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowLeft,
+        isShiftPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        selection.copyWith(
+          end: Position(path: [1], offset: i),
+        ),
+      );
+    }
+    for (var i = text.length; i >= 0; i--) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowLeft,
+        isShiftPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        selection.copyWith(
+          end: Position(path: [0], offset: i),
+        ),
+      );
+    }
+    for (var i = 1; i <= text.length; i++) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowRight,
+        isShiftPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        selection.copyWith(
+          end: Position(path: [0], offset: i),
+        ),
+      );
+    }
+    for (var i = 0; i < text.length; i++) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowRight,
+        isShiftPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        selection.copyWith(
+          end: Position(path: [1], offset: i),
+        ),
+      );
+    }
+  });
+
+  testWidgets(
+      'Presses arrow left/right + shift in not collapsed and backward selection',
+      (tester) async {
+    const text = 'Welcome to Appflowy';
+    final editor = tester.editor
+      ..insertTextNode(text)
+      ..insertTextNode(text);
+    await editor.startTesting();
+    const start = 8;
+    const end = 12;
+    final selection = Selection.single(
+      path: [0],
+      startOffset: start,
+      endOffset: end,
+    );
+    await editor.updateSelection(selection);
+    for (var i = end + 1; i <= text.length; i++) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowRight,
+        isShiftPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        selection.copyWith(
+          end: Position(path: [0], offset: i),
+        ),
+      );
+    }
+    for (var i = text.length - 1; i >= 0; i--) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowLeft,
+        isShiftPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        selection.copyWith(
+          end: Position(path: [0], offset: i),
+        ),
+      );
+    }
+  });
+
+  testWidgets(
+      'Presses arrow left/right + command in not collapsed and forward selection',
+      (tester) async {
+    const text = 'Welcome to Appflowy';
+    final editor = tester.editor
+      ..insertTextNode(text)
+      ..insertTextNode(text);
+    await editor.startTesting();
+    const start = 12;
+    const end = 8;
+    final selection = Selection.single(
+      path: [0],
+      startOffset: start,
+      endOffset: end,
+    );
+    await editor.updateSelection(selection);
+    for (var i = end - 1; i >= 0; i--) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowLeft,
+        isShiftPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        selection.copyWith(
+          end: Position(path: [0], offset: i),
+        ),
+      );
+    }
+    for (var i = 1; i <= text.length; i++) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowRight,
+        isShiftPressed: true,
+      );
+      expect(
+        editor.documentSelection,
+        selection.copyWith(
+          end: Position(path: [0], offset: i),
+        ),
+      );
+    }
+  });
+
+  testWidgets('Presses arrow left/right/up/down + meta in collapsed selection',
+      (tester) async {
+    await _testPressArrowKeyWithMetaInSelection(tester, true, false);
+  });
+
+  testWidgets(
+      'Presses arrow left/right/up/down + meta in not collapsed and backward selection',
+      (tester) async {
+    await _testPressArrowKeyWithMetaInSelection(tester, false, true);
+  });
+
+  testWidgets(
+      'Presses arrow left/right/up/down + meta in not collapsed and forward selection',
+      (tester) async {
+    await _testPressArrowKeyWithMetaInSelection(tester, false, false);
+  });
+
+  testWidgets('Presses arrow up/down + shift in not collapsed selection',
+      (tester) async {
+    const text = 'Welcome to Appflowy 😁';
+    final editor = tester.editor
+      ..insertTextNode(text)
+      ..insertTextNode(text)
+      ..insertTextNode(null)
+      ..insertTextNode(text)
+      ..insertTextNode(null)
+      ..insertTextNode(text)
+      ..insertTextNode(text);
+    await editor.startTesting();
+    final selection = Selection.single(path: [3], startOffset: 8);
+    await editor.updateSelection(selection);
+    for (int i = 0; i < 3; i++) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowUp,
+        isShiftPressed: true,
+      );
+    }
+    expect(
+      editor.documentSelection,
+      selection.copyWith(
+        end: Position(path: [0], offset: 0),
+      ),
+    );
+    for (int i = 0; i < 7; i++) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowDown,
+        isShiftPressed: true,
+      );
+    }
+    expect(
+      editor.documentSelection,
+      selection.copyWith(
+        end: Position(path: [6], offset: 0),
+      ),
+    );
+    for (int i = 0; i < 3; i++) {
+      await editor.pressLogicKey(
+        LogicalKeyboardKey.arrowUp,
+        isShiftPressed: true,
+      );
+    }
+    expect(
+      editor.documentSelection,
+      selection.copyWith(
+        end: Position(path: [3], offset: 0),
+      ),
+    );
+  });
 }
 
 Future<void> _testPressArrowKeyInNotCollapsedSelection(
@@ -82,3 +295,72 @@ Future<void> _testPressArrowKeyInNotCollapsedSelection(
   await editor.pressLogicKey(LogicalKeyboardKey.arrowRight);
   expect(editor.documentSelection?.end, end);
 }
+
+Future<void> _testPressArrowKeyWithMetaInSelection(
+  WidgetTester tester,
+  bool isSingle,
+  bool isBackward,
+) async {
+  const text = 'Welcome to Appflowy';
+  final editor = tester.editor
+    ..insertTextNode(text)
+    ..insertTextNode(text);
+  await editor.startTesting();
+  Selection selection;
+  if (isSingle) {
+    selection = Selection.single(
+      path: [0],
+      startOffset: 8,
+    );
+  } else {
+    if (isBackward) {
+      selection = Selection.single(
+        path: [0],
+        startOffset: 8,
+        endOffset: text.length,
+      );
+    } else {
+      selection = Selection.single(
+        path: [0],
+        startOffset: text.length,
+        endOffset: 8,
+      );
+    }
+  }
+  await editor.updateSelection(selection);
+  await editor.pressLogicKey(
+    LogicalKeyboardKey.arrowLeft,
+    isMetaPressed: true,
+  );
+  expect(
+    editor.documentSelection,
+    Selection.single(path: [0], startOffset: 0),
+  );
+
+  await editor.pressLogicKey(
+    LogicalKeyboardKey.arrowRight,
+    isMetaPressed: true,
+  );
+  expect(
+    editor.documentSelection,
+    Selection.single(path: [0], startOffset: text.length),
+  );
+
+  await editor.pressLogicKey(
+    LogicalKeyboardKey.arrowUp,
+    isMetaPressed: true,
+  );
+  expect(
+    editor.documentSelection,
+    Selection.single(path: [0], startOffset: 0),
+  );
+
+  await editor.pressLogicKey(
+    LogicalKeyboardKey.arrowDown,
+    isMetaPressed: true,
+  );
+  expect(
+    editor.documentSelection,
+    Selection.single(path: [1], startOffset: text.length),
+  );
+}