fmtview 0.3.6

Fast CLI viewer for highlighting, search, and diffs across JSON, JSONL, markup, Markdown, TOML, text, and Jinja
Documentation
use crate::syntax::SyntaxKind;

use super::super::search::SearchTarget;

const JSON_VISIBLE_COMPOSITE_LANDMARK_LINES: usize = 5;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum StructureCandidateKind {
    JsonRecordStart,
    JsonArrayItemStart,
    JsonCompositeField,
    JsonRootStart,
    XmlStartTag,
    MarkdownHeading,
    TomlTable,
    JinjaBlock,
    PlainParagraph,
}

impl StructureCandidateKind {
    pub(super) fn is_landmark_when_visible(self, line_span: Option<usize>) -> bool {
        match self {
            StructureCandidateKind::JsonRecordStart
            | StructureCandidateKind::JsonArrayItemStart
            | StructureCandidateKind::JsonRootStart
            | StructureCandidateKind::MarkdownHeading
            | StructureCandidateKind::TomlTable
            | StructureCandidateKind::JinjaBlock
            | StructureCandidateKind::PlainParagraph => true,
            StructureCandidateKind::XmlStartTag => line_span.is_none_or(|span| span > 1),
            StructureCandidateKind::JsonCompositeField => {
                line_span.is_some_and(|span| span >= JSON_VISIBLE_COMPOSITE_LANDMARK_LINES)
            }
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct StructureCandidate {
    pub(super) line: usize,
    pub(super) byte_index: usize,
    pub(super) kind: StructureCandidateKind,
    pub(super) indent: usize,
}

impl StructureCandidate {
    pub(super) fn target(self) -> SearchTarget {
        SearchTarget {
            line: self.line,
            byte_index: self.byte_index,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct StructureAnchor {
    pub(super) line: usize,
    pub(super) kind: Option<StructureCandidateKind>,
    pub(super) indent: usize,
}

pub(super) fn select_structure_candidate(
    candidates: &[StructureCandidate],
    syntax: SyntaxKind,
    anchor: Option<StructureAnchor>,
) -> Option<StructureCandidate> {
    candidates
        .iter()
        .copied()
        .min_by_key(|candidate| structure_candidate_rank(*candidate, syntax, anchor))
}

fn structure_candidate_rank(
    candidate: StructureCandidate,
    syntax: SyntaxKind,
    anchor: Option<StructureAnchor>,
) -> (usize, usize, usize) {
    let distance = anchor
        .map(|anchor| anchor.line.abs_diff(candidate.line))
        .unwrap_or(candidate.line);
    if syntax != SyntaxKind::Structured {
        return (0, distance, 0);
    }

    let Some(anchor) = anchor else {
        return (0, distance, json_candidate_priority(candidate.kind));
    };

    match anchor.kind {
        Some(StructureCandidateKind::JsonArrayItemStart) => {
            let scope = usize::from(candidate.indent > anchor.indent);
            (scope, json_candidate_priority(candidate.kind), distance)
        }
        Some(StructureCandidateKind::JsonCompositeField) => {
            let scope = usize::from(candidate.indent <= anchor.indent);
            (scope, distance, json_candidate_priority(candidate.kind))
        }
        Some(StructureCandidateKind::JsonRecordStart | StructureCandidateKind::JsonRootStart) => {
            let scope = usize::from(candidate.kind == StructureCandidateKind::JsonRecordStart);
            (scope, distance, json_candidate_priority(candidate.kind))
        }
        _ => (0, distance, json_candidate_priority(candidate.kind)),
    }
}

fn json_candidate_priority(kind: StructureCandidateKind) -> usize {
    match kind {
        StructureCandidateKind::JsonArrayItemStart => 0,
        StructureCandidateKind::JsonRootStart => 1,
        StructureCandidateKind::JsonRecordStart => 2,
        StructureCandidateKind::JsonCompositeField => 3,
        StructureCandidateKind::XmlStartTag => 4,
        StructureCandidateKind::MarkdownHeading
        | StructureCandidateKind::TomlTable
        | StructureCandidateKind::JinjaBlock
        | StructureCandidateKind::PlainParagraph => 5,
    }
}