Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,10 @@ watch = false
# Text files (txt, csv, log...) are rendered as plain Markdown.
# Default: [] (Markdown only)
# extras = ["txt", "csv", "rs", "java", "json", "yaml"]

# Show line numbers inside fenced code blocks
# Only affects code block gutters, the main document line numbers are independent.
# Priority: LEAF_CODE_LINE_NUMBERS > this setting > default (true)
# Accepted env values: 1 = true, 0 = false (other values ignored)
# Default: true
# code-line-numbers = true
2 changes: 2 additions & 0 deletions src/app/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ impl App {
self.render_width,
&at.markdown,
self.file_mode,
self.code_line_numbers,
);

let first_load = self.filepath.is_none();
Expand Down Expand Up @@ -139,6 +140,7 @@ impl App {
self.render_width,
&at.markdown,
self.file_mode,
self.code_line_numbers,
);
let new_total = parsed.lines.len();

Expand Down
6 changes: 6 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ pub(crate) struct App {
numkey_cycle: Option<NumkeyCycleState>,
reverse_mode: bool,
pub(super) file_mode: bool,
pub(super) code_line_numbers: bool,
max_width: Option<usize>,
}

Expand Down Expand Up @@ -287,6 +288,7 @@ impl App {
numkey_cycle: None,
reverse_mode: false,
file_mode: false,
code_line_numbers: true,
max_width: None,
};
app.store_current_theme_preview();
Expand All @@ -302,6 +304,10 @@ impl App {
self.file_mode = file_mode;
}

pub(crate) fn set_code_line_numbers(&mut self, value: bool) {
self.code_line_numbers = value;
}

pub(crate) fn set_max_width(&mut self, max_width: Option<usize>) {
self.max_width = max_width;
}
Expand Down
2 changes: 2 additions & 0 deletions src/app/theme_picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ impl App {
self.render_width,
&at.markdown,
self.file_mode,
self.code_line_numbers,
);
self.store_theme_preview(preset, &parsed.lines, &parsed.toc);
self.replace_content(parsed);
Expand All @@ -153,6 +154,7 @@ impl App {
self.render_width,
&at.markdown,
self.file_mode,
self.code_line_numbers,
);
self.replace_content(parsed);
}
Expand Down
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub(crate) struct LeafConfig {
pub(crate) watch: Option<bool>,
pub(crate) width: Option<usize>,
pub(crate) extras: Vec<String>,
#[serde(rename = "code-line-numbers")]
pub(crate) code_line_numbers: Option<bool>,
pub(crate) themes: BTreeMap<String, CustomThemeConfig>,
#[serde(skip)]
pub(crate) config_dir: Option<PathBuf>,
Expand Down
33 changes: 30 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ fn resolve_configured_width(
config_width.map(|w| w.max(20))
}

fn resolve_code_line_numbers(config_value: Option<bool>) -> bool {
if let Ok(val) = std::env::var("LEAF_CODE_LINE_NUMBERS") {
match val.as_str() {
"1" => return true,
"0" => return false,
_ => {}
}
}
config_value.unwrap_or(true)
}

fn append_config_warning(warning: &mut Option<String>, next: Option<String>) {
let Some(next) = next else {
return;
Expand Down Expand Up @@ -176,6 +187,7 @@ fn main() -> Result<()> {

let watch_from_config = user_config.watch.unwrap_or(false);
let max_width = resolve_configured_width(cli_width, user_config.width);
let code_line_numbers = resolve_code_line_numbers(user_config.code_line_numbers);

if let Some(ref mut spec) = inline_spec {
if spec.width.is_none() {
Expand Down Expand Up @@ -293,8 +305,15 @@ fn main() -> Result<()> {
let format = inline::resolve_format(spec, is_tty);

let at = app_theme();
let mut parsed =
parse_markdown_with_width(&src, &ss, &theme, width, &at.markdown, file_mode);
let mut parsed = parse_markdown_with_width(
&src,
&ss,
&theme,
width,
&at.markdown,
file_mode,
code_line_numbers,
);

while parsed.lines.last().is_some_and(|l| {
l.spans.is_empty() || l.spans.iter().all(|s| s.content.trim().is_empty())
Expand All @@ -310,7 +329,14 @@ fn main() -> Result<()> {
}

let at = app_theme();
let parsed = parse_markdown(&src, &ss, &theme, &at.markdown, file_mode);
let parsed = parse_markdown(
&src,
&ss,
&theme,
&at.markdown,
file_mode,
code_line_numbers,
);
let crate::markdown::ParseResult {
lines,
toc,
Expand Down Expand Up @@ -338,6 +364,7 @@ fn main() -> Result<()> {
app.set_extras(user_config.extras);
app.set_file_mode(file_mode);
app.set_editor_config(Some(resolved_editor));
app.set_code_line_numbers(code_line_numbers);
app.set_config_warning(config_warning);
if let Some(dir) = dir_arg {
app.set_dir_arg(dir);
Expand Down
97 changes: 60 additions & 37 deletions src/markdown/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ pub(super) fn push_heading_lines(
}
}

pub(super) const CODE_BLOCK_GUTTER: &str = "│";
pub(super) const CODE_BLOCK_GUTTER_WRAP: &str = "┊";

pub(super) struct CodeBlockRenderContext<'a> {
pub(super) ss: &'a SyntaxSet,
pub(super) theme: &'a Theme,
Expand All @@ -157,6 +160,7 @@ pub(super) struct CodeBlockRenderContext<'a> {
pub(super) blockquote_depth: usize,
pub(super) list_stack: &'a [ListKind],
pub(super) file_mode: bool,
pub(super) code_line_numbers: bool,
}

pub(super) fn push_code_block_lines(
Expand Down Expand Up @@ -188,6 +192,7 @@ pub(super) fn push_code_block_lines(
} else {
code_lang.clone()
};
let show_line_numbers = ctx.code_line_numbers;
let available_width = ctx.render_width.saturating_sub(prefix_width);
let (code_lines, inner_width, digit_width) = highlight_code(
code_buf,
Expand All @@ -196,8 +201,13 @@ pub(super) fn push_code_block_lines(
ctx.theme,
available_width,
ctx.file_mode,
show_line_numbers,
);
let gutter_width = digit_width + 2;
let gutter_width = if show_line_numbers {
digit_width + 2
} else {
1
};
let gutter_style = Style::default().fg(ctx.theme_colors.code_gutter);
let content_width = inner_width.saturating_sub(gutter_width + 1);

Expand All @@ -220,16 +230,25 @@ pub(super) fn push_code_block_lines(
]);
lines.push(Line::from(header));

let plain_first_gutter = Span::styled(CODE_BLOCK_GUTTER, gutter_style);
let plain_cont_gutter = Span::styled(CODE_BLOCK_GUTTER_WRAP, gutter_style);

for (i, code_line) in code_lines.into_iter().enumerate() {
let line_num = i + 1;
let num_gutter = Span::styled(format!("│{:>w$}│", line_num, w = digit_width), gutter_style);
let blank_gutter = Span::styled(format!("│{:>w$}│", "", w = digit_width), gutter_style);
let (first_gutter, cont_gutter) = if show_line_numbers {
let line_num = i + 1;
(
Span::styled(format!("│{:>w$}│", line_num, w = digit_width), gutter_style),
Span::styled(format!("│{:>w$}│", "", w = digit_width), gutter_style),
)
} else {
(plain_first_gutter.clone(), plain_cont_gutter.clone())
};

let mut first_prefix = prefix.clone();
first_prefix.push(num_gutter);
first_prefix.push(first_gutter);

let mut cont_prefix = prefix.clone();
cont_prefix.push(blank_gutter);
cont_prefix.push(cont_gutter);

push_wrapped_code_lines(
lines,
Expand All @@ -242,12 +261,17 @@ pub(super) fn push_code_block_lines(
}

let mut footer = prefix;
footer.push(Span::styled(
let footer_text = if show_line_numbers {
format!(
"└{}┴{}┘",
"─".repeat(gutter_width - 2),
"─".repeat(inner_width.saturating_sub(gutter_width - 1))
),
)
} else {
format!("└{}┘", "─".repeat(inner_width))
};
footer.push(Span::styled(
footer_text,
Style::default().fg(ctx.theme_colors.code_frame),
));
lines.push(Line::from(footer));
Expand Down Expand Up @@ -332,11 +356,8 @@ pub(super) fn push_special_block_lines<F: Fn(&str) -> Vec<Span<'static>>>(
} else {
String::new()
};
let border_only = if !show_line_numbers {
Some(Span::styled("│".to_string(), gutter_style))
} else {
None
};
let plain_first_gutter = Span::styled(CODE_BLOCK_GUTTER, gutter_style);
let plain_cont_gutter = Span::styled(CODE_BLOCK_GUTTER_WRAP, gutter_style);

for (i, content_line) in content_lines.iter().enumerate() {
let mut content_spans = (ctx.make_spans)(content_line);
Expand All @@ -346,9 +367,9 @@ pub(super) fn push_special_block_lines<F: Fn(&str) -> Vec<Span<'static>>>(

let mut first_prefix = prefix.clone();
let mut cont_prefix = prefix.clone();
if let Some(ref border) = border_only {
first_prefix.push(border.clone());
cont_prefix.push(border.clone());
if !show_line_numbers {
first_prefix.push(plain_first_gutter.clone());
cont_prefix.push(plain_cont_gutter.clone());
} else {
let line_num = i + 1;
first_prefix.push(Span::styled(
Expand Down Expand Up @@ -391,13 +412,18 @@ pub(super) fn push_special_block_lines<F: Fn(&str) -> Vec<Span<'static>>>(
lines.push(Line::from(""));
}

pub(super) struct EmbeddedBlockCtx<'a> {
pub(super) render_width: usize,
pub(super) theme: &'a MarkdownTheme,
pub(super) blockquote_depth: usize,
pub(super) list_stack: &'a [ListKind],
pub(super) code_line_numbers: bool,
}

pub(super) fn push_latex_block_lines(
lines: &mut Vec<Line<'static>>,
content: &str,
render_width: usize,
theme: &MarkdownTheme,
blockquote_depth: usize,
list_stack: &[ListKind],
ctx: EmbeddedBlockCtx<'_>,
item_stack: &mut [ItemState],
) {
let rendered = latex::to_unicode(content);
Expand All @@ -410,18 +436,18 @@ pub(super) fn push_latex_block_lines(
.iter()
.rposition(|l| !l.trim().is_empty())
.map_or(start, |e| e + 1);
let content_style = Style::default().fg(theme.latex_block_fg);
let content_style = Style::default().fg(ctx.theme.latex_block_fg);
push_special_block_lines(
lines,
render_width,
theme,
blockquote_depth,
list_stack,
ctx.render_width,
ctx.theme,
ctx.blockquote_depth,
ctx.list_stack,
item_stack,
SpecialBlockCtx {
label: "latex",
content_lines: &all_lines[start..end],
show_line_numbers: true,
show_line_numbers: ctx.code_line_numbers,
center: false,
make_spans: |line| vec![Span::styled(line.to_string(), content_style)],
},
Expand All @@ -431,10 +457,7 @@ pub(super) fn push_latex_block_lines(
pub(super) fn push_mermaid_block_lines(
lines: &mut Vec<Line<'static>>,
content: &str,
render_width: usize,
theme: &MarkdownTheme,
blockquote_depth: usize,
list_stack: &[ListKind],
ctx: EmbeddedBlockCtx<'_>,
item_stack: &mut [ItemState],
) {
let rendered = mermaid::render(content);
Expand All @@ -444,24 +467,24 @@ pub(super) fn push_mermaid_block_lines(
} else {
content.lines().collect()
};
let content_style = Style::default().fg(theme.mermaid_block_fg);
let content_style = Style::default().fg(ctx.theme.mermaid_block_fg);
push_special_block_lines(
lines,
render_width,
theme,
blockquote_depth,
list_stack,
ctx.render_width,
ctx.theme,
ctx.blockquote_depth,
ctx.list_stack,
item_stack,
SpecialBlockCtx {
label: "mermaid",
content_lines: &content_lines,
show_line_numbers: !use_rendered,
show_line_numbers: !use_rendered && ctx.code_line_numbers,
center: use_rendered,
make_spans: |line| {
if use_rendered {
vec![Span::styled(line.to_string(), content_style)]
} else {
mermaid::colorize_line(line, theme)
mermaid::colorize_line(line, ctx.theme)
}
},
},
Expand Down
Loading
Loading