@@ -117,6 +117,19 @@ CREATE TABLE IF NOT EXISTS regex101_entries (
117117 regex_name <> '' AND
118118 permalink <> '')
119119);
120+
121+ CREATE TABLE IF NOT EXISTS text_bookmarks (
122+ file_path text NOT NULL,
123+ line_number integer NOT NULL,
124+ line_hash text NOT NULL,
125+ mark_type text NOT NULL DEFAULT 'user',
126+ session_time integer NOT NULL,
127+
128+ PRIMARY KEY (file_path, line_number, mark_type),
129+
130+ CHECK(line_number >= 0),
131+ CHECK(mark_type IN ('user', 'sticky'))
132+ );
120133)" ;
121134
122135static const char * const BOOKMARK_LRU_STMT
@@ -129,6 +142,11 @@ static const char* const NETLOC_LRU_STMT
129142 " (SELECT DISTINCT access_time FROM bookmarks "
130143 " ORDER BY access_time DESC LIMIT 1 OFFSET 10)" ;
131144
145+ static const char * const TEXT_BOOKMARK_LRU_STMT
146+ = " DELETE FROM text_bookmarks WHERE session_time <= "
147+ " (SELECT DISTINCT session_time FROM text_bookmarks "
148+ " ORDER BY session_time DESC LIMIT 1 OFFSET 50000)" ;
149+
132150static const char * const UPGRADE_STMTS [] = {
133151 R"( ALTER TABLE bookmarks ADD COLUMN comment text DEFAULT '';)" ,
134152 R"( ALTER TABLE bookmarks ADD COLUMN tags text DEFAULT '';)" ,
@@ -405,6 +423,8 @@ scan_sessions()
405423 return std::make_optional (session_file_names.back ());
406424}
407425
426+ static void load_text_bookmarks (sqlite3* db);
427+
408428void
409429load_time_bookmarks ()
410430{
@@ -742,8 +762,8 @@ load_time_bookmarks()
742762 line_cl);
743763 }
744764 if (sticky) {
745- lss.set_user_mark (
746- &textview_curses:: BM_STICKY , line_cl);
765+ lss.set_user_mark (&textview_curses:: BM_STICKY ,
766+ line_cl);
747767 }
748768 reload_needed = true ;
749769 break ;
@@ -894,6 +914,8 @@ load_time_bookmarks()
894914 if (reload_needed) {
895915 lnav_data.ld_views [LNV_LOG ].reload_data ();
896916 }
917+
918+ load_text_bookmarks (db.in ());
897919}
898920
899921static int
@@ -1129,18 +1151,16 @@ save_user_bookmarks(sqlite3* db,
11291151 continue ;
11301152 }
11311153
1132- auto is_user = user_marks.bv_tree .find (cl)
1133- != user_marks.bv_tree .end ();
1134- auto is_sticky = sticky_marks.bv_tree .find (cl)
1135- != sticky_marks.bv_tree .end ();
1154+ auto is_user = user_marks.bv_tree .find (cl) != user_marks.bv_tree .end ();
1155+ auto is_sticky
1156+ = sticky_marks.bv_tree .find (cl) != sticky_marks.bv_tree .end ();
11361157
11371158 // Use part_name "" for user marks, null for sticky-only
11381159 if (is_user) {
11391160 if (sqlite3_bind_text (stmt, 5 , " " , 0 , SQLITE_TRANSIENT )
11401161 != SQLITE_OK )
11411162 {
1142- log_error (" could not bind part name -- %s" ,
1143- sqlite3_errmsg (db));
1163+ log_error (" could not bind part name -- %s" , sqlite3_errmsg (db));
11441164 return ;
11451165 }
11461166 } else {
@@ -1294,6 +1314,228 @@ save_meta_bookmarks(sqlite3* db, sqlite3_stmt* stmt, logfile* lf)
12941314 }
12951315}
12961316
1317+ static void
1318+ save_text_bookmarks (sqlite3* db)
1319+ {
1320+ auto & tss = lnav_data.ld_text_source ;
1321+
1322+ if (tss.empty ()) {
1323+ return ;
1324+ }
1325+
1326+ tss.copy_bookmarks_to_current_file ();
1327+
1328+ auto_mem<sqlite3_stmt> stmt (sqlite3_finalize);
1329+
1330+ if (sqlite3_prepare_v2 (db,
1331+ " DELETE FROM text_bookmarks WHERE session_time = ?" ,
1332+ -1 ,
1333+ stmt.out (),
1334+ nullptr )
1335+ != SQLITE_OK )
1336+ {
1337+ log_error (" could not prepare text_bookmarks delete -- %s" ,
1338+ sqlite3_errmsg (db));
1339+ return ;
1340+ }
1341+ sqlite3_bind_int64 (stmt.in (), 1 , lnav_data.ld_session_time );
1342+ if (sqlite3_step (stmt.in ()) != SQLITE_DONE ) {
1343+ log_error (" could not execute text_bookmarks delete -- %s" ,
1344+ sqlite3_errmsg (db));
1345+ return ;
1346+ }
1347+
1348+ if (sqlite3_prepare_v2 (
1349+ db,
1350+ " REPLACE INTO text_bookmarks"
1351+ " (file_path, line_number, line_hash, mark_type, session_time)"
1352+ " VALUES (?, ?, ?, ?, ?)" ,
1353+ -1 ,
1354+ stmt.out (),
1355+ nullptr )
1356+ != SQLITE_OK )
1357+ {
1358+ log_error (" could not prepare text_bookmarks replace statement -- %s" ,
1359+ sqlite3_errmsg (db));
1360+ return ;
1361+ }
1362+
1363+ for (const auto & fvs : tss.get_file_states ()) {
1364+ auto & lf = fvs->fvs_file ;
1365+ if (lf == nullptr || lf->size () == 0 ) {
1366+ continue ;
1367+ }
1368+
1369+ auto file_path = lf->get_path_for_key ().string ();
1370+ auto line_count = static_cast <int >(lf->size ());
1371+
1372+ static const bookmark_type_t * SAVE_TYPES [] = {
1373+ &textview_curses::BM_USER ,
1374+ &textview_curses::BM_STICKY ,
1375+ };
1376+
1377+ for (const auto * bm_type : SAVE_TYPES ) {
1378+ auto & bv = fvs->fvs_bookmarks [bm_type];
1379+ if (bv.empty ()) {
1380+ continue ;
1381+ }
1382+
1383+ for (const auto & vl : bv.bv_tree ) {
1384+ if (static_cast <int >(vl) >= line_count) {
1385+ continue ;
1386+ }
1387+
1388+ auto line_iter = lf->begin () + static_cast <int >(vl);
1389+ auto fr = lf->get_file_range (line_iter, false );
1390+ auto read_result = lf->read_range (fr);
1391+
1392+ if (read_result.isErr ()) {
1393+ continue ;
1394+ }
1395+
1396+ auto line_hash
1397+ = read_result
1398+ .map ([](auto sbr) {
1399+ return hasher ()
1400+ .update (sbr.get_data (), sbr.length ())
1401+ .to_string ();
1402+ })
1403+ .unwrap ();
1404+
1405+ sqlite3_clear_bindings (stmt.in ());
1406+ bind_to_sqlite (stmt.in (), 1 , file_path);
1407+ sqlite3_bind_int (stmt.in (), 2 , static_cast <int >(vl));
1408+ bind_to_sqlite (stmt.in (), 3 , line_hash);
1409+ bind_to_sqlite (stmt.in (), 4 , bm_type->get_name ());
1410+ sqlite3_bind_int64 (stmt.in (), 5 , lnav_data.ld_session_time );
1411+
1412+ if (sqlite3_step (stmt.in ()) != SQLITE_DONE ) {
1413+ log_error (" could not execute text_bookmarks insert -- %s" ,
1414+ sqlite3_errmsg (db));
1415+ return ;
1416+ }
1417+
1418+ sqlite3_reset (stmt.in ());
1419+ }
1420+ }
1421+ }
1422+ }
1423+
1424+ static void
1425+ load_text_bookmarks (sqlite3* db)
1426+ {
1427+ static const char * const TEXT_BOOKMARK_STMT = R"(
1428+ SELECT line_number, line_hash, mark_type
1429+ FROM text_bookmarks
1430+ WHERE file_path = ?
1431+ ORDER BY line_number
1432+ )" ;
1433+
1434+ auto & tss = lnav_data.ld_text_source ;
1435+ auto & tc = lnav_data.ld_views [LNV_TEXT ];
1436+
1437+ if (tss.empty ()) {
1438+ return ;
1439+ }
1440+
1441+ auto_mem<sqlite3_stmt> stmt (sqlite3_finalize);
1442+ if (sqlite3_prepare_v2 (db, TEXT_BOOKMARK_STMT , -1 , stmt.out (), nullptr )
1443+ != SQLITE_OK )
1444+ {
1445+ log_error (" could not prepare text_bookmarks select -- %s" ,
1446+ sqlite3_errmsg (db));
1447+ return ;
1448+ }
1449+
1450+ for (const auto & fvs : tss.get_file_states ()) {
1451+ auto & lf = fvs->fvs_file ;
1452+ if (lf == nullptr || lf->size () == 0 ) {
1453+ continue ;
1454+ }
1455+
1456+ auto file_path = lf->get_path_for_key ().string ();
1457+ auto line_count = static_cast <int >(lf->size ());
1458+ sqlite3_reset (stmt.in ());
1459+ sqlite3_clear_bindings (stmt.in ());
1460+ bind_to_sqlite (stmt.in (), 1 , file_path);
1461+
1462+ bool done = false ;
1463+ while (!done) {
1464+ auto rc = sqlite3_step (stmt.in ());
1465+
1466+ switch (rc) {
1467+ case SQLITE_OK :
1468+ case SQLITE_DONE :
1469+ done = true ;
1470+ break ;
1471+
1472+ case SQLITE_ROW : {
1473+ auto line_number = sqlite3_column_int (stmt.in (), 0 );
1474+ auto * stored_hash
1475+ = (const char *) sqlite3_column_text (stmt.in (), 1 );
1476+ auto * mark_type
1477+ = (const char *) sqlite3_column_text (stmt.in (), 2 );
1478+
1479+ if (line_number >= line_count) {
1480+ continue ;
1481+ }
1482+
1483+ auto bm_type_opt = bookmark_type_t::find_type (mark_type);
1484+ if (!bm_type_opt) {
1485+ continue ;
1486+ }
1487+
1488+ auto line_iter = lf->begin () + line_number;
1489+ auto fr = lf->get_file_range (line_iter, false );
1490+ auto read_result = lf->read_range (fr);
1491+
1492+ if (read_result.isErr ()) {
1493+ continue ;
1494+ }
1495+
1496+ auto line_hash
1497+ = read_result
1498+ .map ([](auto sbr) {
1499+ return hasher ()
1500+ .update (sbr.get_data (), sbr.length ())
1501+ .to_string ();
1502+ })
1503+ .unwrap ();
1504+
1505+ if (line_hash != stored_hash) {
1506+ log_warning (" text bookmark hash mismatch at %s:%d" ,
1507+ file_path.c_str (),
1508+ line_number);
1509+ continue ;
1510+ }
1511+
1512+ auto vl = vis_line_t (line_number);
1513+ fvs->fvs_bookmarks [bm_type_opt.value ()].insert_once (vl);
1514+ break ;
1515+ }
1516+
1517+ default :
1518+ log_error (" text bookmark select error: %d -- %s" ,
1519+ rc,
1520+ sqlite3_errmsg (db));
1521+ done = true ;
1522+ break ;
1523+ }
1524+ }
1525+ }
1526+
1527+ // Copy the front file's bookmarks into the textview
1528+ if (!tss.get_file_states ().empty ()) {
1529+ auto & front = tss.get_file_states ().front ();
1530+ tc.get_bookmarks ()[&textview_curses::BM_USER ]
1531+ = front->fvs_bookmarks [&textview_curses::BM_USER ];
1532+ tc.get_bookmarks ()[&textview_curses::BM_STICKY ]
1533+ = front->fvs_bookmarks [&textview_curses::BM_STICKY ];
1534+ }
1535+
1536+ tc.reload_data ();
1537+ }
1538+
12971539static void
12981540save_time_bookmarks ()
12991541{
@@ -1597,6 +1839,8 @@ save_time_bookmarks()
15971839 sqlite3_reset (stmt.in ());
15981840 }
15991841
1842+ save_text_bookmarks (db.in ());
1843+
16001844 log_info (" saved %d bookmarks" , sqlite3_changes (db.in ()));
16011845
16021846 if (sqlite3_exec (db.in (), " COMMIT" , nullptr , nullptr , errmsg.out ())
@@ -1627,6 +1871,18 @@ save_time_bookmarks()
16271871 if (netloc_changes > 0 ) {
16281872 log_info (" deleted %d old netlocs" , netloc_changes);
16291873 }
1874+
1875+ if (sqlite3_exec (
1876+ db.in (), TEXT_BOOKMARK_LRU_STMT , nullptr , nullptr , errmsg.out ())
1877+ != SQLITE_OK )
1878+ {
1879+ log_error (" unable to delete old text bookmarks -- %s" , errmsg.in ());
1880+ return ;
1881+ }
1882+ auto text_bm_changes = sqlite3_changes (db.in ());
1883+ if (text_bm_changes > 0 ) {
1884+ log_info (" deleted %d old text bookmarks" , text_bm_changes);
1885+ }
16301886}
16311887
16321888static void
@@ -1868,6 +2124,10 @@ reset_session()
18682124 tc.reload_data ();
18692125 }
18702126
2127+ for (auto & fvs : lnav_data.ld_text_source .get_file_states ()) {
2128+ fvs->fvs_bookmarks .clear ();
2129+ }
2130+
18712131 lnav_data.ld_filter_view .reload_data ();
18722132 lnav_data.ld_files_view .reload_data ();
18732133 for (const auto & format : log_format::get_root_formats ()) {
0 commit comments