001package gudusoft.gsqlparser.pp2.layout.rules;
002
003import gudusoft.gsqlparser.pp.para.GFmtOpt;
004import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult;
005import gudusoft.gsqlparser.pp2.layout.LayoutContext;
006import gudusoft.gsqlparser.pp2.layout.LayoutDecisionView;
007import gudusoft.gsqlparser.pp2.layout.LayoutPriorities;
008import gudusoft.gsqlparser.pp2.layout.LayoutRule;
009import gudusoft.gsqlparser.pp2.token.Pp2Token;
010import gudusoft.gsqlparser.pp2.token.Pp2TokenStream;
011import gudusoft.gsqlparser.pp2.token.TokenRole;
012
013/**
014 * Indents every line-starting token. The indent width is
015 * {@link GFmtOpt#indentLen} (default 2) and the level is:
016 *
017 * <ul>
018 *   <li>the SQL scope level (subquery / CTE nesting, from S20) — so a
019 *       subquery's lines indent under their enclosing query; plus</li>
020 *   <li>one extra level for clause-body continuation lines (stacked columns,
021 *       AND/OR conjuncts, CASE branches) — i.e. any broken line whose first
022 *       token is <i>not</i> a master/clause keyword.</li>
023 * </ul>
024 *
025 * <p>So master and clause keywords ({@code SELECT}/{@code FROM}/{@code WHERE}/
026 * {@code JOIN}/...) sit at the scope's base column, and the items they govern
027 * sit one level in. Only tokens that begin a line (effective
028 * {@code linebreaksBefore > 0}) are indented; mid-line tokens are untouched.
029 *
030 * <p>Priority {@link LayoutPriorities#INDENT} (overrides the spacing rule's
031 * leading blank). Needs the S20 sql scope. Iterative; read-only over tokens.
032 *
033 * <p>Plan reference: §7.3/S28, §7.4/S28.
034 */
035public final class IndentRules implements LayoutRule {
036
037    @Override
038    public int priority() { return LayoutPriorities.INDENT; }
039
040    @Override
041    public String name() { return "IndentRules"; }
042
043    @Override
044    public void apply(LayoutContext context) {
045        SqlScopeResult sql = context.getSqlScope();
046        if (sql == null) return;
047        int width = indentWidth(context.getOptions().toGFmtOpt());
048        Pp2TokenStream stream = context.getStream();
049        int n = Math.min(stream.size(), sql.size());
050        for (int i = 0; i < n; i++) {
051            if (effectiveLinebreaks(context, stream, i) <= 0) continue; // not a line start
052            Pp2Token t = stream.get(i);
053            boolean clauseKeyword = t.hasRole(TokenRole.KEYWORD_MASTER)
054                || t.hasRole(TokenRole.KEYWORD_CLAUSE);
055            int level = sql.levelAt(i) + (clauseKeyword ? 0 : 1);
056            context.requestBlanksBefore(i, level * width);
057        }
058    }
059
060    private static int indentWidth(GFmtOpt opt) {
061        Integer w = opt.indentLen;
062        if (w == null || w.intValue() < 0) return 2;
063        return w.intValue();
064    }
065
066    /** The linebreaks that will precede token i (decided value, else original). */
067    private static int effectiveLinebreaks(LayoutContext ctx, Pp2TokenStream stream, int i) {
068        LayoutDecisionView d = ctx.decisionAt(i);
069        return d.isLinebreaksDecided() ? d.getLinebreaksBefore()
070            : stream.get(i).getPrecedingLinebreaks();
071    }
072}