Browse Source

add search operation

lucky1213 4 years ago
parent
commit
992518ba23

+ 1
- 0
packages/notus/lib/notus.dart View File

@@ -7,6 +7,7 @@ library notus;
7 7
 
8 8
 export 'src/document.dart';
9 9
 export 'src/history.dart';
10
+export 'src/search.dart' show SearchResultEntity;
10 11
 export 'src/document/attributes.dart';
11 12
 export 'src/document/block.dart';
12 13
 export 'src/document/leaf.dart';

+ 16
- 6
packages/notus/lib/src/document.dart View File

@@ -45,7 +45,7 @@ class NotusDocument {
45 45
         // _delta = Delta()..insert('\n') {
46 46
       _history = NotusHistory(),
47 47
         _delta = Delta()..insert('\n') {
48
-      _searchController = NotusSearch(_delta);
48
+      _searchController = NotusSearch();
49 49
     _loadDocument(_delta);
50 50
   }
51 51
 
@@ -53,7 +53,7 @@ class NotusDocument {
53 53
       : _heuristics = NotusHeuristics.fallback,
54 54
       _history = NotusHistory(),
55 55
         _delta = Delta.fromJson(data) {
56
-          _searchController = NotusSearch(_delta);
56
+          _searchController = NotusSearch();
57 57
     _loadDocument(_delta);
58 58
   }
59 59
 
@@ -61,7 +61,7 @@ class NotusDocument {
61 61
       : assert(delta != null),
62 62
         _heuristics = NotusHeuristics.fallback,
63 63
         _history = NotusHistory(),
64
-        _searchController = NotusSearch(delta),
64
+        _searchController = NotusSearch(),
65 65
         _delta = delta {
66 66
     _loadDocument(_delta);
67 67
   }
@@ -270,7 +270,9 @@ class NotusDocument {
270 270
     final notusChange = NotusChange(before, change, source);
271 271
     _controller.add(notusChange);
272 272
     if (history != true && search != true) _history?.handleDocChange(notusChange);
273
-    if (search != true) _searchController = NotusSearch(_delta);
273
+    // if (search != true) {
274
+    //   _searchController = NotusSearch(_delta);
275
+    // }
274 276
   }
275 277
 
276 278
   //
@@ -323,8 +325,16 @@ class NotusDocument {
323 325
     }
324 326
   }
325 327
 
326
-  void search(String text) {
327
-    _searchController.search(this, text);
328
+  SearchResultEntity search(String text) {
329
+    return _searchController.search(this, text);
330
+  }
331
+
332
+  SearchResultEntity searchPrev() {
333
+    return _searchController.prev(this);
334
+  }
335
+
336
+  SearchResultEntity searchNext() {
337
+    return _searchController.next(this);
328 338
   }
329 339
 
330 340
   Delta undo() {

+ 10
- 3
packages/notus/lib/src/document/attributes.dart View File

@@ -100,7 +100,7 @@ class NotusAttribute<T> implements NotusAttributeBuilder<T> {
100 100
   static const line = LineAttributeBuilder._();
101 101
 
102 102
   // SearchHighLight attributes
103
-  static const highlight = _HighlightAttribute();
103
+  static const highlight = HighlightAttributeBuilder._();
104 104
 
105 105
   // Color attributes
106 106
   static const color = ColorAttributeBuilder._();
@@ -381,8 +381,15 @@ class LineAttributeBuilder extends NotusAttributeBuilder<String> {
381 381
 ///
382 382
 /// There is no need to use this class directly, consider using
383 383
 /// [NotusAttribute.highlight] instead.
384
-class _HighlightAttribute extends NotusAttribute<bool> {
385
-  const _HighlightAttribute() : super._('highlight', NotusAttributeScope.inline, true);
384
+class HighlightAttributeBuilder extends NotusAttributeBuilder<double> {
385
+
386
+  static const _kColor = 'highlight';
387
+  const HighlightAttributeBuilder._()
388
+      : super._(_kColor, NotusAttributeScope.inline);
389
+
390
+  /// Creates a Highlight attribute with specified Highlight [value].
391
+  NotusAttribute<double> fromDouble(double value) =>
392
+      NotusAttribute<double>._(key, scope, value);
386 393
 }
387 394
 
388 395
 /// Builder for color attribute values.

+ 1
- 1
packages/notus/lib/src/document/node.dart View File

@@ -53,7 +53,7 @@ abstract class Node extends LinkedListEntry<Node> {
53 53
 
54 54
   /// Offset in characters of this node in the document.
55 55
   int get documentOffset {
56
-    final parentOffset = (_parent is! RootNode) ? _parent.documentOffset : 0;
56
+    final parentOffset = (_parent is! RootNode && _parent != null) ? _parent.documentOffset : 0;
57 57
     return parentOffset + offset;
58 58
   }
59 59
 

+ 69
- 7
packages/notus/lib/src/search.dart View File

@@ -7,16 +7,78 @@ import 'package:quill_delta/quill_delta.dart';
7 7
 /// used for redo or undo function
8 8
 ///
9 9
 class NotusSearch {
10
-  const NotusSearch(this.delta);
11
-  final Delta delta;
10
+  
11
+  Delta undoDelta;
12
+  List<RegExpMatch> _result = [];
13
+  SearchResultEntity _entity = const SearchResultEntity();
12 14
 
13
-  void search(NotusDocument doc, String text) {
14
-    doc.compose(delta, ChangeSource.remote, search: true);
15
+  SearchResultEntity search(NotusDocument doc, String text) {
16
+    // 恢复
17
+    for (var i = 0; i < _result.length; i++) {
18
+      RegExpMatch item = _result[i];
19
+      doc.format(item.start, item.end - item.start, NotusAttribute.highlight.unset, search: true);
20
+    }
15 21
     if (text.isNotEmpty) {
16
-      var result = RegExp(r''+ text + '', caseSensitive: false, multiLine: true).allMatches(NotusDocument.fromDelta(delta).toPlainText()).toList();
17
-      for (var item in result) {
18
-        doc.format(item.start, item.end - item.start, NotusAttribute.highlight, search: true);
22
+      int offset = 0;
23
+      _result = RegExp(r''+ text + '', caseSensitive: false, multiLine: true).allMatches(doc.toPlainText()).toList();
24
+      for (var i = 0; i < _result.length; i++) {
25
+        RegExpMatch item = _result[i];
26
+        if (i == 0) {
27
+          offset = item.end;
28
+        }
29
+        doc.format(item.start, item.end - item.start, NotusAttribute.highlight.fromDouble(i == 0 ? 0.8 : 0.5), search: true);
19 30
       }
31
+      _entity = _entity.copyWith(current: _result.isEmpty ? 0 : 1, total: _result.length, offset: offset);
32
+    } else {
33
+      _entity = SearchResultEntity();
34
+    }
35
+    return _entity;
36
+  }
37
+
38
+  SearchResultEntity next(NotusDocument doc) {
39
+    if (_entity.hasNext) {
40
+      RegExpMatch current = _result[_entity.current - 1]; // 0 => 1
41
+      doc.format(current.start, current.end - current.start, NotusAttribute.highlight.fromDouble(0.5), search: true);
42
+
43
+      RegExpMatch item = _result[_entity.current];
44
+      doc.format(item.start, item.end - item.start, NotusAttribute.highlight.fromDouble(0.8), search: true);
45
+      int next = _entity.current + 1;
46
+      _entity = _entity.copyWith(current: next, offset: item.end); // 1 => 2
20 47
     }
48
+    return _entity;
49
+  }
50
+  SearchResultEntity prev(NotusDocument doc) {
51
+    if (_entity.hasPrev) {
52
+      RegExpMatch current = _result[_entity.current - 1];
53
+      doc.format(current.start, current.end - current.start, NotusAttribute.highlight.fromDouble(0.5), search: true);
54
+      
55
+      RegExpMatch item = _result[_entity.current - 2];
56
+      doc.format(item.start, item.end - item.start, NotusAttribute.highlight.fromDouble(0.8), search: true);
57
+      int prev = _entity.current - 1;
58
+      _entity = _entity.copyWith(current: prev == 0 ? 1 : prev, offset: item.end); // 1 => 2
59
+    }
60
+    return _entity;
61
+  }
62
+}
63
+
64
+class SearchResultEntity {
65
+  final int total;
66
+  final int current;
67
+  final bool hasPrev;
68
+  final bool hasNext;
69
+  final int offset;
70
+
71
+  const SearchResultEntity({this.total = 0, this.current = 0, this.offset = 0}) : hasPrev = current > 1, hasNext = current < total;
72
+
73
+  SearchResultEntity copyWith({
74
+    int total,
75
+    int current,
76
+    int offset,
77
+  }) {
78
+    return SearchResultEntity(
79
+      total: total ?? this.total,
80
+      current: current ?? this.current,
81
+      offset: offset ?? this.offset,
82
+    );
21 83
   }
22 84
 }

+ 52
- 5
packages/zefyr/example/lib/src/full_page.dart View File

@@ -50,6 +50,7 @@ class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
50 50
   bool _editing = false;
51 51
   StreamSubscription<NotusChange> _sub;
52 52
   bool _darkTheme = false;
53
+  SearchResultEntity _searchResultEntity = SearchResultEntity();
53 54
 
54 55
   @override
55 56
   void initState() {
@@ -88,12 +89,58 @@ class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
88 89
         children: <Widget>[
89 90
           Container(
90 91
             height: 50,
91
-            child: TextField(
92
-              onChanged: (text) {
93
-                _controller.search(text);
94
-              },
92
+            child: Row(
93
+              children: <Widget>[
94
+                Expanded(
95
+                                  child: TextField(
96
+                    onChanged: (text) {
97
+                      SearchResultEntity a = _controller.search(text);
98
+                      setState(() {
99
+                        _searchResultEntity = a;
100
+                      });
101
+                    },
102
+                  ),
103
+                ),
104
+                Container(
105
+                  padding: EdgeInsets.symmetric(horizontal: 10),
106
+                  child: Text('${_searchResultEntity.current} / ${_searchResultEntity.total}'),
107
+                )
108
+              ],
95 109
             )
96 110
           ),
111
+          Container(
112
+            child: Row(
113
+              children: <Widget>[
114
+                FlatButton(
115
+                  onPressed: () {
116
+                    SearchResultEntity a = _controller.searchPrev();
117
+                    setState(() {
118
+                      _searchResultEntity = a;
119
+                    });
120
+                  },
121
+                  child: Text('上一个'),
122
+                ),
123
+                FlatButton(
124
+                  onPressed: () {
125
+                    SearchResultEntity a = _controller.searchNext();
126
+                    setState(() {
127
+                      _searchResultEntity = a;
128
+                    });
129
+                  },
130
+                  child: Text('下一个'),
131
+                ),
132
+                FlatButton(
133
+                  onPressed: () {
134
+                    SearchResultEntity a = _controller.search('');
135
+                    setState(() {
136
+                      _searchResultEntity = a;
137
+                    });
138
+                  },
139
+                  child: Text('完成'),
140
+                ),
141
+              ],
142
+            ),
143
+          ),
97 144
           Expanded(
98 145
                       child: ZefyrScaffold(
99 146
                     child: ZefyrTheme(
@@ -125,7 +172,7 @@ class _FullPageEditorScreenState extends State<FullPageEditorScreen> {
125 172
     return GestureDetector(
126 173
       behavior: HitTestBehavior.translucent,
127 174
       onTap: () {
128
-        _focusNode.unfocus();
175
+        FocusScope.of(context).unfocus();
129 176
       },
130 177
       child: Theme(
131 178
         data: ThemeData(primarySwatch: Colors.cyan),

+ 3
- 1
packages/zefyr/lib/src/widgets/common.dart View File

@@ -179,7 +179,9 @@ class _ZefyrLineState extends State<ZefyrLine> {
179 179
       result = result.merge(theme.attributeTheme.link);
180 180
     }
181 181
     if (style.contains(NotusAttribute.highlight)) {
182
-      result = result.merge(theme.attributeTheme.highlight);
182
+      result = result.merge(theme.attributeTheme.highlight.copyWith(
183
+        backgroundColor: theme.attributeTheme.highlight.backgroundColor.withOpacity(style.value<double>(NotusAttribute.highlight)),
184
+      ));
183 185
     }
184 186
     if (style.contains(NotusAttribute.color)) {
185 187
       final hexStringToColor = (String hex) {

+ 18
- 7
packages/zefyr/lib/src/widgets/controller.dart View File

@@ -223,14 +223,25 @@ class ZefyrController extends ChangeNotifier {
223 223
     }
224 224
   }
225 225
 
226
-  void search(String text) {
227
-    document.search(text);
228
-    _lastChangeSource = ChangeSource.remote;
226
+  SearchResultEntity search(String text) {
227
+    SearchResultEntity result = document.search(text);
228
+    updateSelection(TextSelection.collapsed(offset: result.offset), source: ChangeSource.remote);
229 229
     notifyListeners();
230
-    // var result = RegExp(r''+ text + '', caseSensitive: false, multiLine: true).allMatches(plainTextEditingValue.text).toList();
231
-    // for (var item in result) {
232
-    //   formatText(item.start, item.end - item.start, NotusAttribute.highlight, ignoreChange: true);
233
-    // }
230
+    return result;
231
+  }
232
+
233
+  SearchResultEntity searchPrev() {
234
+    SearchResultEntity result = document.searchPrev();
235
+    updateSelection(TextSelection.collapsed(offset: result.offset), source: ChangeSource.remote);
236
+    notifyListeners();
237
+    return result;
238
+  }
239
+
240
+  SearchResultEntity searchNext() {
241
+    SearchResultEntity result = document.searchNext();
242
+    updateSelection(TextSelection.collapsed(offset: result.offset), source: ChangeSource.remote);
243
+    notifyListeners();
244
+    return result;
234 245
   }
235 246
   
236 247
   /// Returns style of specified text range.

+ 1
- 1
packages/zefyr/lib/src/widgets/editable_text.dart View File

@@ -168,7 +168,7 @@ class _ZefyrEditableTextState extends State<ZefyrEditableText>
168 168
       controls: widget.selectionControls ?? defaultSelectionControls(context),
169 169
     ));
170 170
 
171
-    return Stack(fit: StackFit.expand, children: layers);
171
+    return Stack(fit: StackFit.passthrough, children: layers);
172 172
   }
173 173
 
174 174
   @override

+ 2
- 1
packages/zefyr/lib/src/widgets/input.dart View File

@@ -99,7 +99,8 @@ class InputConnectionController implements TextInputClient {
99 99
 
100 100
   @override
101 101
   void performAction(TextInputAction action) {
102
-    // no-op
102
+    final val = _lastKnownRemoteTextEditingValue;
103
+    onValueChanged(val.selection.start, '', '\n',TextSelection.collapsed(offset: val.selection.start + 1));
103 104
   }
104 105
 
105 106
   @override

+ 1
- 1
packages/zefyr/lib/src/widgets/scaffold.dart View File

@@ -38,7 +38,7 @@ class ZefyrScaffoldState extends State<ZefyrScaffold> {
38 38
   @override
39 39
   Widget build(BuildContext context) {
40 40
     final toolbar =
41
-        (_toolbarBuilder == null) ? Container() : _toolbarBuilder(context);
41
+        (_toolbarBuilder == null) ? Container(height: MediaQuery.of(context).viewInsets.bottom,) : _toolbarBuilder(context);
42 42
     return _ZefyrScaffoldAccess(
43 43
         scaffold: this,
44 44
         child: Column(