use std::collections::BTreeMap;
use crate::hir::{
Block, Choice, Conditional, Content, ContentPart, DivertPath, Expr, HirFile, HirVisitor, Knot,
Sequence, Stitch, Stmt,
};
use brink_format::DefinitionId;
use rowan::TextRange;
use crate::line_index::LineIndex;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpanKind {
Knot,
Stitch,
Choice,
Gather,
ConditionalBranch,
SequenceBranch,
Label,
Param,
VarDecl,
ConstDecl,
ListDecl,
ListMember,
External,
TempDecl,
Divert,
VarRef,
Call,
Content,
Interpolation,
Tag,
Include,
DivertStmt,
TunnelStmt,
ThreadStmt,
DivertTerminal,
Logic,
Conditional,
Sequence,
}
impl SpanKind {
#[must_use]
pub fn is_container(self) -> bool {
matches!(
self,
Self::Knot
| Self::Stitch
| Self::Choice
| Self::Gather
| Self::ConditionalBranch
| Self::SequenceBranch
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ProjectedSpan {
pub range: TextRange,
pub kind: SpanKind,
pub depth: u32,
pub def_id: Option<DefinitionId>,
pub target_id: Option<DefinitionId>,
pub handle: Option<u32>,
pub sticky: Option<bool>,
pub weave_depth: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LineContainer {
pub kind: SpanKind,
pub handle: u32,
pub depth: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LineStack {
pub containers: Vec<LineContainer>,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Projection {
pub spans: Vec<ProjectedSpan>,
pub lines: Vec<LineStack>,
pub option_paths: BTreeMap<u32, Vec<u32>>,
}
#[must_use]
pub fn range_key(range: TextRange) -> (u32, u32) {
(range.start().into(), range.end().into())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JoinKey {
Decl(TextRange),
Ref(TextRange),
Anon(TextRange),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ProjectionParts {
pub spans: Vec<ProjectedSpan>,
pub join_keys: Vec<Option<JoinKey>>,
pub option_paths: BTreeMap<u32, Vec<u32>>,
pub handle_count: u32,
}
#[must_use]
pub fn project_walk_parts(hir: &HirFile, source: &str) -> ProjectionParts {
let empty_decls = BTreeMap::new();
let empty_refs = BTreeMap::new();
let mut v = new_visitor(source, &empty_decls, &empty_refs, &empty_decls);
crate::hir::visit::visit(hir, &mut v);
ProjectionParts {
spans: v.spans,
join_keys: v.join_keys,
option_paths: v.option_paths,
handle_count: v.next_handle,
}
}
#[must_use]
pub fn project_file_decl_parts(hir: &HirFile, source: &str) -> ProjectionParts {
let empty_decls = BTreeMap::new();
let empty_refs = BTreeMap::new();
let mut v = new_visitor(source, &empty_decls, &empty_refs, &empty_decls);
emit_file_decl_spans(&mut v, hir);
ProjectionParts {
spans: v.spans,
join_keys: v.join_keys,
option_paths: v.option_paths,
handle_count: v.next_handle,
}
}
#[must_use]
pub fn project_hir_structural(hir: &HirFile, source: &str) -> Projection {
project_with_maps(
hir,
source,
&BTreeMap::new(),
&BTreeMap::new(),
&BTreeMap::new(),
)
}
fn new_visitor<'a>(
source: &'a str,
decl_ids: &'a BTreeMap<(u32, u32), DefinitionId>,
ref_targets: &'a BTreeMap<(u32, u32), DefinitionId>,
anon_ids: &'a BTreeMap<(u32, u32), DefinitionId>,
) -> ProjectionVisitor<'a> {
ProjectionVisitor {
source,
decl_ids,
ref_targets,
anon_ids,
spans: Vec::new(),
join_keys: Vec::new(),
depth: 0,
next_handle: 0,
cs_depths: Vec::new(),
continuation_labels: BTreeMap::new(),
slot_construct_ranges: Vec::new(),
cs_child_counters: Vec::new(),
open_choices: Vec::new(),
option_paths: BTreeMap::new(),
}
}
fn emit_file_decl_spans(v: &mut ProjectionVisitor<'_>, hir: &HirFile) {
for var in &hir.variables {
v.push_decl(var.name.range, SpanKind::VarDecl);
}
for c in &hir.constants {
v.push_decl(c.name.range, SpanKind::ConstDecl);
}
for list in &hir.lists {
v.push_decl(list.name.range, SpanKind::ListDecl);
for member in &list.members {
v.push_decl(member.name.range, SpanKind::ListMember);
}
}
for ext in &hir.externals {
v.push_decl(ext.name.range, SpanKind::External);
}
for inc in &hir.includes {
v.push_inline(inc.ptr.text_range(), SpanKind::Include);
}
}
pub fn project_with_maps(
hir: &HirFile,
source: &str,
decl_ids: &BTreeMap<(u32, u32), DefinitionId>,
ref_targets: &BTreeMap<(u32, u32), DefinitionId>,
anon_ids: &BTreeMap<(u32, u32), DefinitionId>,
) -> Projection {
let mut v = new_visitor(source, decl_ids, ref_targets, anon_ids);
emit_file_decl_spans(&mut v, hir);
crate::hir::visit::visit(hir, &mut v);
let spans = v.spans;
let option_paths = v.option_paths;
let lines = build_line_stacks(&spans, source);
Projection {
spans,
lines,
option_paths,
}
}
struct ProjectionVisitor<'a> {
source: &'a str,
decl_ids: &'a BTreeMap<(u32, u32), DefinitionId>,
ref_targets: &'a BTreeMap<(u32, u32), DefinitionId>,
anon_ids: &'a BTreeMap<(u32, u32), DefinitionId>,
spans: Vec<ProjectedSpan>,
join_keys: Vec<Option<JoinKey>>,
depth: u32,
next_handle: u32,
cs_depths: Vec<u32>,
continuation_labels: BTreeMap<(u32, u32), u32>,
slot_construct_ranges: Vec<TextRange>,
cs_child_counters: Vec<u32>,
open_choices: Vec<u32>,
option_paths: BTreeMap<u32, Vec<u32>>,
}
impl ProjectionVisitor<'_> {
fn current_weave_depth(&self) -> u32 {
self.cs_depths.last().copied().unwrap_or(0)
}
fn in_slot_construct(&self, range: TextRange) -> bool {
self.slot_construct_ranges
.iter()
.any(|r| r.contains_range(range))
}
fn push_decl(&mut self, range: TextRange, kind: SpanKind) {
let def_id = self.decl_ids.get(&range_key(range)).copied();
self.join_keys.push(Some(JoinKey::Decl(range)));
self.spans.push(ProjectedSpan {
range,
kind,
depth: self.depth,
def_id,
target_id: None,
handle: None,
sticky: None,
weave_depth: None,
});
}
fn push_ref(&mut self, range: TextRange, kind: SpanKind) {
let target_id = self.ref_targets.get(&range_key(range)).copied();
self.join_keys.push(Some(JoinKey::Ref(range)));
self.spans.push(ProjectedSpan {
range,
kind,
depth: self.depth,
def_id: None,
target_id,
handle: None,
sticky: None,
weave_depth: None,
});
}
fn push_inline(&mut self, range: TextRange, kind: SpanKind) {
self.join_keys.push(None);
self.spans.push(ProjectedSpan {
range,
kind,
depth: self.depth,
def_id: None,
target_id: None,
handle: None,
sticky: None,
weave_depth: None,
});
}
fn push_container(&mut self, range: TextRange, kind: SpanKind, join: Option<JoinKey>) {
let _ = self.push_container_full(range, kind, join, None, None);
}
fn push_weave_container(
&mut self,
range: TextRange,
kind: SpanKind,
join: Option<JoinKey>,
sticky: Option<bool>,
weave_depth: u32,
) -> u32 {
self.push_container_full(range, kind, join, sticky, Some(weave_depth))
}
fn push_container_full(
&mut self,
range: TextRange,
kind: SpanKind,
join: Option<JoinKey>,
sticky: Option<bool>,
weave_depth: Option<u32>,
) -> u32 {
let def_id = match join {
Some(JoinKey::Decl(r)) => self.decl_ids.get(&range_key(r)).copied(),
Some(JoinKey::Anon(r)) => self.anon_ids.get(&range_key(r)).copied(),
_ => None,
};
self.join_keys.push(join);
let handle = self.next_handle;
self.next_handle += 1;
self.spans.push(ProjectedSpan {
range,
kind,
depth: self.depth,
def_id,
target_id: None,
handle: Some(handle),
sticky,
weave_depth,
});
handle
}
fn push_divert_target(&mut self, target: &crate::hir::DivertTarget) {
if let DivertPath::Path(path) = &target.path {
self.push_ref(path.range, SpanKind::Divert);
}
}
fn push_cond_branches(&mut self, cond: &Conditional) {
for branch in &cond.branches {
if let Some(ext) = block_extent(&branch.body) {
self.push_container(
ext,
SpanKind::ConditionalBranch,
Some(JoinKey::Anon(branch.ptr.text_range())),
);
}
}
}
fn push_seq_branches(&mut self, seq: &Sequence) {
for branch in &seq.branches {
if let Some(ext) = block_extent(&branch.body) {
self.push_container(
ext,
SpanKind::SequenceBranch,
Some(JoinKey::Anon(branch.ptr.text_range())),
);
}
}
}
fn project_content_extras(&mut self, content: &Content, ctx: crate::hir::ContentContext) {
let in_body = ctx == crate::hir::ContentContext::Body;
for part in &content.parts {
self.project_content_part_extras(part, in_body);
}
for tag in &content.tags {
self.push_inline(tag.ptr.text_range(), SpanKind::Tag);
}
}
fn project_content_part_extras(&mut self, part: &ContentPart, in_body: bool) {
match part {
ContentPart::InlineConditional(cond) => {
if in_body {
self.push_inline(cond.ptr.text_range(), SpanKind::Conditional);
} else {
self.slot_construct_ranges.push(cond.ptr.text_range());
}
self.push_container(cond.ptr.text_range(), SpanKind::ConditionalBranch, None);
}
ContentPart::InlineSequence(seq) => {
if in_body {
self.push_inline(seq.ptr.text_range(), SpanKind::Sequence);
} else {
self.slot_construct_ranges.push(seq.ptr.text_range());
}
self.push_container(
seq.ptr.text_range(),
SpanKind::SequenceBranch,
Some(JoinKey::Anon(seq.ptr.text_range())),
);
}
ContentPart::Span(span) => {
for child in &span.children {
self.project_content_part_extras(child, in_body);
}
}
ContentPart::Text(_)
| ContentPart::Glue
| ContentPart::Spring
| ContentPart::Interpolation(_) => {}
}
}
}
impl HirVisitor for ProjectionVisitor<'_> {
fn visit_exprs(&self) -> bool {
true
}
fn enter_block(&mut self, block: &Block) {
if let Some(label) = &block.label {
let def_id = self.decl_ids.get(&range_key(label.range)).copied();
self.join_keys.push(Some(JoinKey::Decl(label.range)));
self.spans.push(ProjectedSpan {
range: label.range,
kind: SpanKind::Label,
depth: self.depth,
def_id,
target_id: None,
handle: None,
sticky: None,
weave_depth: self
.continuation_labels
.get(&range_key(label.range))
.copied(),
});
}
}
fn enter_knot(&mut self, knot: &Knot) {
self.push_container(
knot.ptr.text_range(),
SpanKind::Knot,
Some(JoinKey::Decl(knot.name.range)),
);
self.push_decl(knot.name.range, SpanKind::Knot);
for param in &knot.params {
self.push_decl(param.name.range, SpanKind::Param);
}
self.depth += 1;
}
fn exit_knot(&mut self, _knot: &Knot) {
self.depth = self.depth.saturating_sub(1);
}
fn enter_stitch(&mut self, stitch: &Stitch) {
self.push_container(
stitch.ptr.text_range(),
SpanKind::Stitch,
Some(JoinKey::Decl(stitch.name.range)),
);
self.push_decl(stitch.name.range, SpanKind::Stitch);
for param in &stitch.params {
self.push_decl(param.name.range, SpanKind::Param);
}
self.depth += 1;
}
fn exit_stitch(&mut self, _stitch: &Stitch) {
self.depth = self.depth.saturating_sub(1);
}
fn enter_choice(&mut self, choice: &Choice) {
let mut extent = choice.ptr.text_range();
if let Some(body) = block_extent(&choice.body) {
extent = extent.cover(body);
}
let weave_depth = choice_sigil_depth(self.source, choice.ptr.text_range().start())
.unwrap_or_else(|| self.current_weave_depth());
let handle = self.push_weave_container(
extent,
SpanKind::Choice,
Some(JoinKey::Anon(choice.ptr.text_range())),
Some(choice.is_sticky),
weave_depth,
);
let index = if let Some(counter) = self.cs_child_counters.last_mut() {
let i = *counter;
*counter += 1;
i
} else {
0
};
self.open_choices.push(index);
self.option_paths.insert(handle, self.open_choices.clone());
if let Some(label) = &choice.label {
self.push_decl(label.range, SpanKind::Label);
}
for tag in &choice.tags {
self.push_inline(tag.ptr.text_range(), SpanKind::Tag);
}
self.depth += 1;
}
fn exit_choice(&mut self, _choice: &Choice) {
self.depth = self.depth.saturating_sub(1);
self.open_choices.pop();
}
fn enter_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::Divert(d) => {
if matches!(d.target.path, DivertPath::Done | DivertPath::End) {
if let Some(ptr) = &d.ptr
&& !self.in_slot_construct(ptr.text_range())
{
self.push_inline(ptr.text_range(), SpanKind::DivertTerminal);
}
} else {
if let Some(ptr) = &d.ptr
&& !self.in_slot_construct(ptr.text_range())
{
self.push_inline(ptr.text_range(), SpanKind::DivertStmt);
}
self.push_divert_target(&d.target);
}
}
Stmt::TunnelCall(t) => {
if !self.in_slot_construct(t.ptr.text_range()) {
self.push_inline(t.ptr.text_range(), SpanKind::TunnelStmt);
}
for target in &t.targets {
self.push_divert_target(target);
}
}
Stmt::ThreadStart(t) => {
if !self.in_slot_construct(t.ptr.text_range()) {
self.push_inline(t.ptr.text_range(), SpanKind::ThreadStmt);
}
self.push_divert_target(&t.target);
}
Stmt::TempDecl(t) => self.push_decl(t.name.range, SpanKind::TempDecl),
Stmt::Assignment(a) if !self.in_slot_construct(a.ptr.text_range()) => {
self.push_inline(a.ptr.text_range(), SpanKind::Logic);
}
Stmt::Return(r) => {
if let Some(ptr) = &r.ptr
&& !self.in_slot_construct(ptr.text_range())
{
self.push_inline(ptr.text_range(), SpanKind::Logic);
}
}
Stmt::ChoiceSet(cs) => {
let weave_depth = if cs.context == crate::ChoiceSetContext::Inline {
cs.choices
.first()
.and_then(|c| choice_sigil_depth(self.source, c.ptr.text_range().start()))
.unwrap_or_else(|| self.current_weave_depth())
} else {
cs.depth
};
if let Some(label) = &cs.continuation.label {
self.continuation_labels
.insert(range_key(label.range), weave_depth);
}
if let Some(ext) = block_extent(&cs.continuation) {
self.push_weave_container(
ext,
SpanKind::Gather,
Some(JoinKey::Anon(ext)),
None,
weave_depth,
);
}
self.cs_depths.push(weave_depth);
self.cs_child_counters.push(0);
}
Stmt::Conditional(cond) => {
self.push_inline(cond.ptr.text_range(), SpanKind::Conditional);
self.push_cond_branches(cond);
}
Stmt::Sequence(seq) => {
self.push_inline(seq.ptr.text_range(), SpanKind::Sequence);
self.push_seq_branches(seq);
}
_ => {}
}
}
fn exit_stmt(&mut self, stmt: &Stmt) {
if matches!(stmt, Stmt::ChoiceSet(_)) {
self.cs_depths.pop();
self.cs_child_counters.pop();
}
}
fn enter_content(&mut self, content: &Content, ctx: crate::hir::ContentContext) {
if let Some(ptr) = &content.ptr {
self.push_inline(ptr.text_range(), SpanKind::Content);
}
self.project_content_extras(content, ctx);
}
fn enter_expr(&mut self, expr: &Expr) {
match expr {
Expr::Path(p) => self.push_ref(p.range, SpanKind::VarRef),
Expr::DivertTarget(p) => self.push_ref(p.range, SpanKind::Divert),
Expr::Call(p, _) => self.push_ref(p.range, SpanKind::Call),
_ => {}
}
}
}
fn choice_sigil_depth(source: &str, offset: rowan::TextSize) -> Option<u32> {
let start = usize::from(offset).min(source.len());
let line_start = source[..start].rfind('\n').map_or(0, |i| i + 1);
let line = source[line_start..].split('\n').next().unwrap_or("");
let mut depth = 0u32;
let mut chars = line.trim_start().chars().peekable();
while let Some(&c) = chars.peek() {
match c {
'*' | '+' => {
depth += 1;
chars.next();
}
' ' => {
chars.next();
}
_ => break,
}
}
(depth > 0).then_some(depth)
}
fn block_extent(block: &Block) -> Option<TextRange> {
let mut acc: Option<TextRange> = block.label.as_ref().map(|l| l.range);
for stmt in &block.stmts {
if let Some(r) = stmt_extent(stmt) {
acc = Some(match acc {
Some(a) => a.cover(r),
None => r,
});
}
}
acc
}
fn stmt_extent(stmt: &Stmt) -> Option<TextRange> {
match stmt {
Stmt::Content(c) => c.ptr.as_ref().map(crate::Provenance::text_range),
Stmt::Divert(d) => d.ptr.as_ref().map(crate::Provenance::text_range),
Stmt::TunnelCall(t) => Some(t.ptr.text_range()),
Stmt::ThreadStart(t) => Some(t.ptr.text_range()),
Stmt::TempDecl(t) => Some(t.ptr.text_range()),
Stmt::Assignment(a) => Some(a.ptr.text_range()),
Stmt::Return(r) => r.ptr.as_ref().map(crate::Provenance::text_range),
Stmt::ChoiceSet(cs) => {
let mut acc: Option<TextRange> = None;
for choice in &cs.choices {
let mut ext = choice.ptr.text_range();
if let Some(body) = block_extent(&choice.body) {
ext = ext.cover(body);
}
acc = Some(acc.map_or(ext, |a| a.cover(ext)));
}
if let Some(cont) = block_extent(&cs.continuation) {
acc = Some(acc.map_or(cont, |a| a.cover(cont)));
}
acc
}
Stmt::LabeledBlock(b) => block_extent(b),
Stmt::Conditional(c) => Some(c.ptr.text_range()),
Stmt::Sequence(s) => Some(s.ptr.text_range()),
Stmt::ExprStmt(_) | Stmt::EndOfLine | Stmt::AttachElement(_) | Stmt::EndElementRun => None,
Stmt::LogicBlock(lb) => Some(lb.ptr.text_range()),
Stmt::Await(a) => Some(a.ptr.text_range()),
}
}
#[must_use]
pub fn tight_container_end_line(idx: &LineIndex, source: &str, range: rowan::TextRange) -> u32 {
let start = usize::from(range.start()).min(source.len());
let end = usize::from(range.end()).min(source.len());
let doc_start = crate::doc_extended_start(source, end);
let content_end = if doc_start > start {
doc_start.min(end)
} else {
end
};
let trimmed = start + source[start..content_end].trim_end().len();
idx.line_col(rowan::TextSize::from(
u32::try_from(trimmed).unwrap_or(u32::MAX),
))
.0
}
#[must_use]
pub fn build_line_stacks(spans: &[ProjectedSpan], source: &str) -> Vec<LineStack> {
let idx = LineIndex::new(source);
let line_count = source.lines().count().max(1);
let mut lines = vec![LineStack::default(); line_count];
for span in spans {
let (Some(handle), true) = (span.handle, span.kind.is_container()) else {
continue;
};
let (start_line, _) = idx.line_col(span.range.start());
let end_line = tight_container_end_line(&idx, source, span.range).max(start_line);
for line in start_line..=end_line {
if let Some(stack) = lines.get_mut(line as usize) {
stack.containers.push(LineContainer {
kind: span.kind,
handle,
depth: span.depth,
});
}
}
}
for stack in &mut lines {
stack.containers.sort_by_key(|c| c.depth);
}
lines
}
#[must_use]
pub fn anonymous_container_ids(hir: &HirFile) -> BTreeMap<(u32, u32), DefinitionId> {
struct Collector {
ids: BTreeMap<(u32, u32), DefinitionId>,
}
impl Collector {
fn insert(&mut self, range: TextRange, id: Option<DefinitionId>) {
if let Some(id) = id {
self.ids.insert(range_key(range), id);
}
}
fn collect_part(&mut self, part: &ContentPart) {
match part {
ContentPart::InlineSequence(seq) => {
self.insert(seq.ptr.text_range(), seq.container_id);
}
ContentPart::Span(span) => {
for child in &span.children {
self.collect_part(child);
}
}
_ => {}
}
}
}
impl HirVisitor for Collector {
fn enter_choice(&mut self, choice: &Choice) {
self.insert(choice.ptr.text_range(), choice.container_id);
}
fn enter_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::ChoiceSet(cs) => {
if let Some(ext) = block_extent(&cs.continuation) {
self.insert(ext, cs.gather_id);
}
}
Stmt::Conditional(cond) => {
for branch in &cond.branches {
self.insert(branch.ptr.text_range(), branch.container_id);
}
}
Stmt::Sequence(seq) => {
for branch in &seq.branches {
self.insert(branch.ptr.text_range(), branch.body.container_id);
}
}
_ => {}
}
}
fn enter_content(&mut self, content: &Content, _ctx: crate::hir::ContentContext) {
for part in &content.parts {
self.collect_part(part);
}
}
}
let mut c = Collector {
ids: BTreeMap::new(),
};
crate::hir::visit::visit(hir, &mut c);
c.ids
}