From 0d2da304ff8a463faa71409438d90e04eeafbd8c Mon Sep 17 00:00:00 2001 From: RivoLink Date: Wed, 1 Jul 2026 23:43:26 +0300 Subject: [PATCH] chore: improve code-block hit bounds --- src/app/code_blocks.rs | 12 +++++--- src/markdown/blocks.rs | 27 +++++++++++------ src/markdown/mod.rs | 56 +++++++++++++++++++++++++----------- src/runtime/mouse.rs | 16 +++++++++-- src/tests/app_code_blocks.rs | 8 +++--- 5 files changed, 83 insertions(+), 36 deletions(-) diff --git a/src/app/code_blocks.rs b/src/app/code_blocks.rs index c2fa2d3..290faaf 100644 --- a/src/app/code_blocks.rs +++ b/src/app/code_blocks.rs @@ -96,10 +96,14 @@ impl App { } } - pub(crate) fn code_block_at_line(&self, line_idx: usize) -> Option { - self.code_blocks - .iter() - .position(|b| line_idx >= b.rendered_start && line_idx <= b.rendered_end) + pub(crate) fn code_block_at(&self, line_idx: usize, inner_col: u16) -> Option { + let col = inner_col as usize; + self.code_blocks.iter().position(|b| { + line_idx >= b.rendered_start + && line_idx <= b.rendered_end + && col >= b.rendered_offset + && col < b.rendered_offset + b.rendered_width + }) } pub(crate) fn highlighted_code_block(&self) -> Option { diff --git a/src/markdown/blocks.rs b/src/markdown/blocks.rs index 38be28a..069f258 100644 --- a/src/markdown/blocks.rs +++ b/src/markdown/blocks.rs @@ -15,6 +15,11 @@ use super::width::display_width; use super::wrapping::{push_wrapped_code_lines, push_wrapped_prefixed_lines}; use super::LastBlock; +pub(super) struct BlockLayout { + pub(super) prefix_width: usize, + pub(super) rendered_width: usize, +} + pub(super) fn block_prefix( in_bq: bool, theme: &MarkdownTheme, @@ -169,7 +174,7 @@ pub(super) fn push_code_block_lines( code_lang: &mut String, ctx: CodeBlockRenderContext<'_>, item_stack: &mut [ItemState], -) { +) -> BlockLayout { let prefix = if !item_stack.is_empty() { list_item_prefix( ctx.blockquote_depth > 0, @@ -275,9 +280,12 @@ pub(super) fn push_code_block_lines( Style::default().fg(ctx.theme_colors.code_frame), )); lines.push(Line::from(footer)); - lines.push(Line::from("")); code_lang.clear(); code_buf.clear(); + BlockLayout { + prefix_width, + rendered_width: inner_width + 2, + } } pub(super) struct SpecialBlockCtx<'a, F: Fn(&str) -> Vec>> { @@ -296,7 +304,7 @@ pub(super) fn push_special_block_lines Vec>>( list_stack: &[ListKind], item_stack: &mut [ItemState], ctx: SpecialBlockCtx<'_, F>, -) { +) -> BlockLayout { let label = ctx.label; let content_lines = ctx.content_lines; let show_line_numbers = ctx.show_line_numbers; @@ -409,7 +417,10 @@ pub(super) fn push_special_block_lines Vec>>( )); } lines.push(Line::from(footer)); - lines.push(Line::from("")); + BlockLayout { + prefix_width, + rendered_width: inner_width + 2, + } } pub(super) struct EmbeddedBlockCtx<'a> { @@ -425,7 +436,7 @@ pub(super) fn push_latex_block_lines( content: &str, ctx: EmbeddedBlockCtx<'_>, item_stack: &mut [ItemState], -) { +) -> BlockLayout { let rendered = latex::to_unicode(content); let all_lines: Vec<&str> = rendered.lines().collect(); let start = all_lines @@ -451,7 +462,7 @@ pub(super) fn push_latex_block_lines( center: false, make_spans: |line| vec![Span::styled(line.to_string(), content_style)], }, - ); + ) } pub(super) fn push_mermaid_block_lines( @@ -459,7 +470,7 @@ pub(super) fn push_mermaid_block_lines( content: &str, ctx: EmbeddedBlockCtx<'_>, item_stack: &mut [ItemState], -) { +) -> BlockLayout { let rendered = mermaid::render(content); let use_rendered = rendered.is_some(); let content_lines: Vec<&str> = if let Some(ref r) = rendered { @@ -488,7 +499,7 @@ pub(super) fn push_mermaid_block_lines( } }, }, - ); + ) } pub(super) fn push_rule_line( diff --git a/src/markdown/mod.rs b/src/markdown/mod.rs index 7382542..4dfc9de 100644 --- a/src/markdown/mod.rs +++ b/src/markdown/mod.rs @@ -40,7 +40,7 @@ use toc::{normalize_toc, TocEntry}; use blocks::{ flush_wrapped_spans, push_code_block_lines, push_heading_lines, push_latex_block_lines, - push_mermaid_block_lines, push_rule_line, trim_paragraph_gap_before_block, + push_mermaid_block_lines, push_rule_line, trim_paragraph_gap_before_block, BlockLayout, CodeBlockRenderContext, EmbeddedBlockCtx, CODE_BLOCK_GUTTER, }; use fences::normalize_code_fences; @@ -224,6 +224,8 @@ fn push_text_event( pub(crate) struct CodeBlockInfo { pub(crate) rendered_start: usize, pub(crate) rendered_end: usize, + pub(crate) rendered_offset: usize, + pub(crate) rendered_width: usize, pub(crate) raw_content: String, } @@ -231,16 +233,19 @@ fn record_code_block( code_blocks: &mut Vec, raw_content: String, rendered_start: usize, - lines_len_after: usize, + rendered_end: usize, + layout: BlockLayout, ) { - let rendered_end = lines_len_after.saturating_sub(1); - if rendered_end >= rendered_start { - code_blocks.push(CodeBlockInfo { - rendered_start, - rendered_end, - raw_content, - }); + if rendered_end < rendered_start { + return; } + code_blocks.push(CodeBlockInfo { + rendered_start, + rendered_end, + rendered_offset: layout.prefix_width, + rendered_width: layout.rendered_width, + raw_content, + }); } pub(crate) struct ParseResult { @@ -411,8 +416,8 @@ pub(crate) fn parse_markdown_with_width( in_code = false; let raw_content = code_buf.clone(); let rendered_start = lines.len(); - if code_lang == "latex" || code_lang == "tex" { - push_latex_block_lines( + let layout = if code_lang == "latex" || code_lang == "tex" { + let layout = push_latex_block_lines( &mut lines, &code_buf, EmbeddedBlockCtx { @@ -427,8 +432,9 @@ pub(crate) fn parse_markdown_with_width( code_buf.clear(); code_lang.clear(); wraps = true; + layout } else if code_lang == "mermaid" { - push_mermaid_block_lines( + let layout = push_mermaid_block_lines( &mut lines, &code_buf, EmbeddedBlockCtx { @@ -442,8 +448,9 @@ pub(crate) fn parse_markdown_with_width( ); code_buf.clear(); code_lang.clear(); + layout } else { - push_code_block_lines( + let layout = push_code_block_lines( &mut lines, &mut code_buf, &mut code_lang, @@ -460,8 +467,16 @@ pub(crate) fn parse_markdown_with_width( &mut item_stack, ); wraps = true; - } - record_code_block(&mut code_blocks, raw_content, rendered_start, lines.len()); + layout + }; + record_code_block( + &mut code_blocks, + raw_content, + rendered_start, + lines.len().saturating_sub(1), + layout, + ); + lines.push(Line::from("")); last_block = LastBlock::Other; } MdEvent::Code(text) => { @@ -570,7 +585,7 @@ pub(crate) fn parse_markdown_with_width( trim_paragraph_gap_before_block(&mut lines, last_block); let raw_content = text.as_ref().trim().to_string(); let rendered_start = lines.len(); - push_latex_block_lines( + let layout = push_latex_block_lines( &mut lines, text.as_ref(), EmbeddedBlockCtx { @@ -582,7 +597,14 @@ pub(crate) fn parse_markdown_with_width( }, &mut item_stack, ); - record_code_block(&mut code_blocks, raw_content, rendered_start, lines.len()); + record_code_block( + &mut code_blocks, + raw_content, + rendered_start, + lines.len().saturating_sub(1), + layout, + ); + lines.push(Line::from("")); wraps = true; last_block = LastBlock::Other; } diff --git a/src/runtime/mouse.rs b/src/runtime/mouse.rs index 6f78d9b..06aa0d7 100644 --- a/src/runtime/mouse.rs +++ b/src/runtime/mouse.rs @@ -161,8 +161,14 @@ pub(super) fn handle_mouse_event(app: &mut App, mouse: MouseEvent) -> bool { scrollbar_scroll_to(app, mouse.row); true } else if is_double_click { - let block_hit = line_idx_at(app, mouse.column, mouse.row) - .and_then(|line_idx| app.code_block_at_line(line_idx)); + let inner_x = content_inner_x(app.content_area, gutter); + let block_hit = if mouse.column >= inner_x { + let inner_col = mouse.column - inner_x; + line_idx_at(app, mouse.column, mouse.row) + .and_then(|line_idx| app.code_block_at(line_idx, inner_col)) + } else { + None + }; if let Some(block_idx) = block_hit { app.code_select = Some(block_idx); app.copy_code_block_at(block_idx); @@ -354,10 +360,14 @@ fn is_in_rect(rect: Rect, col: u16, row: u16) -> bool { col >= rect.x && col < rect.x + rect.width && row >= rect.y && row < rect.y + rect.height } +fn content_inner_x(area: Rect, gutter: u16) -> u16 { + area.x + CONTENT_HORIZONTAL_PADDING + gutter +} + fn line_idx_at(app: &App, col: u16, row: u16) -> Option { let area = app.content_area; let gutter = app.line_number_gutter_width() as u16; - let inner_x = area.x + CONTENT_HORIZONTAL_PADDING + gutter; + let inner_x = content_inner_x(area, gutter); let inner_w = area .width .saturating_sub(CONTENT_HORIZONTAL_PADDING * 2) diff --git a/src/tests/app_code_blocks.rs b/src/tests/app_code_blocks.rs index f737657..6281883 100644 --- a/src/tests/app_code_blocks.rs +++ b/src/tests/app_code_blocks.rs @@ -61,7 +61,7 @@ fn copy_selected_code_block_works_for_mermaid_latex_and_display_math() { } #[test] -fn code_block_at_line_resolves_mermaid_latex_and_display_math() { +fn code_block_at_resolves_mermaid_latex_and_display_math() { let app = build_app_with_mixed_blocks(); let mermaid = &app.code_blocks[1]; let latex = &app.code_blocks[2]; @@ -69,7 +69,7 @@ fn code_block_at_line_resolves_mermaid_latex_and_display_math() { let mermaid_mid = (mermaid.rendered_start + mermaid.rendered_end) / 2; let latex_mid = (latex.rendered_start + latex.rendered_end) / 2; let display_math_mid = (display_math.rendered_start + display_math.rendered_end) / 2; - assert_eq!(app.code_block_at_line(mermaid_mid), Some(1)); - assert_eq!(app.code_block_at_line(latex_mid), Some(2)); - assert_eq!(app.code_block_at_line(display_math_mid), Some(3)); + assert_eq!(app.code_block_at(mermaid_mid, 0), Some(1)); + assert_eq!(app.code_block_at(latex_mid, 0), Some(2)); + assert_eq!(app.code_block_at(display_math_mid, 0), Some(3)); }