Browse Source

add search operation

lucky1213 4 years ago
parent
commit
992518ba23

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

7
 
7
 
8
 export 'src/document.dart';
8
 export 'src/document.dart';
9
 export 'src/history.dart';
9
 export 'src/history.dart';
10
+export 'src/search.dart' show SearchResultEntity;
10
 export 'src/document/attributes.dart';
11
 export 'src/document/attributes.dart';
11
 export 'src/document/block.dart';
12
 export 'src/document/block.dart';
12
 export 'src/document/leaf.dart';
13
 export 'src/document/leaf.dart';

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

45
         // _delta = Delta()..insert('\n') {
45
         // _delta = Delta()..insert('\n') {
46
       _history = NotusHistory(),
46
       _history = NotusHistory(),
47
         _delta = Delta()..insert('\n') {
47
         _delta = Delta()..insert('\n') {
48
-      _searchController = NotusSearch(_delta);
48
+      _searchController = NotusSearch();
49
     _loadDocument(_delta);
49
     _loadDocument(_delta);
50
   }
50
   }
51
 
51
 
53
       : _heuristics = NotusHeuristics.fallback,
53
       : _heuristics = NotusHeuristics.fallback,
54
       _history = NotusHistory(),
54
       _history = NotusHistory(),
55
         _delta = Delta.fromJson(data) {
55
         _delta = Delta.fromJson(data) {
56
-          _searchController = NotusSearch(_delta);
56
+          _searchController = NotusSearch();
57
     _loadDocument(_delta);
57
     _loadDocument(_delta);
58
   }
58
   }
59
 
59
 
61
       : assert(delta != null),
61
       : assert(delta != null),
62
         _heuristics = NotusHeuristics.fallback,
62
         _heuristics = NotusHeuristics.fallback,
63
         _history = NotusHistory(),
63
         _history = NotusHistory(),
64
-        _searchController = NotusSearch(delta),
64
+        _searchController = NotusSearch(),
65
         _delta = delta {
65
         _delta = delta {
66
     _loadDocument(_delta);
66
     _loadDocument(_delta);
67
   }
67
   }
270
     final notusChange = NotusChange(before, change, source);
270
     final notusChange = NotusChange(before, change, source);
271
     _controller.add(notusChange);
271
     _controller.add(notusChange);
272
     if (history != true && search != true) _history?.handleDocChange(notusChange);
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
     }
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
   Delta undo() {
340
   Delta undo() {

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

100
   static const line = LineAttributeBuilder._();
100
   static const line = LineAttributeBuilder._();
101
 
101
 
102
   // SearchHighLight attributes
102
   // SearchHighLight attributes
103
-  static const highlight = _HighlightAttribute();
103
+  static const highlight = HighlightAttributeBuilder._();
104
 
104
 
105
   // Color attributes
105
   // Color attributes
106
   static const color = ColorAttributeBuilder._();
106
   static const color = ColorAttributeBuilder._();
381
 ///
381
 ///
382
 /// There is no need to use this class directly, consider using
382
 /// There is no need to use this class directly, consider using
383
 /// [NotusAttribute.highlight] instead.
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
 /// Builder for color attribute values.
395
 /// Builder for color attribute values.

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

53
 
53
 
54
   /// Offset in characters of this node in the document.
54
   /// Offset in characters of this node in the document.
55
   int get documentOffset {
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
     return parentOffset + offset;
57
     return parentOffset + offset;
58
   }
58
   }
59
 
59
 

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

7
 /// used for redo or undo function
7
 /// used for redo or undo function
8
 ///
8
 ///
9
 class NotusSearch {
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
     if (text.isNotEmpty) {
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
   bool _editing = false;
50
   bool _editing = false;
51
   StreamSubscription<NotusChange> _sub;
51
   StreamSubscription<NotusChange> _sub;
52
   bool _darkTheme = false;
52
   bool _darkTheme = false;
53
+  SearchResultEntity _searchResultEntity = SearchResultEntity();
53
 
54
 
54
   @override
55
   @override
55
   void initState() {
56
   void initState() {
88
         children: <Widget>[
89
         children: <Widget>[
89
           Container(
90
           Container(
90
             height: 50,
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
           Expanded(
144
           Expanded(
98
                       child: ZefyrScaffold(
145
                       child: ZefyrScaffold(
99
                     child: ZefyrTheme(
146
                     child: ZefyrTheme(
125
     return GestureDetector(
172
     return GestureDetector(
126
       behavior: HitTestBehavior.translucent,
173
       behavior: HitTestBehavior.translucent,
127
       onTap: () {
174
       onTap: () {
128
-        _focusNode.unfocus();
175
+        FocusScope.of(context).unfocus();
129
       },
176
       },
130
       child: Theme(
177
       child: Theme(
131
         data: ThemeData(primarySwatch: Colors.cyan),
178
         data: ThemeData(primarySwatch: Colors.cyan),

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

179
       result = result.merge(theme.attributeTheme.link);
179
       result = result.merge(theme.attributeTheme.link);
180
     }
180
     }
181
     if (style.contains(NotusAttribute.highlight)) {
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
     if (style.contains(NotusAttribute.color)) {
186
     if (style.contains(NotusAttribute.color)) {
185
       final hexStringToColor = (String hex) {
187
       final hexStringToColor = (String hex) {

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

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
     notifyListeners();
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
   /// Returns style of specified text range.
247
   /// Returns style of specified text range.

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

168
       controls: widget.selectionControls ?? defaultSelectionControls(context),
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
   @override
174
   @override

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

99
 
99
 
100
   @override
100
   @override
101
   void performAction(TextInputAction action) {
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
   @override
106
   @override

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

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