Skip to content

Commit 902f50f

Browse files
committed
feat: introduce measuring scope
This commit introduce the notion of "measuring scope", which works in conjonction with "regular" custom scopes, to bind the multi-line-ness of the latter to a smaller subsection of itself.
1 parent 9d1c72e commit 902f50f

File tree

2 files changed

+144
-21
lines changed

2 files changed

+144
-21
lines changed

topiary-core/src/atom_collection.rs

Lines changed: 128 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::{
22
borrow::Cow,
3+
cmp::Ordering,
34
collections::{HashMap, HashSet},
45
mem,
56
ops::Deref,
@@ -307,6 +308,34 @@ impl AtomCollection {
307308
predicates,
308309
);
309310
}
311+
"prepend_begin_measuring_scope" => {
312+
self.prepend(
313+
Atom::MeasuringScopeBegin(scope_information_prepend()?),
314+
node,
315+
predicates,
316+
);
317+
}
318+
"append_begin_measuring_scope" => {
319+
self.append(
320+
Atom::MeasuringScopeBegin(scope_information_append()?),
321+
node,
322+
predicates,
323+
);
324+
}
325+
"prepend_end_measuring_scope" => {
326+
self.prepend(
327+
Atom::MeasuringScopeEnd(scope_information_prepend()?),
328+
node,
329+
predicates,
330+
);
331+
}
332+
"append_end_measuring_scope" => {
333+
self.append(
334+
Atom::MeasuringScopeEnd(scope_information_append()?),
335+
node,
336+
predicates,
337+
);
338+
}
310339
// Scoped softlines
311340
"append_empty_scoped_softline" => {
312341
let id = self.next_id();
@@ -404,10 +433,29 @@ impl AtomCollection {
404433
pub fn apply_prepends_and_appends(&mut self) {
405434
let mut expanded: Vec<Atom> = Vec::new();
406435

436+
// We sort the prepends/appends so that:
437+
// * BeginScope(s) will always be the first element(s)
438+
// * EndScope(s) will always be the last element(s)
439+
// This permits proper processing of measuring scopes and scoped atoms
440+
// that are added at the same place as Begin/EndScopes.
441+
fn compare_atoms (a1: &Atom, a2: &Atom) -> Ordering {
442+
match (a1, a2) {
443+
(Atom::ScopeBegin(_), Atom::ScopeBegin(_)) => {Ordering::Equal}
444+
(Atom::ScopeBegin(_), _) => {Ordering::Less}
445+
(_, Atom::ScopeBegin(_)) => {Ordering::Greater}
446+
(Atom::ScopeEnd(_), Atom::ScopeEnd(_)) => {Ordering::Equal}
447+
(Atom::ScopeEnd(_), _) => {Ordering::Greater}
448+
(_, Atom::ScopeEnd(_)) => {Ordering::Less}
449+
(_, _) => {Ordering::Equal}
450+
}
451+
}
452+
407453
for atom in &mut self.atoms {
408454
if let Atom::Leaf { id, .. } = atom {
409455
let prepends = self.prepend.entry(*id).or_default();
456+
prepends.sort_by(compare_atoms);
410457
let appends = self.append.entry(*id).or_default();
458+
appends.sort_by(compare_atoms);
411459

412460
// Rather than cloning the atom from the old vector, we
413461
// simply take it. This will leave a default (empty) atom
@@ -605,10 +653,15 @@ impl AtomCollection {
605653
type ScopeId = String;
606654
type LineIndex = u32;
607655
type ScopedNodeId = usize;
608-
// `opened_scopes` maintains stacks of opened scopes,
609-
// the line at which they started,
610-
// and the list of `ScopedSoftline` they contain.
611-
let mut opened_scopes: HashMap<&ScopeId, Vec<(LineIndex, Vec<&Atom>)>> = HashMap::new();
656+
// `opened_scopes` maintains stacks of opened scopes.
657+
// For each scope, we record:
658+
// * the line at which they started (LineIndex),
659+
// * the list of `ScopedSoftline` and `ScopedConditional` they contain (Vec<&Atom>),
660+
// * if they contain a measuring scope, whether it is multi-line (Option<bool>).
661+
let mut opened_scopes: HashMap<&ScopeId, Vec<(LineIndex, Vec<&Atom>, Option<bool>)>> = HashMap::new();
662+
// `opened_measuring_scopes` maintains stacks of opened measuring scopes,
663+
// and the line at which they started.
664+
let mut opened_measuring_scopes: HashMap<&ScopeId, Vec<LineIndex>> = HashMap::new();
612665
// We can't process `ScopedSoftline` in-place as we encounter them in the list of
613666
// atoms: we need to know when their encompassing scope ends to decide what to
614667
// replace them with. Instead of in-place modifications, we associate a replacement
@@ -631,16 +684,20 @@ impl AtomCollection {
631684
opened_scopes
632685
.entry(scope_id)
633686
.or_default()
634-
.push((*line_start, Vec::new()));
687+
.push((*line_start, Vec::new(), None));
635688
} else if let Atom::ScopeEnd(ScopeInformation {
636689
line_number: line_end,
637690
scope_id,
638691
}) = atom
639692
{
640-
if let Some((line_start, atoms)) =
693+
if let Some((line_start, atoms, measuring_scope)) =
641694
opened_scopes.get_mut(scope_id).and_then(Vec::pop)
642695
{
643-
let multiline = line_start != *line_end;
696+
let multiline = if let Some(mult) = measuring_scope {
697+
mult
698+
} else {
699+
line_start != *line_end
700+
};
644701
for atom in atoms {
645702
if let Atom::ScopedSoftline { id, spaced, .. } = atom {
646703
let new_atom = if multiline {
@@ -671,9 +728,54 @@ impl AtomCollection {
671728
log::warn!("Closing unopened scope {scope_id:?}");
672729
force_apply_modifications = true;
673730
}
731+
// Open measuring scope
732+
} else if let Atom::MeasuringScopeBegin(ScopeInformation {
733+
line_number: line_start,
734+
scope_id,
735+
}) = atom
736+
{
737+
if opened_scopes.entry(scope_id).or_default().is_empty() {
738+
log::warn!("Opening measuring scope with no associated regular scope {scope_id:?}");
739+
force_apply_modifications = true;
740+
} else {
741+
opened_measuring_scopes
742+
.entry(scope_id)
743+
.or_default()
744+
.push(*line_start)
745+
}
746+
// Close measuring scope and register multi-line-ness in the appropriate regular scope
747+
} else if let Atom::MeasuringScopeEnd(ScopeInformation {
748+
line_number: line_end,
749+
scope_id,
750+
}) = atom
751+
{
752+
if let Some(line_start) =
753+
opened_measuring_scopes.get_mut(scope_id).and_then(Vec::pop)
754+
{
755+
let multi_line = line_start != *line_end;
756+
if let Some((regular_line_start, vec, measuring_scope)) =
757+
opened_scopes.get_mut(scope_id).and_then(Vec::pop)
758+
{
759+
if measuring_scope == None {
760+
opened_scopes
761+
.entry(scope_id)
762+
.or_default()
763+
.push((regular_line_start, vec, Some(multi_line)));
764+
} else {
765+
log::warn!("Found several measuring scopes in a single regular scope {scope_id:?}");
766+
force_apply_modifications = true;
767+
}
768+
} else {
769+
log::warn!("Found measuring scope outside of regular scope {scope_id:?}");
770+
force_apply_modifications = true;
771+
}
772+
} else {
773+
log::warn!("Closing unopened measuring scope {scope_id:?}");
774+
force_apply_modifications = true;
775+
}
674776
// Register the ScopedSoftline in the correct scope
675777
} else if let Atom::ScopedSoftline { scope_id, .. } = atom {
676-
if let Some((_, vec)) = opened_scopes.get_mut(&scope_id).and_then(|v| v.last_mut())
778+
if let Some((_, vec, _)) = opened_scopes.get_mut(&scope_id).and_then(|v| v.last_mut())
677779
{
678780
vec.push(atom);
679781
} else {
@@ -682,7 +784,7 @@ impl AtomCollection {
682784
}
683785
// Register the ScopedConditional in the correct scope
684786
} else if let Atom::ScopedConditional { scope_id, .. } = atom {
685-
if let Some((_, vec)) = opened_scopes.get_mut(&scope_id).and_then(|v| v.last_mut())
787+
if let Some((_, vec, _)) = opened_scopes.get_mut(&scope_id).and_then(|v| v.last_mut())
686788
{
687789
vec.push(atom);
688790
} else {
@@ -691,21 +793,32 @@ impl AtomCollection {
691793
}
692794
}
693795
}
694-
let still_opened: Vec<&String> = opened_scopes
796+
let mut still_opened: Vec<&String> = opened_scopes
695797
.into_iter()
696798
.filter_map(|(scope_id, vec)| if vec.is_empty() { None } else { Some(scope_id) })
697799
.collect();
698800
if !still_opened.is_empty() {
699801
log::warn!("Some scopes have been left opened: {:?}", still_opened);
700802
force_apply_modifications = true;
701803
}
804+
still_opened = opened_measuring_scopes
805+
.into_iter()
806+
.filter_map(|(scope_id, vec)| if vec.is_empty() { None } else { Some(scope_id) })
807+
.collect();
808+
if !still_opened.is_empty() {
809+
log::warn!("Some measuring scopes have been left opened: {:?}", still_opened);
810+
force_apply_modifications = true;
811+
}
702812

703813
// Remove scopes from the atom list
704814
for atom in &mut self.atoms {
705-
match atom {
706-
Atom::ScopeBegin(_) | Atom::ScopeEnd(_) => {
707-
*atom = Atom::Empty;
708-
}
815+
match atom
816+
{
817+
| Atom::ScopeBegin(_)
818+
| Atom::ScopeEnd(_)
819+
| Atom::MeasuringScopeBegin(_)
820+
| Atom::MeasuringScopeEnd(_) =>
821+
{*atom = Atom::Empty}
709822
_ => {}
710823
}
711824
}
@@ -954,7 +1067,7 @@ fn collapse_spaces_before_antispace(v: &mut [Atom]) {
9541067
antispace_mode = true;
9551068
} else if *a == Atom::Space && antispace_mode {
9561069
*a = Atom::Empty;
957-
} else {
1070+
} else if *a != Atom::Empty { // Don't change mode when encountering Empty
9581071
antispace_mode = false;
9591072
}
9601073
}

topiary-core/src/lib.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,20 +92,30 @@ pub enum Atom {
9292
/// Indicates the end of a scope, use in combination with the
9393
/// ScopedSoftlines and ScopedConditionals below.
9494
ScopeEnd(ScopeInformation),
95+
// Indicates the beginning of a *measuring* scope, that must be related to a "normal" one.
96+
// Used in combination with ScopedSoftlines and ScopedConditionals below.
97+
MeasuringScopeBegin(ScopeInformation),
98+
// Indicates the end of a *measuring* scope, that must be related to a "normal" one.
99+
// Used in combination with ScopedSoftlines and ScopedConditionals below.
100+
MeasuringScopeEnd(ScopeInformation),
95101
/// Scoped commands
96-
// ScopedSoftline works together with the @{prepend,append}_begin_scope and
97-
// @{prepend,append}_end_scope query tags. To decide if a scoped softline
102+
// ScopedSoftline works together with the @{prepend,append}_begin[_measuring]_scope and
103+
// @{prepend,append}_end[_measuring]_scope query tags. To decide if a scoped softline
98104
// must be expanded into a hardline, we look at the innermost scope having
99105
// the corresponding `scope_id`, that encompasses it. We expand the softline
100-
// if that scope is multi-line. The `id` value is here for technical
101-
// reasons, it allows tracking of the atom during post-processing.
106+
// if that scope is multi-line.
107+
// If that scope contains a *measuring* scope with the same `scope_id`, we expand
108+
// the node iff that *measuring* scope is multi-line.
109+
// The `id` value is here for technical reasons,
110+
// it allows tracking of the atom during post-processing.
102111
ScopedSoftline {
103112
id: usize,
104113
scope_id: String,
105114
spaced: bool,
106115
},
107-
/// Represents an atom that must only be output if the associated scope meets the condition
108-
/// (single-line or multi-line)
116+
/// Represents an atom that must only be output if the associated scope
117+
/// (or its associated measuring scope, see above) meets the condition
118+
/// (single-line or multi-line).
109119
ScopedConditional {
110120
id: usize,
111121
scope_id: String,

0 commit comments

Comments
 (0)