Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/Intelligent highlight #50

Merged
merged 35 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
c83dff0
intelligent-highlight
solid-danylokhvan Jun 27, 2023
e5b96cb
shift offset from right side case
solid-danylokhvan Jun 29, 2023
d3f8522
add _filterMistakesOnChanged function
solid-danylokhvan Jun 29, 2023
19c3bb1
em unnecessary check
solid-danylokhvan Jun 29, 2023
1586dc4
less duration & find mistakes action after debounce
solid-danylokhvan Jun 29, 2023
1920796
add comments
solid-danylokhvan Jun 29, 2023
d786ca5
format code
solid-danylokhvan Jun 29, 2023
1f0594e
get rid from debounce
solid-danylokhvan Jun 29, 2023
f45d462
rm unnecessary comment & reset the variable
solid-danylokhvan Jun 30, 2023
0dad1d9
rm redunant comments
solid-danylokhvan Jul 3, 2023
b20001e
reduce nesting
solid-danylokhvan Jul 3, 2023
4180732
less nested
solid-danylokhvan Jul 3, 2023
d58f231
add selection extension
solid-danylokhvan Jul 3, 2023
a7c6337
code formatting
solid-danylokhvan Jul 3, 2023
4469b7c
add request id in debounce
solid-danylokhvan Jul 3, 2023
4e461b7
add closed range
solid-danylokhvan Jul 4, 2023
1a5c991
process latest operation
solid-danylokhvan Jul 4, 2023
61966e3
rm suffix
solid-danylokhvan Jul 4, 2023
5a3e818
code formatting
solid-danylokhvan Jul 4, 2023
a58e15d
rm nullable
solid-danylokhvan Jul 4, 2023
a8980ce
Merge branch 'main' into feat/intelligent-highlight-2
solid-danylokhvan Jul 4, 2023
96e3e69
Merge branch 'main' into feat/intelligent-highlight-2
solid-danylokhvan Jul 4, 2023
1207c4c
is empty check
solid-danylokhvan Jul 4, 2023
d32bef5
do renerator function
solid-danylokhvan Jul 5, 2023
ef2093e
rm unnecessary remove
solid-danylokhvan Jul 5, 2023
f10ce27
handle selection and caret cursor differently
solid-danylokhvan Jul 5, 2023
0315499
handle selection and cursor caret differently
solid-danylokhvan Jul 5, 2023
2e3192b
add comments
solid-danylokhvan Jul 5, 2023
d53d912
add comments
solid-danylokhvan Jul 5, 2023
9b3258f
reorder
solid-danylokhvan Jul 5, 2023
f504280
Refactor adfer review
solid-danylokhvan Jul 6, 2023
b9f27f1
Merge branch 'main' into feat/intelligent-highlight-2
solid-danylokhvan Jul 6, 2023
84041ff
short-circuit where possible, group statements
solid-yuriiprykhodko Jul 7, 2023
0ff690b
sort mistakes by offset
solid-danylokhvan Jul 7, 2023
710155f
rm sort mistakes
solid-danylokhvan Jul 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 86 additions & 7 deletions lib/core/controllers/colored_text_editing_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'package:languagetool_textfield/core/enums/mistake_type.dart';
import 'package:languagetool_textfield/domain/highlight_style.dart';
import 'package:languagetool_textfield/domain/language_check_service.dart';
import 'package:languagetool_textfield/domain/mistake.dart';
import 'package:languagetool_textfield/utils/closed_range.dart';
import 'package:languagetool_textfield/utils/keep_latest_response_service.dart';
import 'package:languagetool_textfield/utils/mistake_popup.dart';

/// A TextEditingController with overrides buildTextSpan for building
Expand All @@ -18,6 +20,10 @@ class ColoredTextEditingController extends TextEditingController {
/// Language tool API index
final LanguageCheckService languageCheckService;

/// Create an instance of [KeepLatestResponseService]
/// to handle asynchronous operations
final latestResponseService = KeepLatestResponseService();

/// List which contains Mistake objects spans are built from
List<Mistake> _mistakes = [];

Expand Down Expand Up @@ -75,8 +81,10 @@ class ColoredTextEditingController extends TextEditingController {

/// Replaces mistake with given replacement
void replaceMistake(Mistake mistake, String replacement) {
final mistakes = List<Mistake>.from(_mistakes);
mistakes.remove(mistake);
_mistakes = mistakes;
text = text.replaceRange(mistake.offset, mistake.endOffset, replacement);
_mistakes.remove(mistake);
focusNode?.requestFocus();
Future.microtask.call(() {
final newOffset = mistake.offset + replacement.length;
Expand All @@ -89,24 +97,29 @@ class ColoredTextEditingController extends TextEditingController {
Future<void> _handleTextChange(String newText) async {
///set value triggers each time, even when cursor changes its location
///so this check avoid cleaning Mistake list when text wasn't really changed
if (newText == text) return;
if (newText == text || newText.isEmpty) return;

final filteredMistakes = _filterMistakesOnChanged(newText);
_mistakes = filteredMistakes.toList();

// If we have a text change and we have a popup on hold
// it will close the popup
_closePopup();

_mistakes.clear();
for (final recognizer in _recognizers) {
recognizer.dispose();
}
_recognizers.clear();

final mistakesWrapper = await languageCheckService.findMistakes(newText);
final mistakesWrapper = await latestResponseService.processLatestOperation(
() => languageCheckService.findMistakes(newText),
);
final mistakes = mistakesWrapper?.result();
_fetchError = mistakesWrapper?.error;

_mistakes =
mistakesWrapper.hasResult ? mistakesWrapper.result().toList() : [];
_fetchError = mistakesWrapper.error;
if (mistakes == null) return;

_mistakes = mistakes;
notifyListeners();
}

Expand All @@ -118,6 +131,9 @@ class ColoredTextEditingController extends TextEditingController {
int currentOffset = 0; // enter index

for (final Mistake mistake in _mistakes) {
final mistakeEndOffset = min(mistake.endOffset, text.length);
if (mistake.offset > mistakeEndOffset) continue;

/// TextSpan before mistake
yield TextSpan(
text: text.substring(
Expand Down Expand Up @@ -190,6 +206,69 @@ class ColoredTextEditingController extends TextEditingController {
);
}

/// Filters the list of mistakes based on the changes
/// in the text when it is changed.
Iterable<Mistake> _filterMistakesOnChanged(String newText) sync* {
final isSelectionRangeEmpty = selection.end == selection.start;
final lengthDiscrepancy = newText.length - text.length;

for (final mistake in _mistakes) {
Mistake? newMistake;

newMistake = isSelectionRangeEmpty
? _adjustMistakeOffsetWithCaretCursor(
mistake: mistake,
lengthDiscrepancy: lengthDiscrepancy,
)
: _adjustMistakeOffsetWithSelectionRange(
mistake: mistake,
lengthDiscrepancy: lengthDiscrepancy,
);

if (newMistake != null) yield newMistake;
}
}

/// Adjusts the mistake offset when the selection is a caret cursor.
Mistake? _adjustMistakeOffsetWithCaretCursor({
required Mistake mistake,
required int lengthDiscrepancy,
}) {
final mistakeRange = ClosedRange(mistake.offset, mistake.endOffset);
final caretLocation = selection.base.offset;

// Don't highlight mistakes on changed text
// until we get an update from the API.
final isCaretOnMistake = mistakeRange.contains(caretLocation);
if (isCaretOnMistake) return null;

final shouldAdjustOffset = mistakeRange.isBeforeOrAt(caretLocation);
if (!shouldAdjustOffset) return mistake;

final newOffset = mistake.offset + lengthDiscrepancy;

return mistake.copyWith(offset: newOffset);
}

/// Adjusts the mistake offset when the selection is a range.
Mistake? _adjustMistakeOffsetWithSelectionRange({
required Mistake mistake,
required int lengthDiscrepancy,
}) {
final selectionRange = ClosedRange(selection.start, selection.end);
final mistakeRange = ClosedRange(mistake.offset, mistake.endOffset);

final hasSelectedTextChanged = selectionRange.overlapsWith(mistakeRange);
if (hasSelectedTextChanged) return null;

final shouldAdjustOffset = selectionRange.isAfterOrAt(mistake.offset);
if (!shouldAdjustOffset) return mistake;

final newOffset = mistake.offset + lengthDiscrepancy;

return mistake.copyWith(offset: newOffset);
}

/// Returns color for mistake TextSpan style
Color _getMistakeColor(MistakeType type) {
switch (type) {
Expand Down
17 changes: 17 additions & 0 deletions lib/domain/mistake.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,21 @@ class Mistake {
required this.length,
this.replacements = const [],
});

/// Creates a copy of this mistake with optional parameter values overridden.
Mistake copyWith({
String? message,
MistakeType? type,
int? offset,
int? length,
List<String>? replacements,
}) {
return Mistake(
message: message ?? this.message,
type: type ?? this.type,
offset: offset ?? this.offset,
length: length ?? this.length,
replacements: replacements ?? this.replacements,
);
}
}
8 changes: 4 additions & 4 deletions lib/implementations/debounce_lang_tool_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ class DebounceLangToolService extends LanguageCheckService {

@override
Future<Result<List<Mistake>>> findMistakes(String text) async {
final value = await debouncing.debounce(() {
return baseService.findMistakes(text);
}) as Result<List<Mistake>>?;
final value =
await debouncing.debounce(() => baseService.findMistakes(text))
as Result<List<Mistake>>;

return value ?? const Result.success(<Mistake>[]);
return value;
}

@override
Expand Down
2 changes: 1 addition & 1 deletion lib/implementations/lang_tool_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class LangToolService extends LanguageCheckService {
final LanguageToolClient languageTool;

/// Creates a new instance of the [LangToolService].
const LangToolService(this.languageTool);
LangToolService(this.languageTool);

@override
Future<Result<List<Mistake>>> findMistakes(String text) async {
Expand Down
31 changes: 31 additions & 0 deletions lib/utils/closed_range.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'dart:math';

/// The [ClosedRange] class represents a closed range of integers, defined
/// by a starting point and an ending point.
/// The start and end properties represent the bounds of the range.
class ClosedRange {
/// The start of the closed range.
final int start;

/// The start of the closed range.
final int end;

/// Constructor for creating a ClosedRange object
const ClosedRange(this.start, this.end);

/// Checks if the given point is before or at the range
bool isBeforeOrAt(int point) => point <= min(start, end);

/// Checks if the given point is after or at the range
bool isAfterOrAt(int point) => point >= max(start, end);

/// Checks if the range contains the given point
bool contains(int point) {
return start <= point && point <= end;
}

/// Checks if the this range is within the boundaries of the another range
bool overlapsWith(ClosedRange other) {
return contains(other.start) || contains(other.end);
}
}
18 changes: 18 additions & 0 deletions lib/utils/keep_latest_response_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:async/async.dart';

/// A service that executes asynchronous operations and ensures that only
/// the results of the latest operation are returned.
class KeepLatestResponseService {
CancelableOperation<dynamic>? _currentOperation;

/// Executes the latest operation and returns its result.
/// Only the results of the most recent operation are returned,
/// discarding any previous ongoing operations.
Future<T?> processLatestOperation<T>(Future<T> Function() action) async {
final newOperation = CancelableOperation<T>.fromFuture(action());
await _currentOperation?.cancel();
_currentOperation = newOperation;

return _currentOperation?.value as Future<T>?;
}
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ environment:
flutter: ">=1.17.0"

dependencies:
async: ^2.11.0
collection: ^1.17.1
flutter:
sdk: flutter
Expand Down