Write #+RESULTS: blocks after code execution
- Execute returns results and writes them to the org file - Replaces existing #+RESULTS: block if present - Results formatted with : prefix per org convention - Process blocks in reverse order to preserve byte offsets - Blank line added after results for proper separation
This commit is contained in:
parent
17f18b044c
commit
4a865d7546
|
|
@ -1,6 +1,6 @@
|
||||||
//! Server state: manages org documents and their parsed state.
|
//! Server state: manages org documents and their parsed state.
|
||||||
|
|
||||||
use org_ast::{Document, ElementKind, Section, SourceBlock, Timestamp, TimestampValue};
|
use org_ast::{Document, ElementKind, Section, SourceBlock, Span, Timestamp, TimestampValue};
|
||||||
use org_parser::Parser;
|
use org_parser::Parser;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -321,7 +321,7 @@ impl OrkState {
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "document not found"))?;
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "document not found"))?;
|
||||||
|
|
||||||
// Collect blocks to execute based on scope
|
// Collect blocks to execute based on scope
|
||||||
let blocks: Vec<(String, usize, SourceBlock)> = match args.len() {
|
let blocks: Vec<(String, usize, SourceBlock, Span)> = match args.len() {
|
||||||
// execute doc — all blocks in document
|
// execute doc — all blocks in document
|
||||||
1 => self.collect_blocks_doc(&doc_state.doc),
|
1 => self.collect_blocks_doc(&doc_state.doc),
|
||||||
|
|
||||||
|
|
@ -374,23 +374,132 @@ impl OrkState {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute each block
|
// Get document content for modification
|
||||||
let mut results = Vec::new();
|
let mut content = doc_state.content.clone();
|
||||||
for (section_id, idx, block) in &blocks {
|
|
||||||
let result = self.execute_block(block)?;
|
// Execute each block and collect results
|
||||||
results.push(format!(
|
// Process in reverse order so byte offsets remain valid as we insert
|
||||||
"---\nsection: {}\nindex: {}\nlang: {}\nstatus: {}\nstdout: |\n{}\nstderr: |\n{}",
|
let mut results_output = Vec::new();
|
||||||
section_id,
|
let mut blocks_with_results: Vec<_> = blocks.iter().map(|(sid, idx, block, span)| {
|
||||||
idx,
|
let result = self.execute_block(block);
|
||||||
block.language.as_deref().unwrap_or("unknown"),
|
(sid.clone(), *idx, block.clone(), *span, result)
|
||||||
result.status,
|
}).collect();
|
||||||
indent_lines(&result.stdout, " "),
|
|
||||||
indent_lines(&result.stderr, " "),
|
// Sort by span end position descending so we modify from end to start
|
||||||
));
|
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 {
|
||||||
|
match result {
|
||||||
|
Ok(exec_result) => {
|
||||||
|
// Insert results into document
|
||||||
|
content = self.insert_results(&content, *span, &exec_result.stdout);
|
||||||
|
|
||||||
|
results_output.push(format!(
|
||||||
|
"---\nsection: {}\nindex: {}\nlang: {}\nstatus: {}\nstdout: |\n{}\nstderr: |\n{}",
|
||||||
|
section_id,
|
||||||
|
idx,
|
||||||
|
block.language.as_deref().unwrap_or("unknown"),
|
||||||
|
exec_result.status,
|
||||||
|
indent_lines(&exec_result.stdout, " "),
|
||||||
|
indent_lines(&exec_result.stderr, " "),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
results_output.push(format!(
|
||||||
|
"---\nsection: {}\nindex: {}\nlang: {}\nerror: {}",
|
||||||
|
section_id,
|
||||||
|
idx,
|
||||||
|
block.language.as_deref().unwrap_or("unknown"),
|
||||||
|
e,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write updated document back
|
||||||
|
self.write_doc_raw(doc_name, &content)?;
|
||||||
|
|
||||||
Ok(results.join("\n"))
|
// Reverse to get original order
|
||||||
|
results_output.reverse();
|
||||||
|
Ok(results_output.join("\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert #+RESULTS: block after a source block.
|
||||||
|
fn insert_results(&self, content: &str, block_span: Span, stdout: &str) -> String {
|
||||||
|
let end_offset = block_span.end.offset;
|
||||||
|
|
||||||
|
// Find the end of the line after the block (in case span doesn't include newline)
|
||||||
|
let insert_pos = content[end_offset..]
|
||||||
|
.find('\n')
|
||||||
|
.map(|i| end_offset + i + 1)
|
||||||
|
.unwrap_or(content.len());
|
||||||
|
|
||||||
|
// Check if there's already a #+RESULTS: block after this
|
||||||
|
let after_block = &content[insert_pos..];
|
||||||
|
let results_prefix = "#+RESULTS:";
|
||||||
|
|
||||||
|
// Look for #+RESULTS: at the start of the next non-empty line
|
||||||
|
let (replace_start, replace_end) = if let Some(_results_start) = after_block
|
||||||
|
.lines()
|
||||||
|
.next()
|
||||||
|
.filter(|line| line.trim().starts_with(results_prefix))
|
||||||
|
{
|
||||||
|
// Find the extent of the existing results block
|
||||||
|
let abs_start = insert_pos;
|
||||||
|
let mut abs_end = abs_start;
|
||||||
|
|
||||||
|
// Results block continues until we hit a non-result line
|
||||||
|
// (lines starting with : or blank lines after #+RESULTS:)
|
||||||
|
let mut in_results = false;
|
||||||
|
for line in after_block.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.starts_with(results_prefix) {
|
||||||
|
in_results = true;
|
||||||
|
abs_end += line.len() + 1; // +1 for newline
|
||||||
|
} else if in_results && (trimmed.starts_with(':') || trimmed.is_empty()) {
|
||||||
|
abs_end += line.len() + 1;
|
||||||
|
} else if in_results {
|
||||||
|
// End of results block
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
// No results block found
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(abs_start, abs_end.min(content.len()))
|
||||||
|
} else {
|
||||||
|
// No existing results, just insert
|
||||||
|
(insert_pos, insert_pos)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format the new results
|
||||||
|
let formatted_results = format_results(stdout);
|
||||||
|
|
||||||
|
// Build new content
|
||||||
|
let mut new_content = String::with_capacity(content.len() + formatted_results.len());
|
||||||
|
new_content.push_str(&content[..replace_start]);
|
||||||
|
new_content.push_str(&formatted_results);
|
||||||
|
new_content.push_str(&content[replace_end..]);
|
||||||
|
|
||||||
|
new_content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format stdout as org #+RESULTS: block.
|
||||||
|
fn format_results(stdout: &str) -> String {
|
||||||
|
if stdout.is_empty() {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = String::from("#+RESULTS:\n");
|
||||||
|
for line in stdout.lines() {
|
||||||
|
result.push_str(": ");
|
||||||
|
result.push_str(line);
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
// Add blank line after results for separation
|
||||||
|
result.push('\n');
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Indent each line of text with a prefix.
|
/// Indent each line of text with a prefix.
|
||||||
|
|
@ -410,7 +519,8 @@ fn indent_lines(text: &str, prefix: &str) -> String {
|
||||||
|
|
||||||
impl OrkState {
|
impl OrkState {
|
||||||
/// Collect all source blocks from a document.
|
/// Collect all source blocks from a document.
|
||||||
fn collect_blocks_doc(&self, doc: &Document) -> Vec<(String, usize, SourceBlock)> {
|
/// Returns (section_id, element_index, SourceBlock, element_span).
|
||||||
|
fn collect_blocks_doc(&self, doc: &Document) -> Vec<(String, usize, SourceBlock, Span)> {
|
||||||
let mut blocks = Vec::new();
|
let mut blocks = Vec::new();
|
||||||
for section in &doc.sections {
|
for section in &doc.sections {
|
||||||
blocks.extend(self.collect_blocks_subtree(section));
|
blocks.extend(self.collect_blocks_subtree(section));
|
||||||
|
|
@ -419,14 +529,14 @@ impl OrkState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Collect all source blocks from a section and its children (DFS).
|
/// Collect all source blocks from a section and its children (DFS).
|
||||||
fn collect_blocks_subtree(&self, section: &Section) -> Vec<(String, usize, SourceBlock)> {
|
fn collect_blocks_subtree(&self, section: &Section) -> Vec<(String, usize, SourceBlock, Span)> {
|
||||||
let mut blocks = Vec::new();
|
let mut blocks = Vec::new();
|
||||||
let section_id = self.section_id(section);
|
let section_id = self.section_id(section);
|
||||||
|
|
||||||
// Blocks in this section
|
// Blocks in this section
|
||||||
for (idx, elem) in section.content.iter().enumerate() {
|
for (idx, elem) in section.content.iter().enumerate() {
|
||||||
if let ElementKind::SourceBlock(sb) = &elem.kind {
|
if let ElementKind::SourceBlock(sb) = &elem.kind {
|
||||||
blocks.push((section_id.clone(), idx, sb.clone()));
|
blocks.push((section_id.clone(), idx, sb.clone(), elem.span));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -439,7 +549,7 @@ impl OrkState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a named block anywhere in the document.
|
/// Find a named block anywhere in the document.
|
||||||
fn find_named_block_doc(&self, doc: &Document, name: &str) -> Result<Vec<(String, usize, SourceBlock)>> {
|
fn find_named_block_doc(&self, doc: &Document, name: &str) -> Result<Vec<(String, usize, SourceBlock, Span)>> {
|
||||||
for section in &doc.sections {
|
for section in &doc.sections {
|
||||||
if let Some(result) = self.find_named_block_recursive(section, name) {
|
if let Some(result) = self.find_named_block_recursive(section, name) {
|
||||||
return Ok(vec![result]);
|
return Ok(vec![result]);
|
||||||
|
|
@ -452,7 +562,7 @@ impl OrkState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Recursively search for a named block.
|
/// Recursively search for a named block.
|
||||||
fn find_named_block_recursive(&self, section: &Section, name: &str) -> Option<(String, usize, SourceBlock)> {
|
fn find_named_block_recursive(&self, section: &Section, name: &str) -> Option<(String, usize, SourceBlock, Span)> {
|
||||||
let section_id = self.section_id(section);
|
let section_id = self.section_id(section);
|
||||||
|
|
||||||
// Check elements for SourceBlock with #+NAME:
|
// Check elements for SourceBlock with #+NAME:
|
||||||
|
|
@ -469,14 +579,14 @@ impl OrkState {
|
||||||
if aff.key.eq_ignore_ascii_case("NAME") {
|
if aff.key.eq_ignore_ascii_case("NAME") {
|
||||||
if let org_ast::AffiliatedValue::Text(ref val) = aff.value {
|
if let org_ast::AffiliatedValue::Text(ref val) = aff.value {
|
||||||
if val == name {
|
if val == name {
|
||||||
return Some((section_id, idx, sb.clone()));
|
return Some((section_id, idx, sb.clone(), elem.span));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fall back to preceding Keyword element
|
// Fall back to preceding Keyword element
|
||||||
if pending_name == Some(name) {
|
if pending_name == Some(name) {
|
||||||
return Some((section_id, idx, sb.clone()));
|
return Some((section_id, idx, sb.clone(), elem.span));
|
||||||
}
|
}
|
||||||
pending_name = None;
|
pending_name = None;
|
||||||
}
|
}
|
||||||
|
|
@ -497,7 +607,7 @@ impl OrkState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a named block within a section.
|
/// Find a named block within a section.
|
||||||
fn find_named_block_section(&self, section: &Section, section_id: &str, name: &str) -> Result<Vec<(String, usize, SourceBlock)>> {
|
fn find_named_block_section(&self, section: &Section, section_id: &str, name: &str) -> Result<Vec<(String, usize, SourceBlock, Span)>> {
|
||||||
let mut pending_name: Option<&str> = None;
|
let mut pending_name: Option<&str> = None;
|
||||||
for (idx, elem) in section.content.iter().enumerate() {
|
for (idx, elem) in section.content.iter().enumerate() {
|
||||||
match &elem.kind {
|
match &elem.kind {
|
||||||
|
|
@ -510,14 +620,14 @@ impl OrkState {
|
||||||
if aff.key.eq_ignore_ascii_case("NAME") {
|
if aff.key.eq_ignore_ascii_case("NAME") {
|
||||||
if let org_ast::AffiliatedValue::Text(ref val) = aff.value {
|
if let org_ast::AffiliatedValue::Text(ref val) = aff.value {
|
||||||
if val == name {
|
if val == name {
|
||||||
return Ok(vec![(section_id.to_string(), idx, sb.clone())]);
|
return Ok(vec![(section_id.to_string(), idx, sb.clone(), elem.span)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fall back to preceding Keyword element
|
// Fall back to preceding Keyword element
|
||||||
if pending_name == Some(name) {
|
if pending_name == Some(name) {
|
||||||
return Ok(vec![(section_id.to_string(), idx, sb.clone())]);
|
return Ok(vec![(section_id.to_string(), idx, sb.clone(), elem.span)]);
|
||||||
}
|
}
|
||||||
pending_name = None;
|
pending_name = None;
|
||||||
}
|
}
|
||||||
|
|
@ -533,12 +643,12 @@ impl OrkState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a block by index within a section.
|
/// Get a block by index within a section.
|
||||||
fn get_block_by_index(&self, section: &Section, section_id: &str, idx: usize) -> Result<Vec<(String, usize, SourceBlock)>> {
|
fn get_block_by_index(&self, section: &Section, section_id: &str, idx: usize) -> Result<Vec<(String, usize, SourceBlock, Span)>> {
|
||||||
let blocks: Vec<_> = section.content.iter()
|
let blocks: Vec<_> = section.content.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(i, elem)| {
|
.filter_map(|(i, elem)| {
|
||||||
if let ElementKind::SourceBlock(sb) = &elem.kind {
|
if let ElementKind::SourceBlock(sb) = &elem.kind {
|
||||||
Some((i, sb))
|
Some((i, sb, elem.span))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -552,8 +662,8 @@ impl OrkState {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let (elem_idx, sb) = blocks[idx];
|
let (elem_idx, sb, span) = blocks[idx];
|
||||||
Ok(vec![(section_id.to_string(), elem_idx, sb.clone())])
|
Ok(vec![(section_id.to_string(), elem_idx, sb.clone(), span)])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a source block and return the result.
|
/// Execute a source block and return the result.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue