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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/qmd-syntax-helper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ anyhow = "1.0"
regex = "1.10"
colored = "2.1"
quarto-markdown-pandoc.workspace = true
quarto-error-reporting.workspace = true
include_dir = "0.7"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
Expand Down
65 changes: 15 additions & 50 deletions crates/qmd-syntax-helper/src/conversions/div_whitespace.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,20 @@
use anyhow::{Context, Result};
use colored::Colorize;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

use crate::rule::{CheckResult, ConvertResult, Rule};
use crate::utils::file_io::{read_file, write_file};

#[derive(Debug, Serialize, Deserialize)]
struct ErrorLocation {
row: usize,
column: usize,
byte_offset: usize,
size: usize,
}

#[derive(Debug, Serialize, Deserialize)]
struct ParseError {
filename: String,
title: String,
message: String,
location: ErrorLocation,
}

pub struct DivWhitespaceConverter {}

impl DivWhitespaceConverter {
pub fn new() -> Result<Self> {
Ok(Self {})
}

/// Parse a file and get error locations as JSON
fn get_parse_errors(&self, file_path: &Path) -> Result<Vec<ParseError>> {
/// Parse a file and get diagnostic messages
fn get_parse_errors(&self, file_path: &Path) -> Result<Vec<quarto_error_reporting::DiagnosticMessage>> {
let content = fs::read_to_string(file_path)
.with_context(|| format!("Failed to read file: {}", file_path.display()))?;

Expand All @@ -44,43 +27,19 @@ impl DivWhitespaceConverter {
false, // not loose mode
&filename,
&mut sink,
Some(
quarto_markdown_pandoc::readers::qmd_error_messages::produce_json_error_messages
as fn(
&[u8],
&quarto_markdown_pandoc::utils::tree_sitter_log_observer::TreeSitterLogObserver,
&str,
) -> Vec<String>,
),
);

match result {
Ok(_) => Ok(Vec::new()), // No errors
Err(error_messages) => {
// Parse the JSON error output
// The error messages come as a single JSON array string
if error_messages.is_empty() {
return Ok(Vec::new());
}

let json_str = error_messages.join("");

// Try to parse as JSON array
match serde_json::from_str::<Vec<ParseError>>(&json_str) {
Ok(errors) => Ok(errors),
Err(_) => {
// If parsing fails, the messages are likely plain text warnings/debug messages
// rather than actual syntax errors. These don't indicate div whitespace issues,
// so we can safely ignore them for this specific rule.
Ok(Vec::new())
}
}
Err(diagnostics) => {
// Return diagnostic messages directly
Ok(diagnostics)
}
}
}

/// Find div fence errors that need whitespace fixes
fn find_div_whitespace_errors(&self, content: &str, errors: &[ParseError]) -> Vec<usize> {
fn find_div_whitespace_errors(&self, content: &str, errors: &[quarto_error_reporting::DiagnosticMessage]) -> Vec<usize> {
let mut fix_positions = Vec::new();
let lines: Vec<&str> = content.lines().collect();

Expand All @@ -93,12 +52,18 @@ impl DivWhitespaceConverter {
continue;
}

// Extract row from location (if available)
// SourceInfo uses 0-indexed rows, div_whitespace uses them too
let error_row = error.location.as_ref()
.map(|loc| loc.range.start.row)
.unwrap_or(0);

// The error might be on the line itself or the line before (for div fences)
// Check both the current line and the previous line
let lines_to_check = if error.location.row > 0 {
vec![error.location.row - 1, error.location.row]
let lines_to_check = if error_row > 0 {
vec![error_row - 1, error_row]
} else {
vec![error.location.row]
vec![error_row]
};

for &line_idx in &lines_to_check {
Expand Down
8 changes: 0 additions & 8 deletions crates/qmd-syntax-helper/src/diagnostics/parse_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,6 @@ impl ParseChecker {
false,
&filename,
&mut sink,
Some(
quarto_markdown_pandoc::readers::qmd_error_messages::produce_json_error_messages
as fn(
&[u8],
&quarto_markdown_pandoc::utils::tree_sitter_log_observer::TreeSitterLogObserver,
&str,
) -> Vec<String>,
),
);

Ok(result.is_ok())
Expand Down
59 changes: 59 additions & 0 deletions crates/quarto-error-reporting/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,35 @@ impl DiagnosticMessageBuilder {
self.details.push(DetailItem {
kind: DetailKind::Error,
content: detail.into(),
location: None,
});
self
}

/// Add an error detail with a source location.
///
/// This allows adding contextual information that points to specific locations
/// in the source code, creating rich multi-location error messages.
///
/// # Example
///
/// ```ignore
/// use quarto_error_reporting::DiagnosticMessageBuilder;
///
/// let error = DiagnosticMessageBuilder::error("Mismatched brackets")
/// .add_detail_at("Opening bracket here", opening_location)
/// .add_detail_at("But no closing bracket found", end_location)
/// .build();
/// ```
pub fn add_detail_at(
mut self,
detail: impl Into<MessageContent>,
location: quarto_source_map::SourceInfo,
) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Error,
content: detail.into(),
location: Some(location),
});
self
}
Expand All @@ -278,6 +307,21 @@ impl DiagnosticMessageBuilder {
self.details.push(DetailItem {
kind: DetailKind::Info,
content: info.into(),
location: None,
});
self
}

/// Add an info detail with a source location.
pub fn add_info_at(
mut self,
info: impl Into<MessageContent>,
location: quarto_source_map::SourceInfo,
) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Info,
content: info.into(),
location: Some(location),
});
self
}
Expand All @@ -297,6 +341,21 @@ impl DiagnosticMessageBuilder {
self.details.push(DetailItem {
kind: DetailKind::Note,
content: note.into(),
location: None,
});
self
}

/// Add a note detail with a source location.
pub fn add_note_at(
mut self,
note: impl Into<MessageContent>,
location: quarto_source_map::SourceInfo,
) -> Self {
self.details.push(DetailItem {
kind: DetailKind::Note,
content: note.into(),
location: Some(location),
});
self
}
Expand Down
Loading