diff --git a/lib/db.dart b/lib/db.dart index 227a081..92f604a 100644 --- a/lib/db.dart +++ b/lib/db.dart @@ -85,87 +85,6 @@ CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN END; '''; - static Future _getIcuExtensionPath() async { - if (!Platform.isWindows) { - debugPrint('Not on Windows, skipping ICU'); - return null; - } - - try { - // Get the executable's directory - final exePath = Platform.resolvedExecutable; - final exeDir = path.dirname(exePath); - final currentDir = Directory.current.path; - - debugPrint('\n================== ICU EXTENSION SETUP =================='); - debugPrint('Current working directory: $currentDir'); - debugPrint('Executable path: $exePath'); - debugPrint('Executable directory: $exeDir'); - - // In release mode, we want the DLL next to the exe - final targetPath = path.join(exeDir, 'sqlite3_icu.dll'); - final targetFile = File(targetPath); - - // If the DLL doesn't exist in the target location, copy it from assets - if (!await targetFile.exists()) { - debugPrint( - 'DLL not found at target location, attempting to copy from assets...', - ); - try { - // Load from assets and write to target location - final assetPath = 'assets/windows/sqlite3_icu.dll'; - debugPrint('Trying to load from asset path: $assetPath'); - - final data = await rootBundle.load(assetPath); - debugPrint( - 'Asset loaded successfully, size: ${data.lengthInBytes} bytes', - ); - - debugPrint('Writing to target path: $targetPath'); - await targetFile.writeAsBytes( - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes), - ); - debugPrint('Successfully copied DLL to: $targetPath'); - } catch (e, stackTrace) { - debugPrint('Failed to copy DLL from assets:'); - debugPrint('Error: $e'); - debugPrint('Stack trace: $stackTrace'); - - // Try to list available assets for debugging - try { - final manifestContent = await rootBundle.loadString( - 'AssetManifest.json', - ); - debugPrint('Available assets in manifest:'); - debugPrint(manifestContent); - } catch (e) { - debugPrint('Could not load asset manifest: $e'); - } - return null; - } - } else { - debugPrint('Found existing DLL at: $targetPath'); - } - - debugPrint('Verifying DLL exists at: $targetPath'); - if (await targetFile.exists()) { - final fileSize = await targetFile.length(); - debugPrint('DLL exists, size: $fileSize bytes'); - } else { - debugPrint('DLL does not exist at target path!'); - } - - debugPrint('=====================================================\n'); - return targetPath; - } catch (e, stackTrace) { - debugPrint('Error setting up ICU extension:'); - debugPrint('Error: $e'); - debugPrint('Stack trace: $stackTrace'); - debugPrint('=====================================================\n'); - return null; - } - } - // Add this method to check ICU status with visible feedback static Future checkAndShowIcuStatus() async { final status = _hasIcuSupport; @@ -452,122 +371,47 @@ END; 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('_', '\\_'); + // Process terms for FTS5 query using proper tokenization + String ftsQuery = terms + .map((term) { + // Remove dangerous characters but preserve Unicode + String sanitizedTerm = term.replaceAll(RegExp(r'''['"]'''), ''); + if (sanitizedTerm.isEmpty) return ''; - // 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(); + // Use proper FTS5 syntax: each word becomes a separate token with prefix matching + List words = sanitizedTerm.split(RegExp(r'\s+')); + return words.map((word) => '$word*').join(' OR '); + }) + .where((term) => term.isNotEmpty) + .join(' AND '); - if (matchExpressions.isEmpty) { + if (ftsQuery.isEmpty) { debugPrint('Query was sanitized to empty string'); return []; } - // 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]); - } - } + debugPrint('FTS query: "$ftsQuery"'); - debugPrint('Search query: "$query" with ${terms.length} terms'); - debugPrint('Where clause: $whereClause'); - debugPrint('Parameters: $parameters'); - - // Execute the search query + // Execute the FTS query final List> results = await db.rawQuery( ''' - SELECT n.id, n.date, n.content - FROM notes n - WHERE $whereClause - ORDER BY n.date DESC + SELECT n.id, n.date, n.content, + snippet(notes_fts, -1, '', '', '...', 64) as snippet + FROM notes_fts + JOIN notes n ON notes_fts.rowid = n.id + WHERE notes_fts MATCH ? + ORDER BY rank LIMIT 100 ''', - parameters, + [ftsQuery], ); - // 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; + return results; } catch (e, stackTrace) { debugPrint('Search failed: $e'); debugPrint('Stack trace: $stackTrace'); - rethrow; + return []; } } - - // 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; - } } diff --git a/lib/main.dart b/lib/main.dart index 56970dc..bb6c60b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1090,7 +1090,7 @@ class MainPageState extends State with WindowListener { border: const OutlineInputBorder(), filled: _currentlyDisplayedNote?.displayDate != - previousNote?.displayDate, + previousNote?.displayDate, fillColor: _currentlyDisplayedNote?.displayDate != previousNote?.displayDate