From 4a865d7546bbce9aaf02a5f0fe6e48e1ca37ac37 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Thu, 1 Oct 2026 11:46:16 +0200 Subject: [PATCH] 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 --- crates/ork-server/src/state.rs | 170 +++++++++++++++++++++++++++------ 1 file changed, 140 insertions(+), 30 deletions(-) diff --git a/crates/ork-server/src/state.rs b/crates/ork-server/src/state.rs index 43dc995..b0baea6 100644 --- a/crates/ork-server/src/state.rs +++ b/crates/ork-server/src/state.rs @@ -1,6 +1,6 @@ //! 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 parking_lot::RwLock; use std::collections::HashMap; @@ -321,7 +321,7 @@ impl OrkState { .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "document not found"))?; // 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 1 => self.collect_blocks_doc(&doc_state.doc), @@ -374,23 +374,132 @@ impl OrkState { )); } - // Execute each block - let mut results = Vec::new(); - for (section_id, idx, block) in &blocks { - let result = self.execute_block(block)?; - results.push(format!( - "---\nsection: {}\nindex: {}\nlang: {}\nstatus: {}\nstdout: |\n{}\nstderr: |\n{}", - section_id, - idx, - block.language.as_deref().unwrap_or("unknown"), - result.status, - indent_lines(&result.stdout, " "), - indent_lines(&result.stderr, " "), - )); + // Get document content for modification + let mut content = doc_state.content.clone(); + + // Execute each block and collect results + // Process in reverse order so byte offsets remain valid as we insert + let mut results_output = Vec::new(); + let mut blocks_with_results: Vec<_> = blocks.iter().map(|(sid, idx, block, span)| { + let result = self.execute_block(block); + (sid.clone(), *idx, block.clone(), *span, result) + }).collect(); + + // 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. @@ -410,7 +519,8 @@ fn indent_lines(text: &str, prefix: &str) -> String { impl OrkState { /// 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(); for section in &doc.sections { blocks.extend(self.collect_blocks_subtree(section)); @@ -419,14 +529,14 @@ impl OrkState { } /// 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 section_id = self.section_id(section); // Blocks in this section for (idx, elem) in section.content.iter().enumerate() { 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. - fn find_named_block_doc(&self, doc: &Document, name: &str) -> Result> { + fn find_named_block_doc(&self, doc: &Document, name: &str) -> Result> { for section in &doc.sections { if let Some(result) = self.find_named_block_recursive(section, name) { return Ok(vec![result]); @@ -452,7 +562,7 @@ impl OrkState { } /// 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); // Check elements for SourceBlock with #+NAME: @@ -469,14 +579,14 @@ impl OrkState { if aff.key.eq_ignore_ascii_case("NAME") { if let org_ast::AffiliatedValue::Text(ref val) = aff.value { 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 if pending_name == Some(name) { - return Some((section_id, idx, sb.clone())); + return Some((section_id, idx, sb.clone(), elem.span)); } pending_name = None; } @@ -497,7 +607,7 @@ impl OrkState { } /// Find a named block within a section. - fn find_named_block_section(&self, section: &Section, section_id: &str, name: &str) -> Result> { + fn find_named_block_section(&self, section: &Section, section_id: &str, name: &str) -> Result> { let mut pending_name: Option<&str> = None; for (idx, elem) in section.content.iter().enumerate() { match &elem.kind { @@ -510,14 +620,14 @@ impl OrkState { if aff.key.eq_ignore_ascii_case("NAME") { if let org_ast::AffiliatedValue::Text(ref val) = aff.value { 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 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; } @@ -533,12 +643,12 @@ impl OrkState { } /// Get a block by index within a section. - fn get_block_by_index(&self, section: &Section, section_id: &str, idx: usize) -> Result> { + fn get_block_by_index(&self, section: &Section, section_id: &str, idx: usize) -> Result> { let blocks: Vec<_> = section.content.iter() .enumerate() .filter_map(|(i, elem)| { if let ElementKind::SourceBlock(sb) = &elem.kind { - Some((i, sb)) + Some((i, sb, elem.span)) } else { None } @@ -552,8 +662,8 @@ impl OrkState { )); } - let (elem_idx, sb) = blocks[idx]; - Ok(vec![(section_id.to_string(), elem_idx, sb.clone())]) + let (elem_idx, sb, span) = blocks[idx]; + Ok(vec![(section_id.to_string(), elem_idx, sb.clone(), span)]) } /// Execute a source block and return the result.