diff --git a/lib/features/workspace/sql_query_history_dialog.dart b/lib/features/workspace/sql_query_history_dialog.dart index 69ec52b3..4e7bb8aa 100644 --- a/lib/features/workspace/sql_query_history_dialog.dart +++ b/lib/features/workspace/sql_query_history_dialog.dart @@ -85,13 +85,7 @@ class _SqlQueryHistoryDialogContentState }); } - static final _whitespacePattern = RegExp(r'\s+'); - - static String _previewOneLine(String sql) { - final collapsed = sql.replaceAll(_whitespacePattern, ' ').trim(); - if (collapsed.length <= 96) return collapsed; - return '${collapsed.substring(0, 93)}…'; - } + static String _previewOneLine(String sql) => previewSqlOneLine(sql); static String? _formatWhen(String iso) { final t = DateTime.tryParse(iso)?.toLocal(); @@ -405,3 +399,52 @@ class _SqlQueryHistoryDialogContentState ); } } + +/// Longest one-line preview kept whole; longer text is cut to +/// [_previewKeep] characters plus an ellipsis. +const int _previewMax = 96; +const int _previewKeep = 93; + +/// Same set as the regex `\s` (what the old `replaceAll(RegExp(r'\s+'), ' ')` +/// collapsed). +bool _isPreviewWhitespace(int c) => + (c >= 0x09 && c <= 0x0D) || + c == 0x20 || + c == 0xA0 || + c == 0x1680 || + (c >= 0x2000 && c <= 0x200A) || + c == 0x2028 || + c == 0x2029 || + c == 0x202F || + c == 0x205F || + c == 0x3000 || + c == 0xFEFF; + +/// One-line, whitespace-collapsed, trimmed preview of [sql] for a list row. +/// +/// Reads only as far as it needs: a 100 KB migration is cut after about a +/// hundred characters instead of being copied whole by a regex replace (#1545). +@visibleForTesting +String previewSqlOneLine(String sql) { + final out = StringBuffer(); + var length = 0; + var pendingSpace = false; + for (var i = 0; i < sql.length; i++) { + if (_isPreviewWhitespace(sql.codeUnitAt(i))) { + pendingSpace = length > 0; + continue; + } + if (pendingSpace) { + out.writeCharCode(0x20); + length++; + pendingSpace = false; + if (length > _previewMax) break; + } + out.writeCharCode(sql.codeUnitAt(i)); + length++; + if (length > _previewMax) break; + } + final text = out.toString(); + if (length <= _previewMax) return text; + return '${text.substring(0, _previewKeep)}…'; +} diff --git a/test/features/workspace/sql_query_history_preview_test.dart b/test/features/workspace/sql_query_history_preview_test.dart new file mode 100644 index 00000000..ce57adc4 --- /dev/null +++ b/test/features/workspace/sql_query_history_preview_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/features/workspace/sql_query_history_dialog.dart'; + +/// The implementation before #1545: collapse the whole text, then cut. +String _reference(String sql) { + final collapsed = sql.replaceAll(RegExp(r'\s+'), ' ').trim(); + if (collapsed.length <= 96) return collapsed; + return '${collapsed.substring(0, 93)}…'; +} + +void main() { + group('previewSqlOneLine (#1545)', () { + test('matches the old collapse-then-cut result', () { + final inputs = [ + '', + ' ', + ' \n\t ', + 'SELECT 1', + ' SELECT 1 \n FROM\tt ', + 'SELECT a, b FROM t', + 'a' * 96, + 'a' * 97, + '${'a' * 96} ', + '${'a' * 96} b', + '${'a' * 95} b', + '${'a' * 94} b c', + ('select x from t\n' * 20), + '\n\n\n${'x ' * 200}', + 'a' * 5000, + 'word ' * 30000, + ]; + for (final sql in inputs) { + expect(previewSqlOneLine(sql), _reference(sql), + reason: 'input of length ${sql.length}'); + } + }); + + test('long text is cut to 93 characters and an ellipsis', () { + final out = previewSqlOneLine('SELECT ${'column_name, ' * 100}FROM t'); + expect(out.length, 94); + expect(out.endsWith('…'), isTrue); + }); + }); +}