From a84973def6c23cee7846ea12a244c6588537f52a Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Fri, 16 May 2025 15:32:58 +0200 Subject: [PATCH] Fix cyrillic --- lib/db.dart | 144 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 107 insertions(+), 37 deletions(-) diff --git a/lib/db.dart b/lib/db.dart index 6f71303..227a081 100644 --- a/lib/db.dart +++ b/lib/db.dart @@ -441,63 +441,133 @@ END; return []; } - // Process the query for partial word matching // Split into individual terms, filter empty ones - List terms = - query - .trim() - .split(RegExp(r'\s+')) - .where((term) => term.isNotEmpty) - .toList(); + List terms = query + .trim() + .split(RegExp(r'\s+')) + .where((term) => term.isNotEmpty) + .toList(); if (terms.isEmpty) { return []; } - // Add wildcards to each term for prefix matching - // Join terms with AND for all-term matching (results must contain ALL terms) - String ftsQuery = terms - .map((term) { - // Only remove dangerous characters but preserve Unicode - String sanitizedTerm = term.replaceAll(RegExp(r'''['\\]'''), ''); - if (sanitizedTerm.isEmpty) return ''; + // Create the FTS match expression for each term + List matchExpressions = terms.map((term) { + // Remove dangerous characters but preserve Unicode + String sanitizedTerm = term.replaceAll(RegExp(r'''['\\]'''), ''); + if (sanitizedTerm.isEmpty) return ''; + + // Escape special characters in the term for the LIKE pattern + String escapedTerm = sanitizedTerm + .replaceAll('%', '\\%') + .replaceAll('_', '\\_'); + + // Use LIKE for prefix/suffix matching + return '''( + content LIKE '%' || ? || '%' COLLATE NOCASE OR + content LIKE ? || '%' COLLATE NOCASE OR + content LIKE '%' || ? COLLATE NOCASE + )'''; + }).where((expr) => expr.isNotEmpty).toList(); - if (_hasIcuSupport) { - // With ICU support, we can use wildcards with Unicode - return '*$sanitizedTerm*'; - } else { - // Without ICU, just use the term as is for basic matching - return sanitizedTerm; - } - }) - .where((term) => term.isNotEmpty) - .join(' AND '); - - if (ftsQuery.isEmpty) { + if (matchExpressions.isEmpty) { debugPrint('Query was sanitized to empty string'); return []; } - debugPrint('FTS query: "$ftsQuery" (ICU: $_hasIcuSupport)'); + // Combine all terms with AND logic + String whereClause = matchExpressions.join(' AND '); + + // Create the parameter list (each term needs to be repeated 3 times for the different LIKE patterns) + List parameters = []; + for (String term in terms) { + String sanitizedTerm = term.replaceAll(RegExp(r'''['\\]'''), ''); + if (sanitizedTerm.isNotEmpty) { + parameters.addAll([sanitizedTerm, sanitizedTerm, sanitizedTerm]); + } + } - // Execute the FTS query with AND logic + debugPrint('Search query: "$query" with ${terms.length} terms'); + debugPrint('Where clause: $whereClause'); + debugPrint('Parameters: $parameters'); + + // Execute the search query final List> results = await db.rawQuery( ''' - SELECT n.id, n.date, n.content, snippet(notes_fts, 0, '', '', '...', 20) as snippet - FROM notes_fts - JOIN notes n ON notes_fts.rowid = n.id - WHERE notes_fts MATCH ? + SELECT n.id, n.date, n.content + FROM notes n + WHERE $whereClause ORDER BY n.date DESC LIMIT 100 - ''', - [ftsQuery], + ''', + parameters, ); - debugPrint('Search query "$ftsQuery" returned ${results.length} results'); - return results; - } catch (e) { + // Add snippets with highlighting in Dart code + final processedResults = results.map((row) { + final content = row['content'] as String; + final snippet = _createSnippet(content, terms); + return { + ...row, + 'snippet': snippet, + }; + }).toList(); + + debugPrint('Search returned ${results.length} results'); + return processedResults; + } catch (e, stackTrace) { debugPrint('Search failed: $e'); + debugPrint('Stack trace: $stackTrace'); rethrow; } } + + // Helper function to create a snippet with highlighted terms + static String _createSnippet(String content, List terms) { + const int snippetLength = 150; // Maximum length of the snippet + const String ellipsis = '...'; + + // Find the first match position + int firstMatchPos = content.length; + String? firstMatchTerm; + + for (final term in terms) { + final pos = content.toLowerCase().indexOf(term.toLowerCase()); + if (pos != -1 && pos < firstMatchPos) { + firstMatchPos = pos; + firstMatchTerm = term; + } + } + + if (firstMatchTerm == null) { + // No matches found, return start of content + return content.length <= snippetLength + ? content + : content.substring(0, snippetLength) + ellipsis; + } + + // Calculate snippet range around the first match + int start = (firstMatchPos - snippetLength ~/ 3).clamp(0, content.length); + int end = (start + snippetLength).clamp(0, content.length); + + // Adjust start to not cut words + if (start > 0) { + start = content.lastIndexOf(RegExp(r'\s'), start) + 1; + } + + // Create the snippet + String snippet = content.substring(start, end); + if (start > 0) snippet = ellipsis + snippet; + if (end < content.length) snippet = snippet + ellipsis; + + // Highlight all term matches in the snippet + for (final term in terms) { + final pattern = RegExp(RegExp.escape(term), caseSensitive: false); + snippet = snippet.replaceAllMapped(pattern, + (match) => '${match.group(0)}'); + } + + return snippet; + } }