Honor :results header args on code execution

Parse the :results switch from a src block's header arguments and apply
Emacs org-babel semantics when writing results:

- silent/none: execute and report, but do not write a #+RESULTS: block
- raw: insert output verbatim, no prefix or wrapping
- drawer: wrap output in a :results: ... 🔚 drawer
- default: fixed-width for <10 lines, #+begin_example for 10+

The insert_results detection loop now recognizes and replaces all four
wrapping styles in place, so re-execution and cross-mode transitions
(e.g. drawer to fixed-width) update cleanly without duplication. Raw
results, which have no delimiters, terminate at a blank line or heading.
This commit is contained in:
Levi Neely 2026-10-01 13:48:13 +02:00
parent a13f76cd40
commit d073c8fa5e
1 changed files with 201 additions and 41 deletions

View File

@ -416,10 +416,13 @@ impl OrkState {
blocks_with_results.sort_by(|a, b| b.3.end.offset.cmp(&a.3.end.offset)); blocks_with_results.sort_by(|a, b| b.3.end.offset.cmp(&a.3.end.offset));
for (section_id, idx, block, span, result) in &blocks_with_results { for (section_id, idx, block, span, result) in &blocks_with_results {
let params = parse_results_params(block.arguments.as_deref());
match result { match result {
Ok(exec_result) => { Ok(exec_result) => {
// Insert results into document // Insert results into document unless the block is silent.
content = self.insert_results(&content, *span, &exec_result.stdout); if !params.silent {
content = self.insert_results(&content, *span, &exec_result.stdout, params.wrap);
}
results_output.push(format!( results_output.push(format!(
"---\nsection: {}\nindex: {}\nlang: {}\nstatus: {}\nstdout: |\n{}\nstderr: |\n{}", "---\nsection: {}\nindex: {}\nlang: {}\nstatus: {}\nstdout: |\n{}\nstderr: |\n{}",
@ -452,7 +455,7 @@ impl OrkState {
} }
/// Insert #+RESULTS: block after a source block. /// Insert #+RESULTS: block after a source block.
fn insert_results(&self, content: &str, block_span: Span, stdout: &str) -> String { fn insert_results(&self, content: &str, block_span: Span, stdout: &str, wrap: ResultWrap) -> String {
let end_offset = block_span.end.offset.min(content.len()); let end_offset = block_span.end.offset.min(content.len());
// The block span end already points to the start of the line following // The block span end already points to the start of the line following
@ -487,34 +490,56 @@ impl OrkState {
let abs_start = insert_pos; let abs_start = insert_pos;
let mut abs_end = abs_start; let mut abs_end = abs_start;
// Track state: are we in results, and are we in an example block? // Track state across the possible wrapping styles:
// - fixed-width (`: ` prefixed lines)
// - #+begin_example ... #+end_example
// - :results: ... :end: drawer
// - raw (bare contiguous lines terminated by a blank line)
let mut in_results = false; let mut in_results = false;
let mut in_example_block = false; let mut in_example_block = false;
let mut in_drawer = false;
for line in after_block.lines() { for line in after_block.lines() {
let trimmed = line.trim(); let trimmed = line.trim();
let upper = trimmed.to_uppercase(); let upper = trimmed.to_uppercase();
if !in_results {
if upper.starts_with(results_prefix) { if upper.starts_with(results_prefix) {
in_results = true; in_results = true;
abs_end += line.len() + 1; // +1 for newline abs_end += line.len() + 1; // +1 for newline
} else if in_results && upper.starts_with("#+BEGIN_EXAMPLE") { continue;
in_example_block = true; }
abs_end += line.len() + 1; // Haven't reached the results keyword yet; nothing to replace.
} else if in_example_block { break;
}
// We are inside the results block.
if in_example_block {
abs_end += line.len() + 1; abs_end += line.len() + 1;
if upper.starts_with("#+END_EXAMPLE") { if upper.starts_with("#+END_EXAMPLE") {
in_example_block = false; in_example_block = false;
} }
} else if in_results && (trimmed.starts_with(':') || trimmed.is_empty()) { } else if in_drawer {
// Fixed-width format or blank line continuation
abs_end += line.len() + 1; abs_end += line.len() + 1;
} else if in_results { if upper == ":END:" {
// End of results block in_drawer = false;
}
} else if upper.starts_with("#+BEGIN_EXAMPLE") {
in_example_block = true;
abs_end += line.len() + 1;
} else if upper == ":RESULTS:" {
in_drawer = true;
abs_end += line.len() + 1;
} else if trimmed.is_empty() {
// A blank line terminates the results block (consume it).
abs_end += line.len() + 1;
break;
} else if trimmed.starts_with('*') {
// A heading starts a new element; stop without consuming it.
break; break;
} else { } else {
// No results block found // Content line: fixed-width (`: ...`) or raw. Consume it.
break; abs_end += line.len() + 1;
} }
} }
(abs_start, abs_end.min(content.len())) (abs_start, abs_end.min(content.len()))
@ -524,7 +549,7 @@ impl OrkState {
}; };
// Format the new results // Format the new results
let formatted_results = format_results(stdout); let formatted_results = format_results(stdout, wrap);
// Build new content // Build new content
let mut new_content = String::with_capacity(content.len() + formatted_results.len()); let mut new_content = String::with_capacity(content.len() + formatted_results.len());
@ -540,16 +565,92 @@ impl OrkState {
/// Matches Emacs `org-babel-min-lines-for-block-output` default. /// Matches Emacs `org-babel-min-lines-for-block-output` default.
const MIN_LINES_FOR_BLOCK_OUTPUT: usize = 10; const MIN_LINES_FOR_BLOCK_OUTPUT: usize = 10;
/// Format stdout as org #+RESULTS: block. /// How to wrap the output under the `#+RESULTS:` keyword.
/// Uses fixed-width (`: ` prefix) for <10 lines, #+begin_example block for ≥10. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
fn format_results(stdout: &str) -> String { enum ResultWrap {
/// Fixed-width (`: ` prefix) for small output, #+begin_example for large.
Default,
/// Insert output verbatim, no prefix or wrapping.
Raw,
/// Wrap output in a `:results:` ... `:end:` drawer.
Drawer,
}
/// Parsed `:results` header-argument options relevant to insertion.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ResultsParams {
/// `silent`/`none` — execute but do not write a results block.
silent: bool,
/// How to wrap the inserted output.
wrap: ResultWrap,
}
impl Default for ResultsParams {
fn default() -> Self {
ResultsParams { silent: false, wrap: ResultWrap::Default }
}
}
/// Parse the `:results` switch from a source block's raw header arguments.
///
/// `arguments` is the full header string, e.g. `"python :results output drawer"`.
/// Only the tokens following `:results` (up to the next `:switch` or end) are
/// interpreted. Unknown keywords (table, html, value, ...) are ignored, which
/// leaves the default behavior intact.
fn parse_results_params(arguments: Option<&str>) -> ResultsParams {
let mut params = ResultsParams::default();
let Some(args) = arguments else { return params };
let tokens: Vec<&str> = args.split_whitespace().collect();
// Find the ":results" switch (case-insensitive).
let Some(start) = tokens.iter().position(|t| t.eq_ignore_ascii_case(":results")) else {
return params;
};
// Collect values until the next switch (token starting with ':').
for tok in &tokens[start + 1..] {
if tok.starts_with(':') {
break;
}
match tok.to_ascii_lowercase().as_str() {
"silent" | "none" => params.silent = true,
"raw" => params.wrap = ResultWrap::Raw,
"drawer" => params.wrap = ResultWrap::Drawer,
// output/value and other type keywords don't change insertion here.
_ => {}
}
}
params
}
/// Format stdout as org #+RESULTS: block, honoring the wrap mode.
/// Default: fixed-width (`: ` prefix) for <10 lines, #+begin_example for ≥10.
fn format_results(stdout: &str, wrap: ResultWrap) -> String {
if stdout.is_empty() { if stdout.is_empty() {
return String::new(); return String::new();
} }
let line_count = stdout.lines().count();
let mut result = String::from("#+RESULTS:\n"); let mut result = String::from("#+RESULTS:\n");
match wrap {
ResultWrap::Raw => {
// Verbatim: no prefix, no wrapping.
result.push_str(stdout);
if !stdout.ends_with('\n') {
result.push('\n');
}
}
ResultWrap::Drawer => {
result.push_str(":results:\n");
result.push_str(stdout);
if !stdout.ends_with('\n') {
result.push('\n');
}
result.push_str(":end:\n");
}
ResultWrap::Default => {
let line_count = stdout.lines().count();
if line_count < MIN_LINES_FOR_BLOCK_OUTPUT { if line_count < MIN_LINES_FOR_BLOCK_OUTPUT {
// Fixed-width format: prefix each line with ": " // Fixed-width format: prefix each line with ": "
for line in stdout.lines() { for line in stdout.lines() {
@ -566,6 +667,8 @@ fn format_results(stdout: &str) -> String {
} }
result.push_str("#+end_example\n"); result.push_str("#+end_example\n");
} }
}
}
// Add blank line after results for separation // Add blank line after results for separation
result.push('\n'); result.push('\n');
@ -2037,14 +2140,14 @@ mod tests {
#[test] #[test]
fn test_format_results_empty() { fn test_format_results_empty() {
assert_eq!(format_results(""), ""); assert_eq!(format_results("", ResultWrap::Default), "");
} }
#[test] #[test]
fn test_format_results_short() { fn test_format_results_short() {
// < 10 lines uses fixed-width format (: prefix) // < 10 lines uses fixed-width format (: prefix)
let stdout = "line1\nline2\nline3\n"; let stdout = "line1\nline2\nline3\n";
let result = format_results(stdout); let result = format_results(stdout, ResultWrap::Default);
assert!(result.starts_with("#+RESULTS:\n")); assert!(result.starts_with("#+RESULTS:\n"));
assert!(result.contains(": line1\n")); assert!(result.contains(": line1\n"));
assert!(result.contains(": line2\n")); assert!(result.contains(": line2\n"));
@ -2057,7 +2160,7 @@ mod tests {
// >= 10 lines uses #+begin_example block // >= 10 lines uses #+begin_example block
let lines: Vec<_> = (1..=15).map(|i| format!("line{}", i)).collect(); let lines: Vec<_> = (1..=15).map(|i| format!("line{}", i)).collect();
let stdout = lines.join("\n"); let stdout = lines.join("\n");
let result = format_results(&stdout); let result = format_results(&stdout, ResultWrap::Default);
assert!(result.starts_with("#+RESULTS:\n")); assert!(result.starts_with("#+RESULTS:\n"));
assert!(result.contains("#+begin_example\n")); assert!(result.contains("#+begin_example\n"));
assert!(result.contains("#+end_example\n")); assert!(result.contains("#+end_example\n"));
@ -2066,4 +2169,61 @@ mod tests {
// Should NOT have : prefix in example block // Should NOT have : prefix in example block
assert!(!result.contains(": line1")); assert!(!result.contains(": line1"));
} }
#[test]
fn test_format_results_raw() {
// raw: verbatim, no prefix, no wrapping
let stdout = "a\nb\nc\n";
let result = format_results(stdout, ResultWrap::Raw);
assert_eq!(result, "#+RESULTS:\na\nb\nc\n\n");
}
#[test]
fn test_format_results_drawer() {
// drawer: :results: ... :end: wrapper
let stdout = "x\ny\n";
let result = format_results(stdout, ResultWrap::Drawer);
assert_eq!(result, "#+RESULTS:\n:results:\nx\ny\n:end:\n\n");
}
#[test]
fn test_parse_results_params() {
// No arguments → default
let p = parse_results_params(None);
assert_eq!(p, ResultsParams { silent: false, wrap: ResultWrap::Default });
// Language only, no :results → default
let p = parse_results_params(Some("python"));
assert_eq!(p.wrap, ResultWrap::Default);
assert!(!p.silent);
// :results output → default wrap (output doesn't change insertion)
let p = parse_results_params(Some("python :results output"));
assert_eq!(p.wrap, ResultWrap::Default);
assert!(!p.silent);
// :results raw
let p = parse_results_params(Some("bash :results raw"));
assert_eq!(p.wrap, ResultWrap::Raw);
// :results output drawer
let p = parse_results_params(Some("sh :results output drawer"));
assert_eq!(p.wrap, ResultWrap::Drawer);
// :results silent
let p = parse_results_params(Some("python :results silent"));
assert!(p.silent);
// none is an alias for silent
let p = parse_results_params(Some("python :results none"));
assert!(p.silent);
// Values stop at the next switch
let p = parse_results_params(Some("python :results raw :exports both"));
assert_eq!(p.wrap, ResultWrap::Raw);
// Case-insensitive switch
let p = parse_results_params(Some("python :RESULTS drawer"));
assert_eq!(p.wrap, ResultWrap::Drawer);
}
} }