From 26317c6f8a7112d66f8e5464e5b5435ccb2ee20f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 6 Nov 2025 11:33:01 -0500 Subject: [PATCH] better documentation in IGCTextData --- lib/pages/chat/input_bar.dart | 2 +- .../controllers/choreographer.dart | 2 +- .../controllers/igc_controller.dart | 12 +- .../controllers/pangea_text_controller.dart | 6 +- .../models/igc_text_data_model.dart | 217 ++++++++++-------- 5 files changed, 128 insertions(+), 111 deletions(-) diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index 283cb9d26..4f35728f6 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -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 && diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 9441b55a1..8a61be964 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -407,7 +407,7 @@ class Choreographer extends ChangeNotifier { void clearMatches(Object error) { MatrixState.pAnyState.closeAllOverlays(); - igcController.clearIGCMatches(); + igcController.clearMatches(); errorService.setError(ChoreoError(raw: error)); } diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 6be8103ae..8cd17af29 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -27,8 +27,8 @@ class IgcController { PangeaMatchState? get currentlyOpenMatch => _igcTextData?.currentlyOpenMatch; PangeaMatchState? get firstOpenMatch => _igcTextData?.firstOpenMatch; List? get openMatches => _igcTextData?.openMatches; - List? get recentNormalizationMatches => - _igcTextData?.recentNormalizationMatches; + List? get recentAutomaticCorrections => + _igcTextData?.recentAutomaticCorrections; List? 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 getIGCTextData( diff --git a/lib/pangea/choreographer/controllers/pangea_text_controller.dart b/lib/pangea/choreographer/controllers/pangea_text_controller.dart index 1e2991bd3..f14b158fa 100644 --- a/lib/pangea/choreographer/controllers/pangea_text_controller.dart +++ b/lib/pangea/choreographer/controllers/pangea_text_controller.dart @@ -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), diff --git a/lib/pangea/choreographer/models/igc_text_data_model.dart b/lib/pangea/choreographer/models/igc_text_data_model.dart index 03933c9f6..72392bc94 100644 --- a/lib/pangea/choreographer/models/igc_text_data_model.dart +++ b/lib/pangea/choreographer/models/igc_text_data_model.dart @@ -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 _matches; + /// The complete list of detected matches from the initial grammar analysis. + final List _initialMatches; - /// Matches currently pending user action (neither accepted nor ignored). + /// Matches currently awaiting user action (neither accepted nor ignored). final List _openMatches = []; - /// Matches that have been resolved by either accepting or ignoring them. + /// Matches that have been resolved, either accepted or ignored. final List _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 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 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 get openMatches => _openMatches; + /// The list of open matches that are still awaiting user action. + List 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 get recentNormalizationMatches => + /// Closed matches that were automatically corrected in recent steps. + /// + /// Used to display automatic normalization corrections applied + /// by the IGC system. + List 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 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); + } } } }