better documentation in IGCTextData
This commit is contained in:
parent
ab8387c522
commit
26317c6f8a
5 changed files with 128 additions and 111 deletions
|
|
@ -441,7 +441,7 @@ class InputBar extends StatelessWidget {
|
|||
// so we need to adjust the offset accordingly
|
||||
int adjustedOffset = controller!.selection.baseOffset;
|
||||
final normalizationMatches =
|
||||
choreographer.igcController.recentNormalizationMatches;
|
||||
choreographer.igcController.recentAutomaticCorrections;
|
||||
if (normalizationMatches == null || normalizationMatches.isEmpty) return;
|
||||
for (final match in normalizationMatches) {
|
||||
if (match.updatedMatch.match.offset < adjustedOffset &&
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ class Choreographer extends ChangeNotifier {
|
|||
|
||||
void clearMatches(Object error) {
|
||||
MatrixState.pAnyState.closeAllOverlays();
|
||||
igcController.clearIGCMatches();
|
||||
igcController.clearMatches();
|
||||
errorService.setError(ChoreoError(raw: error));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ class IgcController {
|
|||
PangeaMatchState? get currentlyOpenMatch => _igcTextData?.currentlyOpenMatch;
|
||||
PangeaMatchState? get firstOpenMatch => _igcTextData?.firstOpenMatch;
|
||||
List<PangeaMatchState>? get openMatches => _igcTextData?.openMatches;
|
||||
List<PangeaMatchState>? get recentNormalizationMatches =>
|
||||
_igcTextData?.recentNormalizationMatches;
|
||||
List<PangeaMatchState>? get recentAutomaticCorrections =>
|
||||
_igcTextData?.recentAutomaticCorrections;
|
||||
List<PangeaMatchState>? get openNormalizationMatches =>
|
||||
_igcTextData?.openNormalizationMatches;
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ class IgcController {
|
|||
MatrixState.pAnyState.closeAllOverlays();
|
||||
}
|
||||
|
||||
void clearIGCMatches() => _igcTextData?.clearIGCMatches();
|
||||
void clearMatches() => _igcTextData?.clearMatches();
|
||||
|
||||
PangeaMatchState? getMatchByOffset(int offset) =>
|
||||
_igcTextData?.getOpenMatchByOffset(offset);
|
||||
|
|
@ -53,7 +53,7 @@ class IgcController {
|
|||
if (_igcTextData == null) {
|
||||
throw "acceptReplacement called with null igcTextData";
|
||||
}
|
||||
final updateMatch = _igcTextData!.makeAcceptedMatchUpdates(match, status);
|
||||
final updateMatch = _igcTextData!.acceptMatch(match, status);
|
||||
return updateMatch;
|
||||
}
|
||||
|
||||
|
|
@ -62,14 +62,14 @@ class IgcController {
|
|||
if (_igcTextData == null) {
|
||||
throw "should not be in onIgnoreMatch with null igcTextData";
|
||||
}
|
||||
return _igcTextData!.makeIgnoredMatchUpdates(match);
|
||||
return _igcTextData!.ignoreMatch(match);
|
||||
}
|
||||
|
||||
void undoReplacement(PangeaMatchState match) {
|
||||
if (_igcTextData == null) {
|
||||
throw "undoReplacement called with null igcTextData";
|
||||
}
|
||||
_igcTextData!.removeMatchUpdates(match);
|
||||
_igcTextData!.undoMatch(match);
|
||||
}
|
||||
|
||||
Future<void> getIGCTextData(
|
||||
|
|
|
|||
|
|
@ -177,12 +177,12 @@ class PangeaTextController extends TextEditingController {
|
|||
TextStyle? defaultStyle,
|
||||
}) {
|
||||
final openMatches = choreographer.igcController.openMatches ?? const [];
|
||||
final normalizationMatches =
|
||||
choreographer.igcController.recentNormalizationMatches ?? const [];
|
||||
final automaticCorrections =
|
||||
choreographer.igcController.recentAutomaticCorrections ?? const [];
|
||||
|
||||
final textSpanMatches = [
|
||||
...openMatches,
|
||||
...normalizationMatches,
|
||||
...automaticCorrections,
|
||||
]..sort(
|
||||
(a, b) =>
|
||||
a.updatedMatch.match.offset.compareTo(b.updatedMatch.match.offset),
|
||||
|
|
|
|||
|
|
@ -9,34 +9,40 @@ import 'package:fluffychat/pangea/choreographer/models/span_data.dart';
|
|||
import 'package:fluffychat/pangea/choreographer/repo/igc_repo.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
/// A model representing the mutable text and match state used by
|
||||
/// A model representing mutable text and match state used by
|
||||
/// Interactive Grammar Correction (IGC).
|
||||
///
|
||||
/// This class tracks the original input text, the current working text,
|
||||
/// and the states of open and closed grammar matches as the user accepts,
|
||||
/// ignores, or reverses suggested corrections.
|
||||
/// This class tracks the user's original text, the current working text,
|
||||
/// and the states of grammar matches detected during processing.
|
||||
/// It provides methods to accept, ignore, or undo corrections, while
|
||||
/// maintaining consistent text and offset updates across all matches.
|
||||
class IGCTextData {
|
||||
/// The user's original text before any corrections or replacements.
|
||||
final String _originalInput;
|
||||
final String _originalText;
|
||||
|
||||
/// The full list of detected matches from the initial grammar analysis.
|
||||
final List<PangeaMatch> _matches;
|
||||
/// The complete list of detected matches from the initial grammar analysis.
|
||||
final List<PangeaMatch> _initialMatches;
|
||||
|
||||
/// Matches currently pending user action (neither accepted nor ignored).
|
||||
/// Matches currently awaiting user action (neither accepted nor ignored).
|
||||
final List<PangeaMatchState> _openMatches = [];
|
||||
|
||||
/// Matches that have been resolved by either accepting or ignoring them.
|
||||
/// Matches that have been resolved, either accepted or ignored.
|
||||
final List<PangeaMatchState> _closedMatches = [];
|
||||
|
||||
/// The current text content after applying all accepted corrections.
|
||||
/// The current working text after applying accepted corrections.
|
||||
String _currentText;
|
||||
|
||||
/// Creates a new instance of [IGCTextData] from the given [originalInput]
|
||||
/// and list of grammar [matches].
|
||||
///
|
||||
/// Automatically initializes open and closed matches based on their status
|
||||
/// and filters out previously ignored matches.
|
||||
IGCTextData({
|
||||
required String originalInput,
|
||||
required List<PangeaMatch> matches,
|
||||
}) : _currentText = originalInput,
|
||||
_originalInput = originalInput,
|
||||
_matches = matches {
|
||||
}) : _originalText = originalInput,
|
||||
_currentText = originalInput,
|
||||
_initialMatches = matches {
|
||||
for (final match in matches) {
|
||||
final matchState = PangeaMatchState(
|
||||
match: match.match,
|
||||
|
|
@ -52,35 +58,41 @@ class IGCTextData {
|
|||
_filterPreviouslyIgnoredMatches();
|
||||
}
|
||||
|
||||
/// Returns a JSON representation of this IGC text data.
|
||||
Map<String, dynamic> toJson() => {
|
||||
"original_input": _originalInput,
|
||||
"matches": _matches.map((e) => e.toJson()).toList(),
|
||||
'original_input': _originalText,
|
||||
'matches': _initialMatches.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
/// The current working text after any accepted replacements.
|
||||
String get currentText => _currentText;
|
||||
|
||||
List<PangeaMatchState> get openMatches => _openMatches;
|
||||
/// The list of open matches that are still awaiting user action.
|
||||
List<PangeaMatchState> get openMatches => List.unmodifiable(_openMatches);
|
||||
|
||||
/// Whether there are any open matches remaining.
|
||||
bool get hasOpenMatches => _openMatches.isNotEmpty;
|
||||
|
||||
/// The first open match, if one exists.
|
||||
PangeaMatchState? get firstOpenMatch => _openMatches.firstOrNull;
|
||||
|
||||
/// Normalization matches that have been closed in the last choreo step(s).
|
||||
/// Used to display automatic corrections made by the IGC system.
|
||||
List<PangeaMatchState> get recentNormalizationMatches =>
|
||||
/// Closed matches that were automatically corrected in recent steps.
|
||||
///
|
||||
/// Used to display automatic normalization corrections applied
|
||||
/// by the IGC system.
|
||||
List<PangeaMatchState> get recentAutomaticCorrections =>
|
||||
_closedMatches.reversed
|
||||
.takeWhile(
|
||||
(m) => m.updatedMatch.status == PangeaMatchStatus.automatic,
|
||||
)
|
||||
.toList();
|
||||
|
||||
/// Convenience getter for open normalization error matches.
|
||||
/// Used for auto-correction of normalization errors.
|
||||
/// Open matches representing normalization errors that can be auto-corrected.
|
||||
List<PangeaMatchState> get openNormalizationMatches => _openMatches
|
||||
.where((match) => match.updatedMatch.match.isNormalizationError())
|
||||
.toList();
|
||||
|
||||
/// Returns the open match that contains the given text offset, if any.
|
||||
/// Returns the open match that contains the given text [offset], if any.
|
||||
PangeaMatchState? getOpenMatchByOffset(int offset) =>
|
||||
_openMatches.firstWhereOrNull(
|
||||
(match) => match.updatedMatch.match.isOffsetInMatchSpan(offset),
|
||||
|
|
@ -89,15 +101,16 @@ class IGCTextData {
|
|||
/// Returns the match whose span card overlay is currently open, if any.
|
||||
PangeaMatchState? get currentlyOpenMatch {
|
||||
final RegExp pattern = RegExp(r'span_card_overlay_.+');
|
||||
final String? matchingKeys =
|
||||
final String? matchingKey =
|
||||
MatrixState.pAnyState.getMatchingOverlayKeys(pattern).firstOrNull;
|
||||
if (matchingKeys == null) return null;
|
||||
if (matchingKey == null) return null;
|
||||
|
||||
final parts = matchingKeys.split("_");
|
||||
final parts = matchingKey.split('_');
|
||||
if (parts.length != 5) return null;
|
||||
final offset = int.tryParse(parts[3]);
|
||||
final length = int.tryParse(parts[4]);
|
||||
if (offset == null || length == null) return null;
|
||||
|
||||
return _openMatches.firstWhereOrNull(
|
||||
(match) =>
|
||||
match.updatedMatch.match.offset == offset &&
|
||||
|
|
@ -105,144 +118,148 @@ class IGCTextData {
|
|||
);
|
||||
}
|
||||
|
||||
/// Clears all matches from the IGC text data.
|
||||
/// Call on error that make continuing IGC processing invalid.
|
||||
void clearIGCMatches() {
|
||||
/// Clears all match data from this IGC instance.
|
||||
///
|
||||
/// Call this when an error occurs that invalidates current match data.
|
||||
void clearMatches() {
|
||||
_openMatches.clear();
|
||||
_closedMatches.clear();
|
||||
}
|
||||
|
||||
/// Filters out previously ignored matches from the open matches list.
|
||||
/// Filters out any previously ignored matches from the open list.
|
||||
void _filterPreviouslyIgnoredMatches() {
|
||||
for (final match in _openMatches) {
|
||||
if (IgcRepo.isIgnored(match.updatedMatch)) {
|
||||
makeIgnoredMatchUpdates(match);
|
||||
ignoreMatch(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces the span data for a given match.
|
||||
void setSpanData(PangeaMatchState match, SpanData spanData) {
|
||||
/// Updates the [matchState] with new [spanData].
|
||||
///
|
||||
/// Replaces the existing span information for the given match
|
||||
/// while maintaining its position in the open list.
|
||||
void setSpanData(PangeaMatchState matchState, SpanData spanData) {
|
||||
final openMatch = _openMatches.firstWhereOrNull(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
(m) => m.originalMatch == matchState.originalMatch,
|
||||
);
|
||||
|
||||
match.setMatch(spanData);
|
||||
matchState.setMatch(spanData);
|
||||
_openMatches.remove(openMatch);
|
||||
_openMatches.add(match);
|
||||
_openMatches.add(matchState);
|
||||
}
|
||||
|
||||
/// Accepts the specified [match] and updates both the open/closed match lists
|
||||
/// and the [_currentText] to include the chosen replacement text.
|
||||
PangeaMatch makeAcceptedMatchUpdates(
|
||||
PangeaMatchState match,
|
||||
/// Accepts the given [matchState], updates text and state lists,
|
||||
/// and returns the updated [PangeaMatch].
|
||||
///
|
||||
/// Applies the selected replacement text to [_currentText] and
|
||||
/// updates offsets for all matches accordingly.
|
||||
PangeaMatch acceptMatch(
|
||||
PangeaMatchState matchState,
|
||||
PangeaMatchStatus status,
|
||||
) {
|
||||
final openMatch = _openMatches.firstWhere(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
orElse: () => throw Exception(
|
||||
'No open match found for acceptReplacement',
|
||||
(m) => m.originalMatch == matchState.originalMatch,
|
||||
orElse: () => throw StateError(
|
||||
'No open match found while accepting match.',
|
||||
),
|
||||
);
|
||||
|
||||
if (match.updatedMatch.match.selectedChoice == null) {
|
||||
throw Exception(
|
||||
'acceptReplacement called with null selectedChoice',
|
||||
final choice = matchState.updatedMatch.match.selectedChoice;
|
||||
if (choice == null) {
|
||||
throw ArgumentError(
|
||||
'acceptMatch called with a null selectedChoice.',
|
||||
);
|
||||
}
|
||||
|
||||
match.setStatus(status);
|
||||
matchState.setStatus(status);
|
||||
_openMatches.remove(openMatch);
|
||||
_closedMatches.add(match);
|
||||
_closedMatches.add(matchState);
|
||||
|
||||
_runReplacement(
|
||||
match.updatedMatch.match.offset,
|
||||
match.updatedMatch.match.length,
|
||||
match.updatedMatch.match.selectedChoice!.value,
|
||||
_applyReplacement(
|
||||
matchState.updatedMatch.match.offset,
|
||||
matchState.updatedMatch.match.length,
|
||||
choice.value,
|
||||
);
|
||||
|
||||
return match.updatedMatch;
|
||||
return matchState.updatedMatch;
|
||||
}
|
||||
|
||||
/// Ignores a given match and updates the IGC text data state accordingly.
|
||||
PangeaMatch makeIgnoredMatchUpdates(PangeaMatchState match) {
|
||||
/// Ignores the given [matchState] and moves it to the closed match list.
|
||||
///
|
||||
/// Returns the updated [PangeaMatch] after applying the ignore operation.
|
||||
PangeaMatch ignoreMatch(PangeaMatchState matchState) {
|
||||
final openMatch = _openMatches.firstWhere(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
orElse: () => throw Exception(
|
||||
'No open match found for makeIgnoredMatchUpdates',
|
||||
(m) => m.originalMatch == matchState.originalMatch,
|
||||
orElse: () => throw StateError(
|
||||
'No open match found while ignoring match.',
|
||||
),
|
||||
);
|
||||
|
||||
match.setStatus(PangeaMatchStatus.ignored);
|
||||
matchState.setStatus(PangeaMatchStatus.ignored);
|
||||
_openMatches.remove(openMatch);
|
||||
_closedMatches.add(match);
|
||||
return match.updatedMatch;
|
||||
_closedMatches.add(matchState);
|
||||
return matchState.updatedMatch;
|
||||
}
|
||||
|
||||
/// Removes a given match from the closed match history and undoes the
|
||||
/// changes to igc text data state caused by accepting the match.
|
||||
void removeMatchUpdates(PangeaMatchState match) {
|
||||
/// Undoes a previously accepted match by reverting the replacement
|
||||
/// and removing it from the closed match list.
|
||||
void undoMatch(PangeaMatchState matchState) {
|
||||
final closedMatch = _closedMatches.firstWhere(
|
||||
(m) => m.originalMatch == match.originalMatch,
|
||||
orElse: () => throw Exception(
|
||||
'No closed match found for removeMatchUpdates',
|
||||
(m) => m.originalMatch == matchState.originalMatch,
|
||||
orElse: () => throw StateError(
|
||||
'No closed match found while undoing match.',
|
||||
),
|
||||
);
|
||||
|
||||
_closedMatches.remove(closedMatch);
|
||||
final choice = match.updatedMatch.match.selectedChoice?.value;
|
||||
|
||||
if (choice == null) {
|
||||
throw Exception(
|
||||
"match.match.selectedChoice is null in removeMatchUpdates",
|
||||
final selectedValue = matchState.updatedMatch.match.selectedChoice?.value;
|
||||
if (selectedValue == null) {
|
||||
throw StateError(
|
||||
'Cannot undo match without a selectedChoice value.',
|
||||
);
|
||||
}
|
||||
|
||||
final String replacement = match.originalMatch.match.fullText.characters
|
||||
final replacement = matchState.originalMatch.match.fullText.characters
|
||||
.getRange(
|
||||
match.originalMatch.match.offset,
|
||||
match.originalMatch.match.offset + match.originalMatch.match.length,
|
||||
matchState.originalMatch.match.offset,
|
||||
matchState.originalMatch.match.offset +
|
||||
matchState.originalMatch.match.length,
|
||||
)
|
||||
.toString();
|
||||
|
||||
_runReplacement(
|
||||
match.originalMatch.match.offset,
|
||||
choice.characters.length,
|
||||
_applyReplacement(
|
||||
matchState.originalMatch.match.offset,
|
||||
selectedValue.characters.length,
|
||||
replacement,
|
||||
);
|
||||
}
|
||||
|
||||
/// Runs a text replacement and updates match offsets / current text accordingly.
|
||||
void _runReplacement(
|
||||
/// Applies a text replacement to [_currentText] and adjusts match offsets.
|
||||
///
|
||||
/// Called internally when a correction is accepted or undone.
|
||||
void _applyReplacement(
|
||||
int offset,
|
||||
int length,
|
||||
String replacement,
|
||||
) {
|
||||
final start = _currentText.characters.take(offset);
|
||||
final end = _currentText.characters.skip(offset + length);
|
||||
final fullText = start + replacement.characters + end;
|
||||
_currentText = fullText.toString();
|
||||
final updatedText = start + replacement.characters + end;
|
||||
_currentText = updatedText.toString();
|
||||
|
||||
for (int i = 0; i < _openMatches.length; i++) {
|
||||
final match = _openMatches[i].updatedMatch.match;
|
||||
final updatedMatch = match.copyWith(
|
||||
fullText: _currentText,
|
||||
offset: match.offset > offset
|
||||
? match.offset + replacement.characters.length - length
|
||||
: match.offset,
|
||||
);
|
||||
_openMatches[i].setMatch(updatedMatch);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _closedMatches.length; i++) {
|
||||
final match = _closedMatches[i].updatedMatch.match;
|
||||
final updatedMatch = match.copyWith(
|
||||
fullText: _currentText,
|
||||
offset: match.offset > offset
|
||||
? match.offset + replacement.characters.length - length
|
||||
: match.offset,
|
||||
);
|
||||
_closedMatches[i].setMatch(updatedMatch);
|
||||
for (final list in [_openMatches, _closedMatches]) {
|
||||
for (final matchState in list) {
|
||||
final match = matchState.updatedMatch.match;
|
||||
final updatedMatch = match.copyWith(
|
||||
fullText: _currentText,
|
||||
offset: match.offset > offset
|
||||
? match.offset + replacement.characters.length - length
|
||||
: match.offset,
|
||||
);
|
||||
matchState.setMatch(updatedMatch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue