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}