001package gudusoft.gsqlparser.pp2.layout; 002 003import gudusoft.gsqlparser.pp2.Pp2FormatOptions; 004import gudusoft.gsqlparser.pp2.dialect.DialectStrategy; 005import gudusoft.gsqlparser.pp2.island.BlockScopeDetector.BlockScopeResult; 006import gudusoft.gsqlparser.pp2.island.ClauseScopeAnnotator.ClauseScopeResult; 007import gudusoft.gsqlparser.pp2.island.IslandScope; 008import gudusoft.gsqlparser.pp2.island.MultiWordKeywordMerger.Match; 009import gudusoft.gsqlparser.pp2.island.SqlScopeDetector.SqlScopeResult; 010import gudusoft.gsqlparser.pp2.token.Pp2TokenStream; 011 012import java.util.Collections; 013import java.util.List; 014 015/** 016 * Shared, mutable state threaded through the {@link LayoutRulePipeline}. Bundles 017 * the token stream and the upstream analyses (S18 multi-word matches, S19 block 018 * scopes, S20 SQL scopes, S21 clause membership, S22 islands), and holds the 019 * evolving per-token {@link LayoutDecision}s. 020 * 021 * <h2>How rules write decisions</h2> 022 * 023 * <p>Rules never mutate {@link gudusoft.gsqlparser.pp2.token.Pp2Token} (its 024 * {@code precedingBlanks}/{@code precedingLinebreaks} are final). Instead they 025 * call {@link #requestLinebreaksBefore}, {@link #requestBlanksBefore}, and 026 * {@link #requestIndent} with the value they want for a token's leading 027 * whitespace. Each write is mediated by the {@link LayoutConflictResolver} using 028 * the active rule's priority, which {@link LayoutRulePipeline} sets before 029 * invoking each rule. Undecided properties stay {@link LayoutDecision#UNSET} so 030 * the output writer (S31) falls back to the original whitespace — making the 031 * zero-rule pipeline an identity pass. 032 * 033 * <p>Not thread-safe; one context per format call. 034 * 035 * <p>Plan reference: §7.3/S23, §7.4/S23. 036 */ 037public final class LayoutContext { 038 039 private final Pp2TokenStream stream; 040 private final Pp2FormatOptions options; 041 private final LayoutConflictResolver resolver; 042 043 private final LayoutDecision[] decisions; 044 private final int[] lbPriority; 045 private final int[] blPriority; 046 private final int[] indPriority; 047 private final int[] txtPriority; 048 049 // Upstream analyses (nullable until populated by the renderer). 050 private List<Match> multiWordMatches = Collections.emptyList(); 051 private BlockScopeResult blockScope; 052 private SqlScopeResult sqlScope; 053 private ClauseScopeResult clauseScope; 054 private List<IslandScope> islands = Collections.emptyList(); 055 private DialectStrategy dialect; 056 057 // The priority of the rule currently applying; set by the pipeline. 058 private int activePriority = Integer.MIN_VALUE; 059 060 public LayoutContext(Pp2TokenStream stream, Pp2FormatOptions options) { 061 this(stream, options, new LayoutConflictResolver()); 062 } 063 064 public LayoutContext(Pp2TokenStream stream, Pp2FormatOptions options, 065 LayoutConflictResolver resolver) { 066 if (stream == null) throw new NullPointerException("stream"); 067 if (options == null) throw new NullPointerException("options"); 068 if (resolver == null) throw new NullPointerException("resolver"); 069 this.stream = stream; 070 this.options = options; 071 this.resolver = resolver; 072 int n = stream.size(); 073 this.decisions = new LayoutDecision[n]; 074 this.lbPriority = new int[n]; 075 this.blPriority = new int[n]; 076 this.indPriority = new int[n]; 077 this.txtPriority = new int[n]; 078 for (int i = 0; i < n; i++) { 079 decisions[i] = new LayoutDecision(); 080 lbPriority[i] = Integer.MIN_VALUE; 081 blPriority[i] = Integer.MIN_VALUE; 082 indPriority[i] = Integer.MIN_VALUE; 083 txtPriority[i] = Integer.MIN_VALUE; 084 } 085 } 086 087 // ---- core accessors ------------------------------------------------- 088 089 public Pp2TokenStream getStream() { return stream; } 090 public Pp2FormatOptions getOptions() { return options; } 091 public int size() { return decisions.length; } 092 093 /** 094 * Read-only view of the (possibly still-default) layout decision for the 095 * token at {@code index}. Writes must go through the {@code request*} 096 * methods so the {@link LayoutConflictResolver} precedence is honoured. 097 */ 098 public LayoutDecisionView decisionAt(int index) { return decisions[index]; } 099 100 // ---- analyses (fluent setters; nullable getters) -------------------- 101 102 public LayoutContext withMultiWordMatches(List<Match> m) { 103 this.multiWordMatches = m == null ? Collections.<Match>emptyList() : m; 104 return this; 105 } 106 public LayoutContext withBlockScope(BlockScopeResult b) { this.blockScope = b; return this; } 107 public LayoutContext withSqlScope(SqlScopeResult s) { this.sqlScope = s; return this; } 108 public LayoutContext withClauseScope(ClauseScopeResult c) { this.clauseScope = c; return this; } 109 public LayoutContext withIslands(List<IslandScope> i) { 110 this.islands = i == null ? Collections.<IslandScope>emptyList() : i; 111 return this; 112 } 113 public LayoutContext withDialect(DialectStrategy d) { this.dialect = d; return this; } 114 115 public List<Match> getMultiWordMatches() { return multiWordMatches; } 116 public BlockScopeResult getBlockScope() { return blockScope; } 117 public SqlScopeResult getSqlScope() { return sqlScope; } 118 public ClauseScopeResult getClauseScope() { return clauseScope; } 119 public List<IslandScope> getIslands() { return islands; } 120 public DialectStrategy getDialect() { return dialect; } 121 122 // ---- pipeline plumbing ---------------------------------------------- 123 124 /** Set by {@link LayoutRulePipeline} before each rule's {@code apply}. */ 125 void setActivePriority(int priority) { this.activePriority = priority; } 126 127 // ---- rule write API (priority-mediated) ----------------------------- 128 129 /** Request {@code count} linebreaks before the token at {@code index}. */ 130 public void requestLinebreaksBefore(int index, int count) { 131 checkValue(count); 132 if (resolver.accept(activePriority, lbPriority[index])) { 133 decisions[index].setLinebreaksBefore(count); 134 lbPriority[index] = activePriority; 135 } 136 } 137 138 /** Request {@code count} blank columns before the token at {@code index}. */ 139 public void requestBlanksBefore(int index, int count) { 140 checkValue(count); 141 if (resolver.accept(activePriority, blPriority[index])) { 142 decisions[index].setBlanksBefore(count); 143 blPriority[index] = activePriority; 144 } 145 } 146 147 /** Request indent {@code level} for the token at {@code index}. */ 148 public void requestIndent(int index, int level) { 149 checkValue(level); 150 if (resolver.accept(activePriority, indPriority[index])) { 151 decisions[index].setIndentLevel(level); 152 indPriority[index] = activePriority; 153 } 154 } 155 156 /** 157 * Request that the token at {@code index} be emitted as {@code text} instead 158 * of its source text (e.g. keyword case modification). The wrapped 159 * {@code TSourceToken} is never changed; this is a render-time override. 160 */ 161 public void requestText(int index, String text) { 162 if (text == null) throw new NullPointerException("text"); 163 if (resolver.accept(activePriority, txtPriority[index])) { 164 decisions[index].setTextOverride(text); 165 txtPriority[index] = activePriority; 166 } 167 } 168 169 private static void checkValue(int v) { 170 if (v < 0) throw new IllegalArgumentException("value must be >= 0: " + v); 171 } 172}