Skip to content

Commit ab0b9b3

Browse files
committed
Add search on document
1 parent 9bfb95e commit ab0b9b3

6 files changed

Lines changed: 826 additions & 12 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ A lightweight GTK 3 markdown viewer for desktop Linux. It is ideal as your defau
1111
- **Minimal UI** - Clean toolbar with open and settings buttons
1212
- **Lightweight** - Pure C, no web technologies, fast startup
1313
- **Hyperlink support** - Left click opens links and internal anchors
14+
- **Document search** - `Ctrl+F` with next/previous match navigation
1415

1516
## Supported Markdown
1617

@@ -59,8 +60,16 @@ sudo dnf install ./viewmd-*.rpm
5960
Run `viewmd` to start the application.
6061

6162
- **Open button**: Open a markdown document
63+
- **Reload button**: Reload the currently open document from disk
6264
- **Settings button**: Adjust theme, fonts, and markdown accent colors
6365

66+
### Find in Document
67+
68+
- Press `Ctrl+F` to open search.
69+
- Type to highlight matches as you search.
70+
- Press `Enter` for next match and `Shift+Enter` for previous match.
71+
- Press `Esc` to close search.
72+
6473
### Set as Default `.md` Viewer
6574

6675
After installing, associate markdown MIME types with `viewmd.desktop`:

src/editor.c

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ static void apply_markdown(MarkydEditor *self);
1212
static void schedule_markdown_apply(MarkydEditor *self);
1313
static void render_table_widgets(MarkydEditor *self);
1414

15-
static const gchar *TABLE_WIDGET_DATA_KEY = "viewmd-table-widget";
16-
1715
static gchar *get_url_from_iter_tags(GtkTextIter *iter) {
1816
GSList *tags;
1917
gchar *url = NULL;
@@ -152,14 +150,15 @@ static void render_table_widgets(MarkydEditor *self) {
152150
GtkTextChildAnchor *anchor = gtk_text_iter_get_child_anchor(&iter);
153151
if (anchor &&
154152
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_ANCHOR_DATA) != NULL) {
155-
GtkWidget *table = g_object_get_data(G_OBJECT(anchor), TABLE_WIDGET_DATA_KEY);
153+
GtkWidget *table =
154+
g_object_get_data(G_OBJECT(anchor), VIEWMD_TABLE_WIDGET_DATA);
156155
if (!table) {
157156
table = markdown_create_table_widget(anchor);
158157
if (table) {
159158
gtk_text_view_add_child_at_anchor(GTK_TEXT_VIEW(self->text_view), table,
160159
anchor);
161160
gtk_widget_show_all(table);
162-
g_object_set_data(G_OBJECT(anchor), TABLE_WIDGET_DATA_KEY, table);
161+
g_object_set_data(G_OBJECT(anchor), VIEWMD_TABLE_WIDGET_DATA, table);
163162
}
164163
}
165164
}

src/markdown.c

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,103 @@ static void table_capture_append(RenderCtx *ctx, const gchar *text) {
589589
g_free(escaped);
590590
}
591591

592+
static gchar *table_cell_markup_to_plain(const gchar *markup) {
593+
gchar *plain = NULL;
594+
GError *error = NULL;
595+
596+
if (!markup || markup[0] == '\0') {
597+
return g_strdup("");
598+
}
599+
600+
if (pango_parse_markup(markup, -1, 0, NULL, &plain, NULL, &error)) {
601+
return plain;
602+
}
603+
604+
if (error) {
605+
g_error_free(error);
606+
}
607+
return g_strdup(markup);
608+
}
609+
610+
static void table_search_index_free(gpointer data) {
611+
ViewmdTableSearchIndex *index = (ViewmdTableSearchIndex *)data;
612+
if (!index) {
613+
return;
614+
}
615+
if (index->cells) {
616+
g_array_free(index->cells, TRUE);
617+
}
618+
g_free(index);
619+
}
620+
621+
static void table_emit_hidden_search_text(RenderCtx *ctx, ViewmdTable *table,
622+
GtkTextChildAnchor *anchor) {
623+
ViewmdTableSearchIndex *index;
624+
625+
if (!ctx || !ctx->buffer || !table || !anchor || !table->rows ||
626+
table->rows->len == 0 || table->col_count == 0) {
627+
return;
628+
}
629+
630+
index = g_new0(ViewmdTableSearchIndex, 1);
631+
index->cells = g_array_new(FALSE, FALSE, sizeof(ViewmdTableSearchCellRange));
632+
index->start_offset = gtk_text_iter_get_offset(&ctx->iter);
633+
634+
for (guint r = 0; r < table->rows->len; r++) {
635+
ViewmdTableRow *row = g_ptr_array_index(table->rows, r);
636+
if (!row) {
637+
continue;
638+
}
639+
640+
for (guint c = 0; c < table->col_count; c++) {
641+
const gchar *cell_markup = "";
642+
gchar *plain;
643+
gint cell_start;
644+
gint cell_end;
645+
646+
if (c < row->cells->len) {
647+
cell_markup = g_ptr_array_index(row->cells, c);
648+
if (!cell_markup) {
649+
cell_markup = "";
650+
}
651+
}
652+
653+
plain = table_cell_markup_to_plain(cell_markup);
654+
cell_start = gtk_text_iter_get_offset(&ctx->iter);
655+
if (plain && plain[0] != '\0') {
656+
gtk_text_buffer_insert(ctx->buffer, &ctx->iter, plain, -1);
657+
}
658+
cell_end = gtk_text_iter_get_offset(&ctx->iter);
659+
660+
if (cell_end > cell_start) {
661+
ViewmdTableSearchCellRange cell_range = {(gint)r, (gint)c, cell_start,
662+
cell_end};
663+
g_array_append_val(index->cells, cell_range);
664+
}
665+
666+
g_free(plain);
667+
668+
if (c + 1 < table->col_count) {
669+
gtk_text_buffer_insert(ctx->buffer, &ctx->iter, "\t", 1);
670+
}
671+
}
672+
673+
if (r + 1 < table->rows->len) {
674+
gtk_text_buffer_insert(ctx->buffer, &ctx->iter, "\n", 1);
675+
}
676+
}
677+
678+
index->end_offset = gtk_text_iter_get_offset(&ctx->iter);
679+
if (index->end_offset > index->start_offset) {
680+
apply_tag_by_name_offsets(ctx->buffer, TAG_INVISIBLE, index->start_offset,
681+
index->end_offset);
682+
g_object_set_data_full(G_OBJECT(anchor), VIEWMD_TABLE_SEARCH_INDEX_DATA, index,
683+
table_search_index_free);
684+
} else {
685+
table_search_index_free(index);
686+
}
687+
}
688+
592689
static void table_capture_span_enter(RenderCtx *ctx, MD_SPANTYPE type) {
593690
if (!ctx || !ctx->table_cell_text) {
594691
return;
@@ -711,6 +808,9 @@ static void table_emit_anchor(RenderCtx *ctx) {
711808
g_object_set_data_full(G_OBJECT(anchor), TABLE_MODEL_DATA_KEY, ctx->table_model,
712809
viewmd_table_free);
713810

811+
/* Keep table text searchable via Ctrl+F without showing duplicate content. */
812+
table_emit_hidden_search_text(ctx, ctx->table_model, anchor);
813+
714814
/* Force at least one hard line break after the embedded table widget so
715815
* following markdown never shares the same visual line. */
716816
insert_cstr(ctx, "\n");
@@ -1241,6 +1341,10 @@ GtkWidget *markdown_create_table_widget(GtkTextChildAnchor *anchor) {
12411341
gtk_style_context_add_class(gtk_widget_get_style_context(cell),
12421342
"viewmd-table-header-cell");
12431343
}
1344+
g_object_set_data(G_OBJECT(cell), VIEWMD_TABLE_CELL_ROW_DATA,
1345+
GINT_TO_POINTER((gint)r));
1346+
g_object_set_data(G_OBJECT(cell), VIEWMD_TABLE_CELL_COL_DATA,
1347+
GINT_TO_POINTER((gint)c));
12441348
gtk_style_context_add_class(gtk_widget_get_style_context(label),
12451349
"viewmd-table-label");
12461350
gtk_widget_set_hexpand(cell, FALSE);

src/markdown.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,29 @@ void markdown_update_accent_tags(GtkTextBuffer *buffer);
2020

2121
/* GObject data key used to mark table child anchors with parsed table data. */
2222
#define VIEWMD_TABLE_ANCHOR_DATA "viewmd-table-anchor"
23+
/* GObject data key set on table anchors for hidden searchable index metadata. */
24+
#define VIEWMD_TABLE_SEARCH_INDEX_DATA "viewmd-table-search-index"
25+
/* GObject data key set on table anchors for attached table widget instance. */
26+
#define VIEWMD_TABLE_WIDGET_DATA "viewmd-table-widget"
27+
/* GObject data keys set on each table cell widget. */
28+
#define VIEWMD_TABLE_CELL_ROW_DATA "viewmd-table-cell-row"
29+
#define VIEWMD_TABLE_CELL_COL_DATA "viewmd-table-cell-col"
30+
/* CSS classes for table search highlight states. */
31+
#define VIEWMD_TABLE_CELL_MATCH_CLASS "viewmd-table-cell-match"
32+
#define VIEWMD_TABLE_CELL_CURRENT_CLASS "viewmd-table-cell-current"
33+
34+
typedef struct {
35+
gint row;
36+
gint col;
37+
gint start_offset;
38+
gint end_offset;
39+
} ViewmdTableSearchCellRange;
40+
41+
typedef struct {
42+
gint start_offset;
43+
gint end_offset;
44+
GArray *cells; /* ViewmdTableSearchCellRange */
45+
} ViewmdTableSearchIndex;
2346

2447
/* Normalize heading/link text into anchor slug form. Caller owns result. */
2548
gchar *markdown_normalize_anchor_slug(const gchar *text);

0 commit comments

Comments
 (0)