1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/// Yank (clipboard copy) helpers for the viewer.
///
/// All methods are part of `impl App`.
// Submodule of app — intentionally imports all parent symbols.
#[allow(clippy::wildcard_imports)]
use super::*;
impl App {
/// Copy the source-level text of the current cursor line to the system
/// clipboard via OSC 52. Invoked by the `yy` chord in the viewer.
pub(super) fn yank_current_line(&mut self) {
let Some(tab) = self.tabs.active_tab() else {
return;
};
let target_source =
crate::markdown::source_line_at(&tab.view.rendered, tab.view.cursor_line);
// `content` is the raw markdown; we index into its lines.
let content = tab.view.content.clone();
if let Some(line) = content.lines().nth(target_source as usize) {
copy_to_clipboard(line);
}
}
/// Copy the source-level text covered by the current visual-line selection
/// to the system clipboard, then exit visual mode. Invoked by `y` in visual mode.
pub(super) fn yank_visual_selection(&mut self) {
use crate::ui::markdown_view::{VisualMode, extract_line_text_range};
let Some(tab) = self.tabs.active_tab_mut() else {
return;
};
let Some(range) = tab.view.visual_mode else {
return;
};
let text = match range.mode {
VisualMode::Line => {
// Line mode: yank whole source lines (existing behaviour).
let top_source =
crate::markdown::source_line_at(&tab.view.rendered, range.top_line());
let bottom_source =
crate::markdown::source_line_at(&tab.view.rendered, range.bottom_line());
build_yank_text(&tab.view.content, top_source, bottom_source)
}
VisualMode::Char => {
// Char mode: extract rendered text from only the selected columns.
// Walk each line in [top_line, bottom_line] and extract the column
// range reported by char_range_on_line.
let mut parts: Vec<String> = Vec::new();
let mut block_offset = 0u32;
let top = range.top_line();
let bottom = range.bottom_line();
'blocks: for block in &tab.view.rendered {
let height = block.height();
let block_end = block_offset + height;
if block_end <= top {
block_offset = block_end;
continue;
}
if block_offset > bottom {
break;
}
if let crate::markdown::DocBlock::Text { text, .. } = block {
for (local_idx, line) in text.lines.iter().enumerate() {
let abs = block_offset + crate::cast::u32_sat(local_idx);
if abs > bottom {
break 'blocks;
}
// Compute display width of this line from its spans.
let line_width: u16 = line
.spans
.iter()
.map(|s| {
crate::cast::u16_sat(unicode_width::UnicodeWidthStr::width(
s.content.as_ref(),
))
})
.fold(0u16, u16::saturating_add);
if let Some((sc, ec)) = range.char_range_on_line(abs, line_width) {
parts.push(extract_line_text_range(line, sc, ec));
}
}
}
block_offset = block_end;
}
parts.join("\n")
}
};
copy_to_clipboard(&text);
tab.view.visual_mode = None;
}
}