Skip to content

Commit 5d059e7

Browse files
yescorpalexeyinkin
andauthored
Add Custom Search (CTRL + F) (akvelon#229)
* WIP (akvelon#228) * initial commit * bug fixes * more bug fixes * overlay added * Overlay customized * bug fixes * regex strategy added * lint * regex changed to multiline * snapshot builder * search result span builder refactored * font size bug fixed * adjustments * listener added to update search result * search arrows added * isEnabled moved to search settings * changes after review * folded blocks unfolding added * fixes * isEnabled moved to search controller * currentMatch highlight added * Doc comment added * widgets separated * adjustments * cutSearchResult added * rename to visibleSearchResult * SearchNavigationController changed to be value notifier * TODO added * search result adjustments * changes after review * lint * adjustments * dispose added * Icons replaced with text * lint * some tests added * changes after review * closest match added to moveNext and movePrevious * navigation widget arrows changes * dimiss on focus change added * bug fixes * border added * changes after review * changes after review * refocus on ctrl f added * fixes * Bump version to v0.2.20 * scroll on Enter added * result sorting moved to search controller * tests added * intent and action renamed * getClosestMatch renamed * cutSearchResult tests added * lint * WIP * adjustments * adjustments * settings widget redundant code removed * changes after review * current match reset removed when focused on codefield * SearchController renamed to CodeSearchController * some tests added * strategies tests moved to a directory * tests added * search navigation with several folded blocks test * tests added * tests * invalid regexp test added * redundant method removed * changes after review * Bump version to v0.2.21 * changes after review * FocusNode used after being disposed fex * disable builtin search method added * tests fix * changes after review * setState called on widget disposal fix * comments added * durations adjusted * sort removed from searchController * changes after review * regexp test changed * navigationController tests added * lint * IgnoreIntent added * CodeController.readonly added * hardwarekeyboard keyF added to the handler * hardware keyboard check added to code controller as well * changes after review --------- Co-authored-by: Alexey Inkin <alexey.inkin@akvelon.com> Co-authored-by: Alexey Inkin <leha@inkin.ru>
1 parent 72fa1df commit 5d059e7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2734
-21
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## 0.2.21
2+
3+
* 'Enter' key in the search pattern input scrolls to the next match.
4+
5+
## 0.2.20
6+
7+
* Alpha version of search.
8+
19
## 0.2.19
210

311
* Fixed inability to change the value with `WidgetTester.enterText()` (Issue [232](https://github.com/akvelon/flutter-code-editor/issues/232)).

lib/flutter_code_editor.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export 'src/code/tokens.dart';
1414
export 'src/code_field/code_controller.dart';
1515
export 'src/code_field/code_field.dart';
1616
export 'src/code_field/editor_params.dart';
17+
export 'src/code_field/js_workarounds/js_workarounds.dart'
18+
show disableBuiltInSearchIfWeb;
1719
export 'src/code_field/text_editing_value.dart';
1820

1921
export 'src/code_modifiers/close_block_code_modifier.dart';

lib/src/code/key_event.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:flutter/services.dart';
2+
3+
extension KeyEventExtension on KeyEvent {
4+
bool isCtrlF(Set<LogicalKeyboardKey> logicalKeysPressed) {
5+
if (physicalKey != PhysicalKeyboardKey.keyF ||
6+
logicalKey != LogicalKeyboardKey.keyF) {
7+
return false;
8+
}
9+
10+
final isMetaOrControlPressed =
11+
logicalKeysPressed.contains(LogicalKeyboardKey.metaLeft) ||
12+
logicalKeysPressed.contains(LogicalKeyboardKey.metaRight) ||
13+
logicalKeysPressed.contains(LogicalKeyboardKey.controlLeft) ||
14+
logicalKeysPressed.contains(LogicalKeyboardKey.controlRight);
15+
16+
return isMetaOrControlPressed;
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:flutter/material.dart';
2+
3+
import '../code_controller.dart';
4+
5+
class CustomDismissAction extends Action<DismissIntent> {
6+
final CodeController controller;
7+
8+
CustomDismissAction({
9+
required this.controller,
10+
});
11+
12+
@override
13+
Object? invoke(DismissIntent intent) {
14+
controller.dismiss();
15+
16+
return null;
17+
}
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import 'package:flutter/material.dart';
2+
3+
import '../code_controller.dart';
4+
5+
class EnterKeyIntent extends Intent {
6+
const EnterKeyIntent();
7+
}
8+
9+
class EnterKeyAction extends Action<EnterKeyIntent> {
10+
final CodeController controller;
11+
EnterKeyAction({
12+
required this.controller,
13+
});
14+
15+
@override
16+
Object? invoke(EnterKeyIntent intent) {
17+
controller.onEnterKeyAction();
18+
return null;
19+
}
20+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'package:flutter/cupertino.dart';
2+
3+
import '../code_controller.dart';
4+
5+
class SearchIntent extends Intent {
6+
const SearchIntent();
7+
}
8+
9+
class SearchAction extends Action<SearchIntent> {
10+
final CodeController controller;
11+
12+
SearchAction({
13+
required this.controller,
14+
});
15+
16+
@override
17+
Object? invoke(SearchIntent intent) {
18+
controller.showSearch();
19+
20+
return null;
21+
}
22+
}

lib/src/code_field/code_controller.dart

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,30 @@ import 'package:collection/collection.dart';
66
import 'package:flutter/material.dart';
77
import 'package:flutter/services.dart';
88
import 'package:highlight/highlight_core.dart';
9+
import 'package:meta/meta.dart';
910

1011
import '../../flutter_code_editor.dart';
1112
import '../autocomplete/autocompleter.dart';
1213
import '../code/code_edit_result.dart';
14+
import '../code/key_event.dart';
1315
import '../history/code_history_controller.dart';
1416
import '../history/code_history_record.dart';
17+
import '../search/controller.dart';
18+
import '../search/result.dart';
19+
import '../search/search_navigation_controller.dart';
20+
import '../search/settings_controller.dart';
1521
import '../single_line_comments/parser/single_line_comments.dart';
1622
import '../wip/autocomplete/popup_controller.dart';
1723
import 'actions/comment_uncomment.dart';
1824
import 'actions/copy.dart';
25+
import 'actions/dismiss.dart';
26+
import 'actions/enter_key.dart';
1927
import 'actions/indent.dart';
2028
import 'actions/outdent.dart';
2129
import 'actions/redo.dart';
30+
import 'actions/search.dart';
2231
import 'actions/undo.dart';
32+
import 'search_result_highlighted_builder.dart';
2333
import 'span_builder.dart';
2434

2535
class CodeController extends TextEditingController {
@@ -80,6 +90,11 @@ class CodeController extends TextEditingController {
8090
///If it is not empty, all another code except specified will be hidden.
8191
Set<String> _visibleSectionNames = {};
8292

93+
/// Makes the text un-editable, but allows to set the full text.
94+
/// Focusing and moving the selection inside of a [CodeField] will
95+
/// still be possible.
96+
final bool readonly;
97+
8398
String get languageId => _languageId;
8499

85100
Code _code;
@@ -91,6 +106,17 @@ class CodeController extends TextEditingController {
91106
final autocompleter = Autocompleter();
92107
late final historyController = CodeHistoryController(codeController: this);
93108

109+
@internal
110+
late final searchController = CodeSearchController(codeController: this);
111+
112+
SearchSettingsController get _searchSettingsController =>
113+
searchController.settingsController;
114+
SearchNavigationController get _searchNavigationController =>
115+
searchController.navigationController;
116+
117+
@internal
118+
SearchResult fullSearchResult = SearchResult.empty;
119+
94120
/// The last [TextSpan] returned from [buildTextSpan].
95121
///
96122
/// This can be used in tests to make sure that the updated text was actually
@@ -105,6 +131,9 @@ class CodeController extends TextEditingController {
105131
OutdentIntent: OutdentIntentAction(controller: this),
106132
RedoTextIntent: RedoAction(controller: this),
107133
UndoTextIntent: UndoAction(controller: this),
134+
SearchIntent: SearchAction(controller: this),
135+
DismissIntent: CustomDismissAction(controller: this),
136+
EnterKeyIntent: EnterKeyAction(controller: this),
108137
};
109138

110139
CodeController({
@@ -118,6 +147,7 @@ class CodeController extends TextEditingController {
118147
Map<String, TextStyle>? theme,
119148
this.analysisResult = const AnalysisResult(issues: []),
120149
this.patternMap,
150+
this.readonly = false,
121151
this.stringMap,
122152
this.params = const EditorParams(),
123153
this.modifiers = const [
@@ -135,6 +165,11 @@ class CodeController extends TextEditingController {
135165
fullText = text ?? '';
136166

137167
addListener(_scheduleAnalysis);
168+
addListener(_updateSearchResult);
169+
_searchSettingsController.addListener(_updateSearchResult);
170+
// This listener is called when search controller notifies about
171+
// showing or hiding the search popup.
172+
searchController.addListener(_updateSearchResult);
138173

139174
// Create modifier map
140175
for (final el in modifiers) {
@@ -158,6 +193,20 @@ class CodeController extends TextEditingController {
158193
unawaited(analyzeCode());
159194
}
160195

196+
void _updateSearchResult() {
197+
final result = searchController.search(
198+
code,
199+
settings: _searchSettingsController.value,
200+
);
201+
202+
if (result == fullSearchResult) {
203+
return;
204+
}
205+
206+
fullSearchResult = result;
207+
notifyListeners();
208+
}
209+
161210
void _scheduleAnalysis() {
162211
_debounce?.cancel();
163212

@@ -268,6 +317,11 @@ class CodeController extends TextEditingController {
268317
}
269318

270319
KeyEventResult _onKeyDownRepeat(KeyEvent event) {
320+
if (event.isCtrlF(HardwareKeyboard.instance.logicalKeysPressed)) {
321+
showSearch();
322+
return KeyEventResult.handled;
323+
}
324+
271325
if (popupController.shouldShow) {
272326
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
273327
popupController.scrollByArrow(ScrollDirection.up);
@@ -277,19 +331,34 @@ class CodeController extends TextEditingController {
277331
popupController.scrollByArrow(ScrollDirection.down);
278332
return KeyEventResult.handled;
279333
}
280-
if (event.logicalKey == LogicalKeyboardKey.enter) {
281-
insertSelectedWord();
282-
return KeyEventResult.handled;
283-
}
284-
if (event.logicalKey == LogicalKeyboardKey.escape) {
285-
popupController.hide();
286-
return KeyEventResult.handled;
287-
}
288334
}
289335

290336
return KeyEventResult.ignored; // The framework will handle.
291337
}
292338

339+
void onEnterKeyAction() {
340+
if (popupController.shouldShow) {
341+
insertSelectedWord();
342+
return;
343+
}
344+
345+
final currentMatchIndex =
346+
_searchNavigationController.value.currentMatchIndex;
347+
348+
if (searchController.shouldShow && currentMatchIndex != null) {
349+
final fullSelection = code.hiddenRanges.recoverSelection(selection);
350+
final currentMatch = fullSearchResult.matches[currentMatchIndex];
351+
352+
if (fullSelection.start == currentMatch.start &&
353+
fullSelection.end == currentMatch.end) {
354+
_searchNavigationController.moveNext();
355+
return;
356+
}
357+
}
358+
359+
insertStr('\n');
360+
}
361+
293362
/// Inserts the word selected from the list of completions
294363
void insertSelectedWord() {
295364
final previousSelection = selection;
@@ -585,6 +654,10 @@ class CodeController extends TextEditingController {
585654
void modifySelectedLines(
586655
String Function(String line) modifierCallback,
587656
) {
657+
if (readonly) {
658+
return;
659+
}
660+
588661
if (selection.start == -1 || selection.end == -1) {
589662
return;
590663
}
@@ -660,6 +733,10 @@ class CodeController extends TextEditingController {
660733
Code get code => _code;
661734

662735
CodeEditResult? _getEditResultNotBreakingReadOnly(TextEditingValue newValue) {
736+
if (readonly) {
737+
return null;
738+
}
739+
663740
final editResult = _code.getEditResult(value.selection, newValue);
664741
if (!_code.isReadOnlyInLineRange(editResult.linesChanged)) {
665742
return editResult;
@@ -802,8 +879,23 @@ class CodeController extends TextEditingController {
802879
TextStyle? style,
803880
bool? withComposing,
804881
}) {
882+
final spanBeforeSearch = _createTextSpan(
883+
context: context,
884+
style: style,
885+
);
886+
887+
final visibleSearchResult =
888+
_code.hiddenRanges.cutSearchResult(fullSearchResult);
889+
805890
// TODO(alexeyinkin): Return cached if the value did not change, https://github.com/akvelon/flutter-code-editor/issues/127
806-
return lastTextSpan = _createTextSpan(context: context, style: style);
891+
lastTextSpan = SearchResultHighlightedBuilder(
892+
searchResult: visibleSearchResult,
893+
rootStyle: style,
894+
textSpan: spanBeforeSearch,
895+
searchNavigationState: _searchNavigationController.value,
896+
).build();
897+
898+
return lastTextSpan!;
807899
}
808900

809901
TextSpan _createTextSpan({
@@ -864,10 +956,30 @@ class CodeController extends TextEditingController {
864956
return CodeTheme.of(context) ?? CodeThemeData();
865957
}
866958

959+
void dismiss() {
960+
_dismissSuggestions();
961+
_dismissSearch();
962+
}
963+
964+
void _dismissSuggestions() {
965+
if (popupController.enabled) {
966+
popupController.hide();
967+
}
968+
}
969+
970+
void _dismissSearch() {
971+
searchController.hideSearch(returnFocusToCodeField: true);
972+
}
973+
974+
void showSearch() {
975+
searchController.showSearch();
976+
}
977+
867978
@override
868979
void dispose() {
869980
_debounce?.cancel();
870981
historyController.dispose();
982+
searchController.dispose();
871983

872984
super.dispose();
873985
}

0 commit comments

Comments
 (0)