diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart
index 35ac1f467e..a8163f094d 100644
--- a/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/path.dart
@@ -1 +1,7 @@
+import 'package:flutter/foundation.dart';
+
 typedef Path = List<int>;
+
+bool pathEquals(Path path1, Path path2) {
+  return listEquals(path1, path2);
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart
new file mode 100644
index 0000000000..2c7d85f908
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart
@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+
+import './path.dart';
+
+class Position {
+  final Path path;
+  final int offset;
+
+  Position({
+    required this.path,
+    this.offset = 0,
+  });
+
+  @override
+  bool operator==(Object other) {
+    if (other is! Position) {
+      return false;
+    }
+    return pathEquals(path, other.path) && offset == other.offset;
+  }
+
+  @override
+  int get hashCode {
+    final pathHash = hashList(path);
+    return pathHash ^ offset;
+  }
+
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart
new file mode 100644
index 0000000000..a03ccae37f
--- /dev/null
+++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart
@@ -0,0 +1,28 @@
+import './position.dart';
+
+class Selection {
+  final Position start;
+  final Position end;
+
+  Selection({
+    required this.start,
+    required this.end,
+  });
+
+  factory Selection.collapsed(Position pos) {
+    return Selection(start: pos, end: pos);
+  }
+
+  Selection collapse({ bool atStart = false }) {
+    if (atStart) {
+      return Selection(start: start, end: start);
+    } else {
+      return Selection(start: end, end: end);
+    }
+  }
+
+  bool isCollapsed() {
+    return start == end;
+  }
+
+}
diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart
index e8f14bc9c7..cb67f61d96 100644
--- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart
+++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart
@@ -2,6 +2,10 @@ import 'dart:convert';
 
 import 'package:flowy_editor/document/node.dart';
 import 'package:flowy_editor/document/state_tree.dart';
+import 'package:flowy_editor/document/path.dart';
+import 'package:flowy_editor/document/position.dart';
+import 'package:flowy_editor/document/selection.dart';
+import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_test/flutter_test.dart';
 
@@ -61,4 +65,69 @@ void main() {
     expect(updatedNode != null, true);
     expect(updatedNode!.attributes['text-type'], 'heading1');
   });
+
+  test('test path utils 1', () {
+    final path1 = <int>[1];
+    final path2 = <int>[1];
+    expect(pathEquals(path1, path2), true);
+
+    expect(hashList(path1), hashList(path2));
+  });
+
+  test('test path utils 2', () {
+    final path1 = <int>[1];
+    final path2 = <int>[2];
+    expect(pathEquals(path1, path2), false);
+
+    expect(hashList(path1) != hashList(path2), true);
+  });
+
+  test('test position comparator', () {
+    final pos1 = Position(path: [1], offset: 0);
+    final pos2 = Position(path: [1], offset: 0);
+    expect(pos1 == pos2, true);
+    expect(pos1.hashCode == pos2.hashCode, true);
+  });
+
+  test('test position comparator with offset', () {
+    final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100);
+    final pos2 = Position(path: [1, 1, 1, 1, 1], offset: 100);
+    expect(pos1, pos2);
+    expect(pos1.hashCode, pos2.hashCode);
+  });
+
+  test('test position comparator false', () {
+    final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100);
+    final pos2 = Position(path: [1, 1, 2, 1, 1], offset: 100);
+    expect(pos1 == pos2, false);
+    expect(pos1.hashCode == pos2.hashCode, false);
+  });
+
+  test('test position comparator with offset false', () {
+    final pos1 = Position(path: [1, 1, 1, 1, 1], offset: 100);
+    final pos2 = Position(path: [1, 1, 1, 1, 1], offset: 101);
+    expect(pos1 == pos2, false);
+    expect(pos1.hashCode == pos2.hashCode, false);
+  });
+
+  test('test selection comparator', () {
+    final pos = Position(path: [0], offset: 0);
+    final sel = Selection.collapsed(pos);
+    expect(sel.start, sel.end);
+    expect(sel.isCollapsed(), true);
+  });
+
+  test('test selection collapse', () {
+    final start = Position(path: [0], offset: 0);
+    final end = Position(path: [0], offset: 10);
+    final sel = Selection(start: start, end: end);
+
+    final collapsedSelAtStart = sel.collapse(atStart: true);
+    expect(collapsedSelAtStart.start, start);
+    expect(collapsedSelAtStart.end, start);
+
+    final collapsedSelAtEnd = sel.collapse();
+    expect(collapsedSelAtEnd.start, end);
+    expect(collapsedSelAtEnd.end, end);
+  });
 }