Skip to content

Commit ef71caf

Browse files
committed
merge from main
2 parents 24cc670 + b4e7714 commit ef71caf

Some content is hidden

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

45 files changed

+2836
-24
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,23 @@
1+
## 0.2.23
2+
3+
* Added `CodeController.readOnly`.
4+
5+
## 0.2.22
6+
7+
* Fixed most of the search bugs (Issue [228](https://github.com/akvelon/flutter-code-editor/issues/228)).
8+
9+
## 0.2.21
10+
11+
* 'Enter' key in the search pattern input scrolls to the next match.
12+
13+
## 0.2.20
14+
15+
* Alpha version of search.
16+
17+
## 0.2.19
18+
19+
* Fixed inability to change the value with `WidgetTester.enterText()` (Issue [232](https://github.com/akvelon/flutter-code-editor/issues/232)).
20+
121
## 0.2.18
222

323
* Fixed the suggestion box horizontal offset (Issue [224](https://github.com/akvelon/flutter-code-editor/issues/224)).

example/lib/03.change_language_theme/constants.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:highlight/languages/dart.dart';
22
import 'package:highlight/languages/go.dart';
33
import 'package:highlight/languages/java.dart';
4+
import 'package:highlight/languages/php.dart';
45
import 'package:highlight/languages/python.dart';
56
import 'package:highlight/languages/scala.dart';
67
import 'package:highlight/languages/yaml.dart';
@@ -9,6 +10,7 @@ final builtinLanguages = {
910
'dart': dart,
1011
'go': go,
1112
'java': java,
13+
'php': php,
1214
'python': python,
1315
'scala': scala,
1416
'yaml': yaml,
@@ -18,6 +20,7 @@ const languageList = <String>[
1820
'dart',
1921
'go',
2022
'java',
23+
'php',
2124
'python',
2225
'scala',
2326
'yaml',

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/code.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,10 +381,11 @@ class Code {
381381
// If there is any folded block that is going to be removed
382382
// because of `backspace` or `delete`, return unchanged text.
383383
if (oldSelection.isCollapsed &&
384+
visibleAfter.text.length == visibleText.length - 1 &&
384385
foldedBlocks.any(
385-
(element) =>
386-
element.lastLine >= firstChangedLine &&
387-
element.lastLine <= lastChangedLine,
386+
(block) =>
387+
block.lastLine >= firstChangedLine &&
388+
block.lastLine <= lastChangedLine,
388389
)) {
389390
return CodeEditResult(
390391
fullTextAfter: text,

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,21 +6,31 @@ 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 '../code_modifiers/insertion.dart';
1416
import '../history/code_history_controller.dart';
1517
import '../history/code_history_record.dart';
18+
import '../search/controller.dart';
19+
import '../search/result.dart';
20+
import '../search/search_navigation_controller.dart';
21+
import '../search/settings_controller.dart';
1622
import '../single_line_comments/parser/single_line_comments.dart';
1723
import '../wip/autocomplete/popup_controller.dart';
1824
import 'actions/comment_uncomment.dart';
1925
import 'actions/copy.dart';
26+
import 'actions/dismiss.dart';
27+
import 'actions/enter_key.dart';
2028
import 'actions/indent.dart';
2129
import 'actions/outdent.dart';
2230
import 'actions/redo.dart';
31+
import 'actions/search.dart';
2332
import 'actions/undo.dart';
33+
import 'search_result_highlighted_builder.dart';
2434
import 'span_builder.dart';
2535

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

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

86101
Code _code;
@@ -92,6 +107,17 @@ class CodeController extends TextEditingController {
92107
final autocompleter = Autocompleter();
93108
late final historyController = CodeHistoryController(codeController: this);
94109

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

111140
CodeController({
@@ -119,6 +148,7 @@ class CodeController extends TextEditingController {
119148
Map<String, TextStyle>? theme,
120149
this.analysisResult = const AnalysisResult(issues: []),
121150
this.patternMap,
151+
this.readOnly = false,
122152
this.stringMap,
123153
this.params = const EditorParams(),
124154
this.modifiers = const [
@@ -142,6 +172,11 @@ class CodeController extends TextEditingController {
142172
fullText = text ?? '';
143173

144174
addListener(_scheduleAnalysis);
175+
addListener(_updateSearchResult);
176+
_searchSettingsController.addListener(_updateSearchResult);
177+
// This listener is called when search controller notifies about
178+
// showing or hiding the search popup.
179+
searchController.addListener(_updateSearchResult);
145180

146181
// Create modifier map
147182
for (final el in modifiers) {
@@ -165,6 +200,20 @@ class CodeController extends TextEditingController {
165200
unawaited(analyzeCode());
166201
}
167202

203+
void _updateSearchResult() {
204+
final result = searchController.search(
205+
code,
206+
settings: _searchSettingsController.value,
207+
);
208+
209+
if (result == fullSearchResult) {
210+
return;
211+
}
212+
213+
fullSearchResult = result;
214+
notifyListeners();
215+
}
216+
168217
void _scheduleAnalysis() {
169218
_debounce?.cancel();
170219

@@ -275,6 +324,11 @@ class CodeController extends TextEditingController {
275324
}
276325

277326
KeyEventResult _onKeyDownRepeat(KeyEvent event) {
327+
if (event.isCtrlF(HardwareKeyboard.instance.logicalKeysPressed)) {
328+
showSearch();
329+
return KeyEventResult.handled;
330+
}
331+
278332
if (popupController.shouldShow) {
279333
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
280334
popupController.scrollByArrow(ScrollDirection.up);
@@ -284,19 +338,34 @@ class CodeController extends TextEditingController {
284338
popupController.scrollByArrow(ScrollDirection.down);
285339
return KeyEventResult.handled;
286340
}
287-
if (event.logicalKey == LogicalKeyboardKey.enter) {
288-
insertSelectedWord();
289-
return KeyEventResult.handled;
290-
}
291-
if (event.logicalKey == LogicalKeyboardKey.escape) {
292-
popupController.hide();
293-
return KeyEventResult.handled;
294-
}
295341
}
296342

297343
return KeyEventResult.ignored; // The framework will handle.
298344
}
299345

346+
void onEnterKeyAction() {
347+
if (popupController.shouldShow) {
348+
insertSelectedWord();
349+
return;
350+
}
351+
352+
final currentMatchIndex =
353+
_searchNavigationController.value.currentMatchIndex;
354+
355+
if (searchController.shouldShow && currentMatchIndex != null) {
356+
final fullSelection = code.hiddenRanges.recoverSelection(selection);
357+
final currentMatch = fullSearchResult.matches[currentMatchIndex];
358+
359+
if (fullSelection.start == currentMatch.start &&
360+
fullSelection.end == currentMatch.end) {
361+
_searchNavigationController.moveNext();
362+
return;
363+
}
364+
}
365+
366+
insertStr('\n');
367+
}
368+
300369
/// Inserts the word selected from the list of completions
301370
void insertSelectedWord() {
302371
final previousSelection = selection;
@@ -592,6 +661,10 @@ class CodeController extends TextEditingController {
592661
void modifySelectedLines(
593662
String Function(String line) modifierCallback,
594663
) {
664+
if (readOnly) {
665+
return;
666+
}
667+
595668
if (selection.start == -1 || selection.end == -1) {
596669
return;
597670
}
@@ -667,6 +740,10 @@ class CodeController extends TextEditingController {
667740
Code get code => _code;
668741

669742
CodeEditResult? _getEditResultNotBreakingReadOnly(TextEditingValue newValue) {
743+
if (readOnly) {
744+
return null;
745+
}
746+
670747
final editResult = _code.getEditResult(value.selection, newValue);
671748
if (!_code.isReadOnlyInLineRange(editResult.linesChanged)) {
672749
return editResult;
@@ -809,8 +886,23 @@ class CodeController extends TextEditingController {
809886
TextStyle? style,
810887
bool? withComposing,
811888
}) {
889+
final spanBeforeSearch = _createTextSpan(
890+
context: context,
891+
style: style,
892+
);
893+
894+
final visibleSearchResult =
895+
_code.hiddenRanges.cutSearchResult(fullSearchResult);
896+
812897
// TODO(alexeyinkin): Return cached if the value did not change, https://github.com/akvelon/flutter-code-editor/issues/127
813-
return lastTextSpan = _createTextSpan(context: context, style: style);
898+
lastTextSpan = SearchResultHighlightedBuilder(
899+
searchResult: visibleSearchResult,
900+
rootStyle: style,
901+
textSpan: spanBeforeSearch,
902+
searchNavigationState: _searchNavigationController.value,
903+
).build();
904+
905+
return lastTextSpan!;
814906
}
815907

816908
TextSpan _createTextSpan({
@@ -871,10 +963,30 @@ class CodeController extends TextEditingController {
871963
return CodeTheme.of(context) ?? CodeThemeData();
872964
}
873965

966+
void dismiss() {
967+
_dismissSuggestions();
968+
_dismissSearch();
969+
}
970+
971+
void _dismissSuggestions() {
972+
if (popupController.enabled) {
973+
popupController.hide();
974+
}
975+
}
976+
977+
void _dismissSearch() {
978+
searchController.hideSearch(returnFocusToCodeField: true);
979+
}
980+
981+
void showSearch() {
982+
searchController.showSearch();
983+
}
984+
874985
@override
875986
void dispose() {
876987
_debounce?.cancel();
877988
historyController.dispose();
989+
searchController.dispose();
878990

879991
super.dispose();
880992
}

0 commit comments

Comments
 (0)