Skip to content

Commit 02ff26d

Browse files
shigarovclaude
andcommitted
core: record item source spans in CellItem
CellItem (and its Python mirror CellDerivedItem) now carries `span` — the byte range of the item's source segment within the raw cell text, before extractors: atomic content spans the whole text; delimited and compound segments keep the offsets the matcher already computed while splitting (adjusted for java_trim). Matching semantics are unchanged (pytest: 1908). Enables per-item highlighting in the vscode-rtl match preview (vscode-rtl plan §3.2 p. 5, §5.4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent e07eab6 commit 02ff26d

4 files changed

Lines changed: 60 additions & 14 deletions

File tree

src/matcher.rs

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ fn process_atomic(
358358
col: usize,
359359
input_text: &str,
360360
item_index: usize,
361+
span: (usize, usize),
361362
sem: &mut SemanticsCore,
362363
) -> Result<(), SemErr> {
363364
if atomic.idd == Idd::Skip {
@@ -376,6 +377,7 @@ fn process_atomic(
376377
row,
377378
col,
378379
ty,
380+
span,
379381
});
380382
for action_spec in &atomic.actions {
381383
let action = instantiate_action(ItemId::Cell(item_id), action_spec, sem)?;
@@ -384,19 +386,33 @@ fn process_atomic(
384386
Ok(())
385387
}
386388

389+
/// Non-empty `java_trim`med parts of a literal split as
390+
/// (original part position, trimmed part, byte span in the original cell
391+
/// text); `base` is the offset of `text` within that cell text.
392+
fn split_with_spans(delim: &str, text: &str, base: usize) -> Vec<(usize, String, (usize, usize))> {
393+
let mut out = Vec::new();
394+
let mut start = 0usize;
395+
for (i, part) in split_literal(delim, text).into_iter().enumerate() {
396+
let trimmed = java_trim(&part);
397+
if !trimmed.is_empty() {
398+
let lead = part.len() - part.trim_start_matches(|c: char| c <= ' ').len();
399+
let from = base + start + lead;
400+
out.push((i, trimmed.to_string(), (from, from + trimmed.len())));
401+
}
402+
start += part.len() + delim.len();
403+
}
404+
out
405+
}
406+
387407
fn process_delimited(
388408
d: &DelimitedSpec,
389409
row: usize,
390410
col: usize,
391411
text: &str,
392412
sem: &mut SemanticsCore,
393413
) -> Result<(), SemErr> {
394-
let parts = split_literal(&d.delimiter, text);
395-
for (i, part) in parts.iter().enumerate() {
396-
let part = java_trim(part);
397-
if !part.is_empty() {
398-
process_atomic(&d.atom, row, col, part, i, sem)?;
399-
}
414+
for (i, part, span) in split_with_spans(&d.delimiter, text, 0) {
415+
process_atomic(&d.atom, row, col, &part, i, span, sem)?;
400416
}
401417
Ok(())
402418
}
@@ -444,16 +460,13 @@ fn process_compound(
444460
let substring = &text[pos..end_pos];
445461
match &seg.spec {
446462
ContentSpec::Atomic(a) => {
447-
process_atomic(a, row, col, substring, item_index, sem)?;
463+
process_atomic(a, row, col, substring, item_index, (pos, end_pos), sem)?;
448464
item_index += 1;
449465
}
450466
ContentSpec::Delimited(d) => {
451-
for part in split_literal(&d.delimiter, substring) {
452-
let trimmed = java_trim(&part);
453-
if !trimmed.is_empty() {
454-
process_atomic(&d.atom, row, col, trimmed, item_index, sem)?;
455-
item_index += 1;
456-
}
467+
for (_, part, span) in split_with_spans(&d.delimiter, substring, pos) {
468+
process_atomic(&d.atom, row, col, &part, item_index, span, sem)?;
469+
item_index += 1;
457470
}
458471
}
459472
_ => unreachable!("compound segment restricted to atomic/delimited"),
@@ -476,7 +489,8 @@ fn process_content_spec(
476489
match cs {
477490
ContentSpec::Atomic(a) => {
478491
let text = syntax.cell(row, col).text.clone();
479-
process_atomic(a, row, col, &text, 0, sem)
492+
let span = (0, text.len());
493+
process_atomic(a, row, col, &text, 0, span, sem)
480494
}
481495
ContentSpec::Delimited(d) => {
482496
let text = syntax.cell(row, col).text.clone();

src/py.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub fn call_item_filter(
8585
ty: it.ty,
8686
row: it.row,
8787
col: it.col,
88+
span: it.span,
8889
table: table.as_ref().map(|t| t.clone_ref(py)),
8990
};
9091
let res = func
@@ -635,6 +636,7 @@ pub struct PyCellDerivedItem {
635636
pub ty: sp::ItemType,
636637
pub row: usize,
637638
pub col: usize,
639+
pub span: (usize, usize),
638640
pub table: Option<Py<PyTableSyntax>>,
639641
}
640642

@@ -644,6 +646,12 @@ impl PyCellDerivedItem {
644646
fn str(&self) -> String {
645647
self.s.clone()
646648
}
649+
/// Byte range of the item's source segment within the raw cell text
650+
/// (before extractors).
651+
#[getter]
652+
fn span(&self) -> (usize, usize) {
653+
self.span
654+
}
647655
#[getter]
648656
fn tags(&self) -> Vec<String> {
649657
self.tags.clone()
@@ -2610,6 +2618,7 @@ impl PyTableSemantics {
26102618
ty: it.ty,
26112619
row: it.row,
26122620
col: it.col,
2621+
span: it.span,
26132622
table: Some(self.table.clone_ref(py)),
26142623
})
26152624
.collect()

src/semantics.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ pub struct CellItem {
2121
pub row: usize,
2222
pub col: usize,
2323
pub ty: ItemType,
24+
/// Byte range of the item's source segment within the raw cell text
25+
/// (before extractors): atomic content spans the whole text, delimited
26+
/// and compound content spans the segment the item was derived from.
27+
pub span: (usize, usize),
2428
}
2529

2630
/// Context-derived item.

src/tests.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@ fn end_to_end_match_and_interpret() {
112112
let sem = match_atp(&pattern, &mut syntax, Vec::new())
113113
.unwrap()
114114
.expect("pattern must match");
115+
116+
// Item spans: the compound cell "0 Jan" (row 1, col 1) yields two VAL
117+
// items whose spans point at the segments of the raw cell text.
118+
let compound: Vec<_> = sem
119+
.cell_items
120+
.iter()
121+
.filter(|it| it.row == 1 && it.col == 1)
122+
.collect();
123+
assert_eq!(compound.len(), 2);
124+
assert_eq!((compound[0].s.as_str(), compound[0].span), ("0", (0, 1)));
125+
assert_eq!((compound[1].s.as_str(), compound[1].span), ("Jan", (2, 5)));
126+
// Atomic cells span their whole text.
127+
let atomic = sem
128+
.cell_items
129+
.iter()
130+
.find(|it| it.row == 1 && it.col == 0)
131+
.unwrap();
132+
assert_eq!(atomic.span, (0, "IKT".len()));
133+
115134
let rs = interpret(&InterpreterCfg::default(), &syntax, &sem, None).unwrap();
116135

117136
assert_eq!(rs.schema.attributes, vec!["ND", "AIRLINE", "AIRPORT", "MON"]);

0 commit comments

Comments
 (0)