better documentation in IGCTextData

This commit is contained in:
ggurdin 2025-11-06 11:33:01 -05:00
parent ab8387c522
commit 26317c6f8a
No known key found for this signature in database
GPG key ID: A01CB41737CBB478
5 changed files with 128 additions and 111 deletions

View file

@ -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 &&

View file

@ -407,7 +407,7 @@ class Choreographer extends ChangeNotifier {
void clearMatches(Object error) {
MatrixState.pAnyState.closeAllOverlays();
igcController.clearIGCMatches();
igcController.clearMatches();
errorService.setError(ChoreoError(raw: error));
}

View file

@ -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(

View file

@ -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),

View file

@ -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);
}
}
}
}