fix buffer tag search by the fragment

This commit is contained in:
yggverse 2026-03-11 04:33:50 +02:00
parent a1d9c080d1
commit 6a491751b6
3 changed files with 34 additions and 36 deletions

View file

@ -4,11 +4,11 @@ mod tags;
use super::{ItemAction, WindowAction}; use super::{ItemAction, WindowAction};
use crate::{app::browser::window::action::Position, profile::Profile}; use crate::{app::browser::window::action::Position, profile::Profile};
use gtk::{ use gtk::{
EventControllerMotion, GestureClick, PopoverMenu, TextBuffer, TextSearchFlags, TextTag, EventControllerMotion, GestureClick, PopoverMenu, TextBuffer, TextTag, TextTagTable, TextView,
TextTagTable, TextView, TextWindowType, UriLauncher, Window, WrapMode, TextWindowType, UriLauncher, Window, WrapMode,
gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA}, gdk::{BUTTON_MIDDLE, BUTTON_PRIMARY, BUTTON_SECONDARY, Display, RGBA},
gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup}, gio::{Cancellable, Menu, SimpleAction, SimpleActionGroup},
glib::{ControlFlow, GString, Uri, idle_add_local, uri_unescape_string, uuid_string_random}, glib::{ControlFlow, GString, Uri, idle_add_local, uuid_string_random},
prelude::{PopoverExt, TextBufferExt, TextTagExt, TextViewExt, WidgetExt}, prelude::{PopoverExt, TextBufferExt, TextTagExt, TextViewExt, WidgetExt},
}; };
use gutter::Gutter; use gutter::Gutter;
@ -312,6 +312,7 @@ impl Markdown {
// Init events // Init events
primary_button_controller.connect_released({ primary_button_controller.connect_released({
let headers = headers.clone();
let item_action = item_action.clone(); let item_action = item_action.clone();
let links = links.clone(); let links = links.clone();
let text_view = text_view.clone(); let text_view = text_view.clone();
@ -327,7 +328,7 @@ impl Markdown {
// Tag is link // Tag is link
if let Some(uri) = links.get(&tag) { if let Some(uri) = links.get(&tag) {
return if let Some(fragment) = uri.fragment() { return if let Some(fragment) = uri.fragment() {
scroll_to_anchor(&text_view, fragment); scroll_to_anchor(&text_view, &headers, fragment);
} else { } else {
open_link_in_current_tab(&uri.to_string(), &item_action); open_link_in_current_tab(&uri.to_string(), &item_action);
}; };
@ -338,10 +339,10 @@ impl Markdown {
}); });
secondary_button_controller.connect_pressed({ secondary_button_controller.connect_pressed({
let links = links.clone();
let headers = headers.clone(); let headers = headers.clone();
let text_view = text_view.clone();
let link_context = link_context.clone(); let link_context = link_context.clone();
let links = links.clone();
let text_view = text_view.clone();
move |_, _, window_x, window_y| { move |_, _, window_x, window_y| {
let x = window_x as i32; let x = window_x as i32;
let y = window_y as i32; let y = window_y as i32;
@ -518,7 +519,7 @@ impl Markdown {
let text_view = text_view.clone(); let text_view = text_view.clone();
move || { move || {
if let Some(fragment) = base.fragment() { if let Some(fragment) = base.fragment() {
scroll_to_anchor(&text_view, fragment); scroll_to_anchor(&text_view, &headers, fragment);
} }
ControlFlow::Break ControlFlow::Break
} }
@ -528,29 +529,20 @@ impl Markdown {
} }
} }
fn scroll_to_anchor(text_view: &TextView, fragment: GString) -> bool { fn scroll_to_anchor(
fn try_scroll(text_view: &TextView, query: &str) -> bool { text_view: &TextView,
let mut cursor = text_view.buffer().start_iter(); headers: &HashMap<TextTag, (String, Uri)>,
while let Some((mut match_start, match_end)) = fragment: GString,
cursor.forward_search(query, TextSearchFlags::CASE_INSENSITIVE, None) ) {
{ if let Some((tag, _)) = headers.iter().find(|(_, (_, uri))| {
if match_start uri.fragment()
.tags() .is_some_and(|f| fragment == tags::format_header_fragment(&f))
.iter() }) {
.any(|t| t.name().is_some_and(|n| n.starts_with("h"))) let mut iter = text_view.buffer().start_iter();
{ if iter.starts_tag(Some(tag)) || iter.forward_to_tag_toggle(Some(tag)) {
return text_view.scroll_to_iter(&mut match_start, 0.0, true, 0.0, 0.0); text_view.scroll_to_iter(&mut iter, 0.0, true, 0.0, 0.0);
} }
cursor = match_end;
} }
false
}
let query = uri_unescape_string(&fragment, None::<&str>).unwrap_or(fragment);
let result = try_scroll(text_view, &query); // exact match
if !result {
return try_scroll(text_view, &query.replace(" ", "-")); // alt syntax
}
result
} }
fn is_internal_link(request: &str) -> bool { fn is_internal_link(request: &str) -> bool {

View file

@ -10,7 +10,12 @@ mod underline;
use bold::Bold; use bold::Bold;
use code::Code; use code::Code;
use gtk::{TextBuffer, TextSearchFlags, TextTag, gdk::RGBA, glib::Uri, prelude::TextBufferExt}; use gtk::{
TextBuffer, TextSearchFlags, TextTag,
gdk::RGBA,
glib::{GString, Uri},
prelude::TextBufferExt,
};
use header::Header; use header::Header;
use pre::Pre; use pre::Pre;
use quote::Quote; use quote::Quote;
@ -99,4 +104,9 @@ impl Tags {
} }
} }
/// Shared URL #fragment logic (for the Header tags ref)
pub fn format_header_fragment(value: &str) -> GString {
Uri::escape_string(&value.to_lowercase().replace(" ", "-"), None, true)
}
const ESC: &str = "\\"; const ESC: &str = "\\";

View file

@ -126,8 +126,8 @@ impl Header {
} }
// Create unique phantom tag for each header // Create unique phantom tag for each header
// * it is required for context menu relationships // * for the #fragment references implementation
let h = TextTag::builder().build(); let h = TextTag::new(Some(&format!("h{}", gtk::glib::uuid_string_random())));
assert!(table.add(&h)); assert!(table.add(&h));
// Render header in text buffer // Render header in text buffer
@ -158,11 +158,7 @@ impl Header {
base.port(), base.port(),
&base.path(), &base.path(),
base.query().as_deref(), base.query().as_deref(),
Some(&Uri::escape_string( Some(&super::format_header_fragment(&cap["title"])),
&cap["title"].to_lowercase().replace(" ", "-"),
None,
true
)),
) )
), ),
) )