use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use harn_vm::VmValue;
use streaming_iterator::StreamingIterator;
use tree_sitter::{Node, Query, QueryCursor, QueryError, QueryErrorKind, Tree};
use crate::error::HostlibError;
use crate::tools::args::{build_dict, str_value};
use super::health::{ParserHealth, ParserOperation};
use super::language::{Language, TEXT_PATCH_FALLBACK};
use super::parse::parse_bytes;
#[derive(Debug, Clone)]
pub(super) struct Span {
pub(super) start_byte: usize,
pub(super) end_byte: usize,
pub(super) start_row: usize,
pub(super) start_col: usize,
pub(super) end_row: usize,
pub(super) end_col: usize,
pub(super) original: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Selector {
Unique,
First,
All,
Nth(usize),
}
impl Selector {
pub(super) fn from_request(
builtin: &'static str,
dict: &harn_vm::value::DictMap,
) -> Result<Self, HostlibError> {
let raw = crate::tools::args::optional_string(builtin, dict, "select")?;
let nth = match dict.get("nth") {
None | Some(VmValue::Nil) => None,
Some(VmValue::Int(n)) => Some(*n),
Some(other) => {
return Err(HostlibError::InvalidParameter {
builtin,
param: "nth",
message: format!("expected integer, got {}", other.type_name()),
});
}
};
Self::parse(builtin, raw.as_deref(), nth)
}
pub(super) fn parse(
builtin: &'static str,
raw: Option<&str>,
nth: Option<i64>,
) -> Result<Self, HostlibError> {
match raw.unwrap_or("unique") {
"unique" => Ok(Self::Unique),
"first" => Ok(Self::First),
"all" => Ok(Self::All),
"nth" => {
let n = nth.ok_or(HostlibError::InvalidParameter {
builtin,
param: "nth",
message: "`select: \"nth\"` requires a positive `nth` (1-based)".into(),
})?;
if n < 1 {
return Err(HostlibError::InvalidParameter {
builtin,
param: "nth",
message: format!("`nth` must be >= 1, got {n}"),
});
}
Ok(Self::Nth(n as usize))
}
other => Err(HostlibError::InvalidParameter {
builtin,
param: "select",
message: format!(
"expected one of [\"unique\", \"first\", \"all\", \"nth\"], got `{other}`"
),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum SelectFailure {
Ambiguous,
NthOutOfRange { requested: usize },
}
pub(super) fn select_spans(spans: &[Span], selector: Selector) -> Result<Vec<Span>, SelectFailure> {
match selector {
Selector::Unique => {
if spans.len() > 1 {
Err(SelectFailure::Ambiguous)
} else {
Ok(spans.to_vec())
}
}
Selector::First => Ok(spans.first().cloned().into_iter().collect()),
Selector::All => Ok(spans.to_vec()),
Selector::Nth(n) => match spans.get(n - 1) {
Some(span) => Ok(vec![span.clone()]),
None => Err(SelectFailure::NthOutOfRange { requested: n }),
},
}
}
pub(super) fn collect_target_spans(
query: &Query,
tree: &Tree,
source_bytes: &[u8],
target_index: u32,
) -> Vec<Span> {
let mut cursor = QueryCursor::new();
let mut matches = cursor.matches(query, tree.root_node(), source_bytes);
let mut seen: BTreeMap<(usize, usize), Span> = BTreeMap::new();
while let Some(m) = matches.next() {
for capture in m.captures() {
if capture.index != target_index {
continue;
}
insert_span(&mut seen, capture.node, source_bytes);
}
}
let mut spans: Vec<Span> = seen.into_values().collect();
spans.sort_by_key(|s| s.start_byte);
spans
}
fn insert_span(into: &mut BTreeMap<(usize, usize), Span>, node: Node<'_>, source_bytes: &[u8]) {
let key = (node.start_byte(), node.end_byte());
into.entry(key).or_insert_with(|| {
let start = node.start_position();
let end = node.end_position();
let original = std::str::from_utf8(&source_bytes[node.start_byte()..node.end_byte()])
.unwrap_or_default()
.to_string();
Span {
start_byte: node.start_byte(),
end_byte: node.end_byte(),
start_row: start.row,
start_col: start.column,
end_row: end.row,
end_col: end.column,
original,
}
});
}
pub(super) fn splice(source: &[u8], chosen: &[Span], replacement: &str) -> Vec<u8> {
let mut by_start: Vec<&Span> = chosen.iter().collect();
by_start.sort_by_key(|s| std::cmp::Reverse(s.start_byte));
let mut out = source.to_vec();
for span in by_start {
let end = span.end_byte.min(out.len());
let start = span.start_byte.min(end);
out.splice(start..end, replacement.bytes());
}
out
}
pub(super) fn resolve_target_capture(query: &Query, requested: &str) -> Result<u32, String> {
let names = query.capture_names();
if let Some(idx) = names.iter().position(|n| *n == requested) {
return Ok(idx as u32);
}
if names.len() == 1 {
return Ok(0);
}
Err(format!(
"query has no capture named `{requested}`; available captures: [{}]",
names
.iter()
.map(|n| format!("@{n}"))
.collect::<Vec<_>>()
.join(", ")
))
}
pub(super) fn query_error_kind_str(kind: &QueryErrorKind) -> &'static str {
match kind {
QueryErrorKind::Syntax => "syntax",
QueryErrorKind::NodeType => "node_type",
QueryErrorKind::Field => "field",
QueryErrorKind::Capture => "capture",
QueryErrorKind::Predicate => "predicate",
QueryErrorKind::Structure => "structure",
QueryErrorKind::Language => "language",
}
}
pub(super) fn format_query_error(err: &QueryError) -> String {
format!(
"tree-sitter rejected query at row {} col {}: {} ({})",
err.row,
err.column,
err.message,
query_error_kind_str(&err.kind),
)
}
pub(super) fn first_syntax_error(source: &[u8], language: Language) -> Option<String> {
let tree = parse_bytes(source, language).ok()?;
let root = tree.root_node();
if !root.has_error() {
return None;
}
let mut stack = vec![root];
while let Some(node) = stack.pop() {
if node.is_missing() {
let pos = node.start_position();
return Some(format!(
"missing `{}` at line {}, column {}",
node.kind(),
pos.row + 1,
pos.column + 1
));
}
if node.is_error() {
let pos = node.start_position();
let snippet = node_text(node, source);
let trimmed: String = snippet.chars().take(40).collect();
return Some(format!(
"unexpected `{trimmed}` at line {}, column {}",
pos.row + 1,
pos.column + 1
));
}
for i in (0..node.child_count()).rev() {
if let Some(child) = node.child(i) {
if child.has_error() || child.is_missing() {
stack.push(child);
}
}
}
}
Some("post-edit source has parse errors".into())
}
pub(super) fn node_text(node: Node<'_>, source: &[u8]) -> String {
let start = node.start_byte().min(source.len());
let end = node.end_byte().min(source.len());
if start >= end {
return String::new();
}
std::str::from_utf8(&source[start..end])
.map(|s| s.to_string())
.unwrap_or_default()
}
pub(super) fn sha256_hex(bytes: &[u8]) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
pub(super) fn read_source(
builtin: &'static str,
path: &Path,
session_id: Option<&str>,
max_bytes: usize,
) -> Result<Vec<u8>, HostlibError> {
let bytes = if let Some(result) = crate::fs::read(path, session_id) {
result.map_err(|err| HostlibError::Backend {
builtin,
message: format!("read `{}`: {err}", path.display()),
})?
} else {
std::fs::read(path).map_err(|err| HostlibError::Backend {
builtin,
message: format!("read `{}`: {err}", path.display()),
})?
};
if max_bytes != 0 && bytes.len() > max_bytes {
Ok(bytes[..max_bytes].to_vec())
} else {
Ok(bytes)
}
}
pub(super) fn lossy_str(source: &[u8]) -> String {
String::from_utf8_lossy(source).into_owned()
}
pub(super) fn write_source(
builtin: &'static str,
path: &Path,
contents: &[u8],
session_id: Option<&str>,
) -> Result<(), HostlibError> {
if crate::fs::stage_write_or_none(builtin, path, contents, true, true, session_id)?.is_some() {
return Ok(());
}
crate::fs_snapshot::auto_capture_for_write(builtin, path);
let owned = PathBuf::from(path);
if let Some(parent) = owned.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(|err| HostlibError::Backend {
builtin,
message: format!("mkdir `{}`: {err}", parent.display()),
})?;
}
}
std::fs::write(path, contents).map_err(|err| HostlibError::Backend {
builtin,
message: format!("write `{}`: {err}", path.display()),
})?;
crate::fs_snapshot::record_completed_write(path);
Ok(())
}
pub(super) struct PreparedEdit {
pub(super) language: Language,
pub(super) source: Vec<u8>,
pub(super) query: Query,
pub(super) target_index: u32,
pub(super) tree: Tree,
pub(super) source_health: ParserHealth,
}
pub(super) enum PrepareFailure {
UnknownLanguage,
GrammarUnavailable(Language),
Read(Language, HostlibError),
InvalidQuery(Language, QueryError),
NoTargetCapture(Language, String),
Parse(Language, String),
NotVerified(ParserHealth),
}
pub(super) const SOURCE_NOT_VERIFIED: &str =
"source structure is not verified; refusing structural mutation";
pub(super) const UNVALIDATED_WRITE: &str = "`validate: false` is allowed only for dry-run previews";
impl PrepareFailure {
pub(super) fn health(&self) -> ParserHealth {
match self {
Self::UnknownLanguage => ParserHealth::unavailable(None, ParserOperation::SafeEdit),
Self::GrammarUnavailable(language)
| Self::Read(language, _)
| Self::Parse(language, _) => {
ParserHealth::unavailable(Some(*language), ParserOperation::SafeEdit)
}
Self::InvalidQuery(language, _) | Self::NoTargetCapture(language, _) => {
ParserHealth::not_observed(*language, ParserOperation::SafeEdit)
}
Self::NotVerified(health) => health.clone(),
}
}
pub(super) fn into_response(
self,
builtin: &'static str,
path: &str,
language_hint: Option<&str>,
query: &str,
target_capture: &str,
) -> Result<VmValue, HostlibError> {
let health = self.health();
let response = match self {
Self::UnknownLanguage | Self::GrammarUnavailable(_) => {
unsupported_language_response(path, language_hint)
}
Self::Read(_, err) => return Err(err),
Self::Parse(_, message) => return Err(HostlibError::Backend { builtin, message }),
Self::InvalidQuery(_, err) => invalid_query_response(query, &err),
Self::NoTargetCapture(_, detail) => {
no_match_response(path, query, target_capture, &detail)
}
Self::NotVerified(_) => non_verified_response(path, SOURCE_NOT_VERIFIED),
};
Ok(health.attach_to(response))
}
}
pub(super) fn prepare_edit(
builtin: &'static str,
path: &Path,
language_hint: Option<&str>,
query_text: &str,
target_capture: &str,
session_id: Option<&str>,
max_bytes: usize,
) -> Result<PreparedEdit, PrepareFailure> {
let language = Language::detect(path, language_hint).ok_or(PrepareFailure::UnknownLanguage)?;
let ts_language = language
.ts_language()
.ok_or(PrepareFailure::GrammarUnavailable(language))?;
let source = read_source(builtin, path, session_id, max_bytes)
.map_err(|err| PrepareFailure::Read(language, err))?;
let query = Query::new(&ts_language, query_text)
.map_err(|err| PrepareFailure::InvalidQuery(language, err))?;
let target_index = resolve_target_capture(&query, target_capture)
.map_err(|detail| PrepareFailure::NoTargetCapture(language, detail))?;
let tree = parse_bytes(&source, language)
.map_err(|err| PrepareFailure::Parse(language, err.to_string()))?;
let source_health = ParserHealth::observed(
language,
ParserOperation::SafeEdit,
tree.root_node().has_error(),
);
if !source_health.is_verified() {
return Err(PrepareFailure::NotVerified(source_health));
}
Ok(PreparedEdit {
language,
source,
query,
target_index,
tree,
source_health,
})
}
pub(super) enum PostEditRejection {
Syntax(String),
UnvalidatedWrite,
}
impl PostEditRejection {
pub(super) fn health(&self, language: Language) -> ParserHealth {
match self {
Self::Syntax(_) => ParserHealth::observed(language, ParserOperation::SafeEdit, true),
Self::UnvalidatedWrite => {
ParserHealth::unavailable(Some(language), ParserOperation::SafeEdit)
}
}
}
}
pub(super) fn check_post_edit(
patched: &[u8],
language: Language,
validate: bool,
dry_run: bool,
) -> Result<ParserHealth, PostEditRejection> {
let post_edit_error = first_syntax_error(patched, language);
if validate {
if let Some(detail) = post_edit_error {
return Err(PostEditRejection::Syntax(detail));
}
} else if !dry_run {
return Err(PostEditRejection::UnvalidatedWrite);
}
Ok(ParserHealth::observed(
language,
ParserOperation::SafeEdit,
post_edit_error.is_some(),
))
}
pub(super) fn no_match_response(
path: &str,
query: &str,
target_capture: &str,
details: &str,
) -> VmValue {
build_dict([
("result", str_value("no_match")),
("applied", VmValue::Bool(false)),
("path", str_value(path)),
("query", str_value(query)),
("target_capture", str_value(target_capture)),
("details", str_value(details)),
])
}
pub(super) fn invalid_query_response(query: &str, err: &QueryError) -> VmValue {
build_dict([
("result", str_value("invalid_query")),
("applied", VmValue::Bool(false)),
("query", str_value(query)),
("details", str_value(format_query_error(err))),
("error_row", VmValue::Int(err.row as i64)),
("error_column", VmValue::Int(err.column as i64)),
])
}
pub(super) fn unsupported_language_response(path: &str, hint: Option<&str>) -> VmValue {
build_dict([
("result", str_value("unsupported_language")),
("applied", VmValue::Bool(false)),
("path", str_value(path)),
(
"details",
str_value(unsupported_language_detail(path, hint)),
),
("fallback_suggestion", str_value(TEXT_PATCH_FALLBACK)),
])
}
pub(super) fn unsupported_language_detail(path: &str, hint: Option<&str>) -> String {
format!(
"could not infer a tree-sitter grammar for `{path}` (hint: {})",
hint.unwrap_or("none")
)
}
pub(super) fn non_verified_response(path: &str, details: &str) -> VmValue {
build_dict([
("result", str_value("parser_not_verified")),
("applied", VmValue::Bool(false)),
("path", str_value(path)),
("details", str_value(details)),
])
}