Skip to content

Commit

Permalink
merge from main
Browse files Browse the repository at this point in the history
  • Loading branch information
Malarg committed May 11, 2023
2 parents 24cc670 + b4e7714 commit ef71caf
Show file tree
Hide file tree
Showing 45 changed files with 2,836 additions and 24 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
## 0.2.23

* Added `CodeController.readOnly`.

## 0.2.22

* Fixed most of the search bugs (Issue [228](https://github.com/akvelon/flutter-code-editor/issues/228)).

## 0.2.21

* 'Enter' key in the search pattern input scrolls to the next match.

## 0.2.20

* Alpha version of search.

## 0.2.19

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

## 0.2.18

* Fixed the suggestion box horizontal offset (Issue [224](https://github.com/akvelon/flutter-code-editor/issues/224)).
Expand Down
3 changes: 3 additions & 0 deletions example/lib/03.change_language_theme/constants.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:highlight/languages/dart.dart';
import 'package:highlight/languages/go.dart';
import 'package:highlight/languages/java.dart';
import 'package:highlight/languages/php.dart';
import 'package:highlight/languages/python.dart';
import 'package:highlight/languages/scala.dart';
import 'package:highlight/languages/yaml.dart';
Expand All @@ -9,6 +10,7 @@ final builtinLanguages = {
'dart': dart,
'go': go,
'java': java,
'php': php,
'python': python,
'scala': scala,
'yaml': yaml,
Expand All @@ -18,6 +20,7 @@ const languageList = <String>[
'dart',
'go',
'java',
'php',
'python',
'scala',
'yaml',
Expand Down
2 changes: 2 additions & 0 deletions lib/flutter_code_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export 'src/code/tokens.dart';
export 'src/code_field/code_controller.dart';
export 'src/code_field/code_field.dart';
export 'src/code_field/editor_params.dart';
export 'src/code_field/js_workarounds/js_workarounds.dart'
show disableBuiltInSearchIfWeb;
export 'src/code_field/text_editing_value.dart';

export 'src/code_modifiers/close_block_code_modifier.dart';
Expand Down
7 changes: 4 additions & 3 deletions lib/src/code/code.dart
Original file line number Diff line number Diff line change
Expand Up @@ -381,10 +381,11 @@ class Code {
// If there is any folded block that is going to be removed
// because of `backspace` or `delete`, return unchanged text.
if (oldSelection.isCollapsed &&
visibleAfter.text.length == visibleText.length - 1 &&
foldedBlocks.any(
(element) =>
element.lastLine >= firstChangedLine &&
element.lastLine <= lastChangedLine,
(block) =>
block.lastLine >= firstChangedLine &&
block.lastLine <= lastChangedLine,
)) {
return CodeEditResult(
fullTextAfter: text,
Expand Down
18 changes: 18 additions & 0 deletions lib/src/code/key_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:flutter/services.dart';

extension KeyEventExtension on KeyEvent {
bool isCtrlF(Set<LogicalKeyboardKey> logicalKeysPressed) {
if (physicalKey != PhysicalKeyboardKey.keyF ||
logicalKey != LogicalKeyboardKey.keyF) {
return false;
}

final isMetaOrControlPressed =
logicalKeysPressed.contains(LogicalKeyboardKey.metaLeft) ||
logicalKeysPressed.contains(LogicalKeyboardKey.metaRight) ||
logicalKeysPressed.contains(LogicalKeyboardKey.controlLeft) ||
logicalKeysPressed.contains(LogicalKeyboardKey.controlRight);

return isMetaOrControlPressed;
}
}
18 changes: 18 additions & 0 deletions lib/src/code_field/actions/dismiss.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';

import '../code_controller.dart';

class CustomDismissAction extends Action<DismissIntent> {
final CodeController controller;

CustomDismissAction({
required this.controller,
});

@override
Object? invoke(DismissIntent intent) {
controller.dismiss();

return null;
}
}
20 changes: 20 additions & 0 deletions lib/src/code_field/actions/enter_key.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';

import '../code_controller.dart';

class EnterKeyIntent extends Intent {
const EnterKeyIntent();
}

class EnterKeyAction extends Action<EnterKeyIntent> {
final CodeController controller;
EnterKeyAction({
required this.controller,
});

@override
Object? invoke(EnterKeyIntent intent) {
controller.onEnterKeyAction();
return null;
}
}
22 changes: 22 additions & 0 deletions lib/src/code_field/actions/search.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:flutter/cupertino.dart';

import '../code_controller.dart';

class SearchIntent extends Intent {
const SearchIntent();
}

class SearchAction extends Action<SearchIntent> {
final CodeController controller;

SearchAction({
required this.controller,
});

@override
Object? invoke(SearchIntent intent) {
controller.showSearch();

return null;
}
}
130 changes: 121 additions & 9 deletions lib/src/code_field/code_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,31 @@ import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:highlight/highlight_core.dart';
import 'package:meta/meta.dart';

import '../../flutter_code_editor.dart';
import '../autocomplete/autocompleter.dart';
import '../code/code_edit_result.dart';
import '../code/key_event.dart';
import '../code_modifiers/insertion.dart';
import '../history/code_history_controller.dart';
import '../history/code_history_record.dart';
import '../search/controller.dart';
import '../search/result.dart';
import '../search/search_navigation_controller.dart';
import '../search/settings_controller.dart';
import '../single_line_comments/parser/single_line_comments.dart';
import '../wip/autocomplete/popup_controller.dart';
import 'actions/comment_uncomment.dart';
import 'actions/copy.dart';
import 'actions/dismiss.dart';
import 'actions/enter_key.dart';
import 'actions/indent.dart';
import 'actions/outdent.dart';
import 'actions/redo.dart';
import 'actions/search.dart';
import 'actions/undo.dart';
import 'search_result_highlighted_builder.dart';
import 'span_builder.dart';

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

/// Makes the text un-editable, but allows to set the full text.
/// Focusing and moving the selection inside of a [CodeField] will
/// still be possible.
final bool readOnly;

String get languageId => _languageId;

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

@internal
late final searchController = CodeSearchController(codeController: this);

SearchSettingsController get _searchSettingsController =>
searchController.settingsController;
SearchNavigationController get _searchNavigationController =>
searchController.navigationController;

@internal
SearchResult fullSearchResult = SearchResult.empty;

/// The last [TextSpan] returned from [buildTextSpan].
///
/// This can be used in tests to make sure that the updated text was actually
Expand All @@ -106,6 +132,9 @@ class CodeController extends TextEditingController {
OutdentIntent: OutdentIntentAction(controller: this),
RedoTextIntent: RedoAction(controller: this),
UndoTextIntent: UndoAction(controller: this),
SearchIntent: SearchAction(controller: this),
DismissIntent: CustomDismissAction(controller: this),
EnterKeyIntent: EnterKeyAction(controller: this),
};

CodeController({
Expand All @@ -119,6 +148,7 @@ class CodeController extends TextEditingController {
Map<String, TextStyle>? theme,
this.analysisResult = const AnalysisResult(issues: []),
this.patternMap,
this.readOnly = false,
this.stringMap,
this.params = const EditorParams(),
this.modifiers = const [
Expand All @@ -142,6 +172,11 @@ class CodeController extends TextEditingController {
fullText = text ?? '';

addListener(_scheduleAnalysis);
addListener(_updateSearchResult);
_searchSettingsController.addListener(_updateSearchResult);
// This listener is called when search controller notifies about
// showing or hiding the search popup.
searchController.addListener(_updateSearchResult);

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

void _updateSearchResult() {
final result = searchController.search(
code,
settings: _searchSettingsController.value,
);

if (result == fullSearchResult) {
return;
}

fullSearchResult = result;
notifyListeners();
}

void _scheduleAnalysis() {
_debounce?.cancel();

Expand Down Expand Up @@ -275,6 +324,11 @@ class CodeController extends TextEditingController {
}

KeyEventResult _onKeyDownRepeat(KeyEvent event) {
if (event.isCtrlF(HardwareKeyboard.instance.logicalKeysPressed)) {
showSearch();
return KeyEventResult.handled;
}

if (popupController.shouldShow) {
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
popupController.scrollByArrow(ScrollDirection.up);
Expand All @@ -284,19 +338,34 @@ class CodeController extends TextEditingController {
popupController.scrollByArrow(ScrollDirection.down);
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.enter) {
insertSelectedWord();
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.escape) {
popupController.hide();
return KeyEventResult.handled;
}
}

return KeyEventResult.ignored; // The framework will handle.
}

void onEnterKeyAction() {
if (popupController.shouldShow) {
insertSelectedWord();
return;
}

final currentMatchIndex =
_searchNavigationController.value.currentMatchIndex;

if (searchController.shouldShow && currentMatchIndex != null) {
final fullSelection = code.hiddenRanges.recoverSelection(selection);
final currentMatch = fullSearchResult.matches[currentMatchIndex];

if (fullSelection.start == currentMatch.start &&
fullSelection.end == currentMatch.end) {
_searchNavigationController.moveNext();
return;
}
}

insertStr('\n');
}

/// Inserts the word selected from the list of completions
void insertSelectedWord() {
final previousSelection = selection;
Expand Down Expand Up @@ -592,6 +661,10 @@ class CodeController extends TextEditingController {
void modifySelectedLines(
String Function(String line) modifierCallback,
) {
if (readOnly) {
return;
}

if (selection.start == -1 || selection.end == -1) {
return;
}
Expand Down Expand Up @@ -667,6 +740,10 @@ class CodeController extends TextEditingController {
Code get code => _code;

CodeEditResult? _getEditResultNotBreakingReadOnly(TextEditingValue newValue) {
if (readOnly) {
return null;
}

final editResult = _code.getEditResult(value.selection, newValue);
if (!_code.isReadOnlyInLineRange(editResult.linesChanged)) {
return editResult;
Expand Down Expand Up @@ -809,8 +886,23 @@ class CodeController extends TextEditingController {
TextStyle? style,
bool? withComposing,
}) {
final spanBeforeSearch = _createTextSpan(
context: context,
style: style,
);

final visibleSearchResult =
_code.hiddenRanges.cutSearchResult(fullSearchResult);

// TODO(alexeyinkin): Return cached if the value did not change, https://github.com/akvelon/flutter-code-editor/issues/127
return lastTextSpan = _createTextSpan(context: context, style: style);
lastTextSpan = SearchResultHighlightedBuilder(
searchResult: visibleSearchResult,
rootStyle: style,
textSpan: spanBeforeSearch,
searchNavigationState: _searchNavigationController.value,
).build();

return lastTextSpan!;
}

TextSpan _createTextSpan({
Expand Down Expand Up @@ -871,10 +963,30 @@ class CodeController extends TextEditingController {
return CodeTheme.of(context) ?? CodeThemeData();
}

void dismiss() {
_dismissSuggestions();
_dismissSearch();
}

void _dismissSuggestions() {
if (popupController.enabled) {
popupController.hide();
}
}

void _dismissSearch() {
searchController.hideSearch(returnFocusToCodeField: true);
}

void showSearch() {
searchController.showSearch();
}

@override
void dispose() {
_debounce?.cancel();
historyController.dispose();
searchController.dispose();

super.dispose();
}
Expand Down
Loading

0 comments on commit ef71caf

Please sign in to comment.