@tablecraft-app/plugin-api 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +46 -2
- package/dist/catalog.d.ts +112 -0
- package/dist/catalog.js +5 -0
- package/dist/completion.d.ts +109 -0
- package/dist/completion.js +12 -0
- package/dist/errors.d.ts +58 -0
- package/dist/errors.js +106 -0
- package/dist/explain.d.ts +58 -0
- package/dist/explain.js +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/inspection.d.ts +38 -0
- package/dist/inspection.js +21 -0
- package/dist/manifest.d.ts +186 -0
- package/dist/manifest.js +176 -0
- package/dist/objects.d.ts +140 -0
- package/dist/objects.js +2 -0
- package/dist/plugin.d.ts +460 -0
- package/dist/plugin.js +49 -0
- package/dist/sessions.d.ts +59 -0
- package/dist/sessions.js +2 -0
- package/dist/sql/classify.d.ts +19 -0
- package/dist/sql/classify.js +95 -0
- package/dist/sql/complete.d.ts +23 -0
- package/dist/sql/complete.js +480 -0
- package/dist/sql/explain.d.ts +42 -0
- package/dist/sql/explain.js +108 -0
- package/dist/sql/format.d.ts +26 -0
- package/dist/sql/format.js +117 -0
- package/dist/sql/functions.d.ts +47 -0
- package/dist/sql/functions.js +159 -0
- package/dist/sql/generate.d.ts +22 -0
- package/dist/sql/generate.js +64 -0
- package/dist/sql/index.d.ts +16 -0
- package/dist/sql/index.js +16 -0
- package/dist/sql/inspect.d.ts +10 -0
- package/dist/sql/inspect.js +134 -0
- package/dist/sql/inspectContext.d.ts +78 -0
- package/dist/sql/inspectContext.js +251 -0
- package/dist/sql/inspectResolve.d.ts +6 -0
- package/dist/sql/inspectResolve.js +391 -0
- package/dist/sql/inspectStyle.d.ts +3 -0
- package/dist/sql/inspectStyle.js +384 -0
- package/dist/sql/inspectSyntax.d.ts +10 -0
- package/dist/sql/inspectSyntax.js +179 -0
- package/dist/sql/layout.d.ts +8 -0
- package/dist/sql/layout.js +114 -0
- package/dist/sql/layoutNodes.d.ts +46 -0
- package/dist/sql/layoutNodes.js +317 -0
- package/dist/sql/layoutPrint.d.ts +60 -0
- package/dist/sql/layoutPrint.js +806 -0
- package/dist/sql/layoutWriter.d.ts +21 -0
- package/dist/sql/layoutWriter.js +77 -0
- package/dist/sql/literals.d.ts +28 -0
- package/dist/sql/literals.js +78 -0
- package/dist/sql/namespace.d.ts +41 -0
- package/dist/sql/namespace.js +220 -0
- package/dist/sql/objects.d.ts +42 -0
- package/dist/sql/objects.js +222 -0
- package/dist/sql/parameters.d.ts +20 -0
- package/dist/sql/parameters.js +88 -0
- package/dist/sql/resolution.d.ts +73 -0
- package/dist/sql/resolution.js +192 -0
- package/dist/sql/routines.d.ts +27 -0
- package/dist/sql/routines.js +42 -0
- package/dist/sql/scope.d.ts +117 -0
- package/dist/sql/scope.js +540 -0
- package/dist/sql/select.d.ts +41 -0
- package/dist/sql/select.js +287 -0
- package/dist/sql/signature.d.ts +18 -0
- package/dist/sql/signature.js +137 -0
- package/dist/sql/splitter.d.ts +15 -0
- package/dist/sql/splitter.js +217 -0
- package/dist/sql/suggest.d.ts +6 -0
- package/dist/sql/suggest.js +36 -0
- package/dist/sql/tokens.d.ts +62 -0
- package/dist/sql/tokens.js +227 -0
- package/dist/sql/words.d.ts +23 -0
- package/dist/sql/words.js +72 -0
- package/dist/testing/index.d.ts +177 -0
- package/dist/testing/index.js +2070 -0
- package/dist/tls.d.ts +33 -0
- package/dist/tls.js +118 -0
- package/dist/url.d.ts +52 -0
- package/dist/url.js +219 -0
- package/dist/values.d.ts +44 -0
- package/dist/values.js +44 -0
- package/dist/version.d.ts +22 -0
- package/dist/version.js +29 -0
- package/package.json +42 -4
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { sqlSplitter } from "./splitter.js";
|
|
2
|
+
import { Lexer } from "./tokens.js";
|
|
3
|
+
import { isLineComment, parseStatement, Unsupported } from "./layoutNodes.js";
|
|
4
|
+
import { AlignedPrinter } from "./layoutPrint.js";
|
|
5
|
+
export { ALIAS_ALIGN_LIMIT, CASE_INLINE_LIMIT, RIGHT_MARGIN } from "./layoutPrint.js";
|
|
6
|
+
const LINE_BREAK = "\n";
|
|
7
|
+
const DELIMITER = ";";
|
|
8
|
+
const SPACE = " ";
|
|
9
|
+
/** The statements of `text` with the comments around them, so none is lost between statements. */
|
|
10
|
+
function segment(text, dialect) {
|
|
11
|
+
const lexer = new Lexer(text, dialect);
|
|
12
|
+
const pieces = [];
|
|
13
|
+
let cursor = 0;
|
|
14
|
+
for (const span of sqlSplitter(dialect)(text)) {
|
|
15
|
+
lexer.position = cursor;
|
|
16
|
+
let start = span.from;
|
|
17
|
+
for (let token = lexer.next(); token !== null && token.from < span.from; token = lexer.next()) {
|
|
18
|
+
if (token.kind === "comment") {
|
|
19
|
+
start = token.from;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
lexer.position = span.to;
|
|
24
|
+
let end = span.to;
|
|
25
|
+
let delimited = false;
|
|
26
|
+
for (let token = lexer.next(); token !== null; token = lexer.next()) {
|
|
27
|
+
if (token.kind === "comment") {
|
|
28
|
+
end = token.to;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
delimited = token.kind === "delimiter";
|
|
32
|
+
cursor = delimited ? token.to : end;
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (!delimited)
|
|
36
|
+
cursor = end;
|
|
37
|
+
const after = [];
|
|
38
|
+
if (delimited) {
|
|
39
|
+
lexer.position = cursor;
|
|
40
|
+
for (let token = lexer.next(); token !== null && token.kind === "comment"; token = lexer.next()) {
|
|
41
|
+
if (text.slice(cursor, token.from).includes(LINE_BREAK))
|
|
42
|
+
break;
|
|
43
|
+
after.push(text.slice(token.from, token.to));
|
|
44
|
+
cursor = token.to;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
pieces.push({ text: text.slice(start, end), delimited, after });
|
|
48
|
+
}
|
|
49
|
+
const tail = [];
|
|
50
|
+
lexer.position = cursor;
|
|
51
|
+
for (let token = lexer.next(); token !== null; token = lexer.next()) {
|
|
52
|
+
if (token.kind === "comment")
|
|
53
|
+
tail.push(text.slice(token.from, token.to));
|
|
54
|
+
}
|
|
55
|
+
return { pieces, tail };
|
|
56
|
+
}
|
|
57
|
+
function tokenStream(text, dialect) {
|
|
58
|
+
const lexer = new Lexer(text, dialect);
|
|
59
|
+
const stream = [];
|
|
60
|
+
for (let token = lexer.next(); token !== null; token = lexer.next()) {
|
|
61
|
+
const content = text.slice(token.from, token.to);
|
|
62
|
+
stream.push(token.kind === "word" ? content.toUpperCase() : content);
|
|
63
|
+
}
|
|
64
|
+
return stream;
|
|
65
|
+
}
|
|
66
|
+
function sameTokens(before, after, dialect) {
|
|
67
|
+
const expected = tokenStream(before, dialect);
|
|
68
|
+
const actual = tokenStream(after, dialect);
|
|
69
|
+
return expected.length === actual.length && expected.every((token, index) => token === actual[index]);
|
|
70
|
+
}
|
|
71
|
+
function lastIsLineComment(text, dialect) {
|
|
72
|
+
const lexer = new Lexer(text, dialect);
|
|
73
|
+
let last = null;
|
|
74
|
+
for (let token = lexer.next(); token !== null; token = lexer.next()) {
|
|
75
|
+
last = token.kind === "comment" ? text.slice(token.from, token.to) : null;
|
|
76
|
+
}
|
|
77
|
+
return last !== null && isLineComment(last);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The aligned layout (decision 061), statement by statement. A statement it does not read, or one
|
|
81
|
+
* whose tokens it would change, goes to `standard` as written (decision 062).
|
|
82
|
+
*/
|
|
83
|
+
export function formatAligned(text, format, dialect, standard) {
|
|
84
|
+
const { pieces, tail } = segment(text, dialect);
|
|
85
|
+
if (pieces.length === 0)
|
|
86
|
+
return standard(text);
|
|
87
|
+
const printer = new AlignedPrinter(format);
|
|
88
|
+
const statement = (sql) => {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = parseStatement(sql, dialect);
|
|
91
|
+
const printed = printer.print(parsed.nodes, parsed.tail);
|
|
92
|
+
if (!sameTokens(sql, printed.text, dialect))
|
|
93
|
+
throw new Unsupported("the layout would change a token");
|
|
94
|
+
return printed;
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
if (!(cause instanceof Unsupported))
|
|
98
|
+
throw cause;
|
|
99
|
+
const fallback = standard(sql);
|
|
100
|
+
return { text: fallback, endsInLineComment: lastIsLineComment(fallback, dialect) };
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
const blocks = pieces.map((piece) => {
|
|
104
|
+
const formatted = statement(piece.text);
|
|
105
|
+
let block = formatted.text;
|
|
106
|
+
if (piece.delimited)
|
|
107
|
+
block += formatted.endsInLineComment ? LINE_BREAK + DELIMITER : DELIMITER;
|
|
108
|
+
for (const comment of piece.after)
|
|
109
|
+
block += SPACE + comment;
|
|
110
|
+
return block;
|
|
111
|
+
});
|
|
112
|
+
const joined = blocks.join(LINE_BREAK.repeat(format.linesBetweenStatements + 1));
|
|
113
|
+
return tail.length === 0 ? joined : [joined, ...tail].join(LINE_BREAK);
|
|
114
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type SqlDialectOptions } from "./tokens.js";
|
|
2
|
+
/** Raised for SQL the aligned layout does not read; the statement then goes to `sql-formatter` (decision 062). */
|
|
3
|
+
export declare class Unsupported extends Error {
|
|
4
|
+
}
|
|
5
|
+
export interface Tok {
|
|
6
|
+
readonly type: "tok";
|
|
7
|
+
readonly kind: "word" | "quoted" | "string" | "symbol";
|
|
8
|
+
readonly text: string;
|
|
9
|
+
readonly upper: string;
|
|
10
|
+
/** Whitespace or a comment stood between it and the token before it in the source. */
|
|
11
|
+
readonly spaced: boolean;
|
|
12
|
+
/** Read as a keyword: cased by `keywordCase`, and a `(` after it gets a space. */
|
|
13
|
+
keyword: boolean;
|
|
14
|
+
/** Comments on their own lines before it. */
|
|
15
|
+
readonly leading: string[];
|
|
16
|
+
/** Comments after it on its line. */
|
|
17
|
+
readonly trailing: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface Group {
|
|
20
|
+
readonly type: "group";
|
|
21
|
+
readonly open: Tok;
|
|
22
|
+
readonly children: Node[];
|
|
23
|
+
readonly close: Tok;
|
|
24
|
+
}
|
|
25
|
+
/** `CASE … END`, a node so its `WHEN a AND b` never splits a condition list. */
|
|
26
|
+
export interface CaseNode {
|
|
27
|
+
readonly type: "case";
|
|
28
|
+
readonly head: Tok;
|
|
29
|
+
readonly children: Node[];
|
|
30
|
+
readonly end: Tok;
|
|
31
|
+
}
|
|
32
|
+
export type Node = Tok | Group | CaseNode;
|
|
33
|
+
export interface Statement {
|
|
34
|
+
readonly nodes: Node[];
|
|
35
|
+
/** Comments on their own lines after the last token. */
|
|
36
|
+
readonly tail: string[];
|
|
37
|
+
}
|
|
38
|
+
export declare function isLineComment(comment: string): boolean;
|
|
39
|
+
/** The statement as tokens nested by parentheses and `CASE … END`; throws `Unsupported` when they do not balance. */
|
|
40
|
+
export declare function parseStatement(text: string, dialect: SqlDialectOptions): Statement;
|
|
41
|
+
export declare function firstTok(node: Node): Tok;
|
|
42
|
+
export declare function lastTok(node: Node): Tok;
|
|
43
|
+
/** `node` is the keyword `word`. */
|
|
44
|
+
export declare function isWord(node: Node | undefined, word: string): node is Tok;
|
|
45
|
+
/** `node` is a word spelled `word`, keyword or not (a clause the layout reads structurally). */
|
|
46
|
+
export declare function spells(node: Node | undefined, word: string): node is Tok;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { Lexer } from "./tokens.js";
|
|
2
|
+
/** Raised for SQL the aligned layout does not read; the statement then goes to `sql-formatter` (decision 062). */
|
|
3
|
+
export class Unsupported extends Error {
|
|
4
|
+
}
|
|
5
|
+
const OPERATOR_CHARS = new Set("+-*/<>=~!@#%^&|`?:");
|
|
6
|
+
const OPEN = "(";
|
|
7
|
+
const CLOSE = ")";
|
|
8
|
+
const COMMA = ",";
|
|
9
|
+
const DOT = ".";
|
|
10
|
+
const CAST = "::";
|
|
11
|
+
const CASE = "CASE";
|
|
12
|
+
const END = "END";
|
|
13
|
+
const LINE_BREAK = "\n";
|
|
14
|
+
/** Reserved in PostgreSQL or MySQL, so never a bare column or table name worth keeping in its case. */
|
|
15
|
+
const RESERVED = new Set([
|
|
16
|
+
"ALL",
|
|
17
|
+
"AND",
|
|
18
|
+
"ANY",
|
|
19
|
+
"AS",
|
|
20
|
+
"ASC",
|
|
21
|
+
"BETWEEN",
|
|
22
|
+
"BY",
|
|
23
|
+
"CASE",
|
|
24
|
+
"CROSS",
|
|
25
|
+
"DEFAULT",
|
|
26
|
+
"DELETE",
|
|
27
|
+
"DESC",
|
|
28
|
+
"DISTINCT",
|
|
29
|
+
"DISTINCTROW",
|
|
30
|
+
"DIV",
|
|
31
|
+
"ELSE",
|
|
32
|
+
"END",
|
|
33
|
+
"ESCAPE",
|
|
34
|
+
"EXCEPT",
|
|
35
|
+
"EXISTS",
|
|
36
|
+
"FALSE",
|
|
37
|
+
"FETCH",
|
|
38
|
+
"FOR",
|
|
39
|
+
"FROM",
|
|
40
|
+
"FULL",
|
|
41
|
+
"GROUP",
|
|
42
|
+
"HAVING",
|
|
43
|
+
"HIGH_PRIORITY",
|
|
44
|
+
"IGNORE",
|
|
45
|
+
"ILIKE",
|
|
46
|
+
"IN",
|
|
47
|
+
"INNER",
|
|
48
|
+
"INSERT",
|
|
49
|
+
"INTERSECT",
|
|
50
|
+
"INTERVAL",
|
|
51
|
+
"INTO",
|
|
52
|
+
"IS",
|
|
53
|
+
"ISNULL",
|
|
54
|
+
"JOIN",
|
|
55
|
+
"LATERAL",
|
|
56
|
+
"LEFT",
|
|
57
|
+
"LIKE",
|
|
58
|
+
"LIMIT",
|
|
59
|
+
"LOW_PRIORITY",
|
|
60
|
+
"NATURAL",
|
|
61
|
+
"NOT",
|
|
62
|
+
"NOTNULL",
|
|
63
|
+
"NULL",
|
|
64
|
+
"OFFSET",
|
|
65
|
+
"ON",
|
|
66
|
+
"OR",
|
|
67
|
+
"ORDER",
|
|
68
|
+
"OUTER",
|
|
69
|
+
"OVER",
|
|
70
|
+
"PARTITION",
|
|
71
|
+
"RECURSIVE",
|
|
72
|
+
"REGEXP",
|
|
73
|
+
"RETURNING",
|
|
74
|
+
"RIGHT",
|
|
75
|
+
"RLIKE",
|
|
76
|
+
"SELECT",
|
|
77
|
+
"SET",
|
|
78
|
+
"SIMILAR",
|
|
79
|
+
"SOME",
|
|
80
|
+
"SQL_BIG_RESULT",
|
|
81
|
+
"SQL_BUFFER_RESULT",
|
|
82
|
+
"SQL_CACHE",
|
|
83
|
+
"SQL_CALC_FOUND_ROWS",
|
|
84
|
+
"SQL_NO_CACHE",
|
|
85
|
+
"SQL_SMALL_RESULT",
|
|
86
|
+
"STRAIGHT_JOIN",
|
|
87
|
+
"TABLESAMPLE",
|
|
88
|
+
"THEN",
|
|
89
|
+
"TRUE",
|
|
90
|
+
"UNION",
|
|
91
|
+
"UPDATE",
|
|
92
|
+
"USING",
|
|
93
|
+
"VALUES",
|
|
94
|
+
"WHEN",
|
|
95
|
+
"WHERE",
|
|
96
|
+
"WINDOW",
|
|
97
|
+
"WITH",
|
|
98
|
+
"XOR",
|
|
99
|
+
]);
|
|
100
|
+
/** Keywords that are also functions; glued to a `(` as written they are the function, cased as written. */
|
|
101
|
+
const FUNCTION_KEYWORDS = new Set(["DEFAULT", "INSERT", "INTERVAL", "LEFT", "RIGHT", "VALUES"]);
|
|
102
|
+
/** Pairs where the second word is a keyword only after the first (`USE INDEX`, `NULLS LAST`). */
|
|
103
|
+
const CONTEXTUAL_PAIRS = [
|
|
104
|
+
[new Set(["USE", "FORCE", "IGNORE"]), new Set(["INDEX", "KEY"])],
|
|
105
|
+
[new Set(["ON"]), new Set(["DUPLICATE", "CONFLICT", "CONSTRAINT"])],
|
|
106
|
+
[new Set(["DUPLICATE"]), new Set(["KEY"])],
|
|
107
|
+
[new Set(["DO"]), new Set(["NOTHING", "UPDATE"])],
|
|
108
|
+
[new Set(["NULLS"]), new Set(["FIRST", "LAST"])],
|
|
109
|
+
[new Set(["LOCK"]), new Set(["IN"])],
|
|
110
|
+
[new Set(["SHARE"]), new Set(["MODE"])],
|
|
111
|
+
[new Set(["FOR"]), new Set(["SHARE", "NO", "KEY"])],
|
|
112
|
+
[new Set(["NO"]), new Set(["KEY"])],
|
|
113
|
+
[new Set(["KEY"]), new Set(["SHARE", "UPDATE"])],
|
|
114
|
+
[new Set(["SKIP"]), new Set(["LOCKED"])],
|
|
115
|
+
[new Set(["FETCH"]), new Set(["FIRST", "NEXT"])],
|
|
116
|
+
[new Set(["ROW", "ROWS"]), new Set(["ONLY"])],
|
|
117
|
+
[new Set(["WITH"]), new Set(["TIES"])],
|
|
118
|
+
[new Set(["FROM", "JOIN", "UPDATE"]), new Set(["ONLY"])],
|
|
119
|
+
[new Set(["AS", "NOT"]), new Set(["MATERIALIZED"])],
|
|
120
|
+
[new Set(["UPDATE", "SHARE"]), new Set(["NOWAIT"])],
|
|
121
|
+
];
|
|
122
|
+
/** The first word of a pair that is a keyword itself once its second word follows. */
|
|
123
|
+
const CONTEXTUAL_LEADS = new Set(["USE", "FORCE", "DUPLICATE", "DO", "NULLS", "LOCK", "SHARE", "NO", "KEY", "SKIP"]);
|
|
124
|
+
/** Inside `OVER ( … )`: the frame clause. */
|
|
125
|
+
const WINDOW_WORDS = new Set([
|
|
126
|
+
"ROWS",
|
|
127
|
+
"RANGE",
|
|
128
|
+
"GROUPS",
|
|
129
|
+
"UNBOUNDED",
|
|
130
|
+
"PRECEDING",
|
|
131
|
+
"FOLLOWING",
|
|
132
|
+
"CURRENT",
|
|
133
|
+
"ROW",
|
|
134
|
+
"EXCLUDE",
|
|
135
|
+
"TIES",
|
|
136
|
+
"OTHERS",
|
|
137
|
+
"NO",
|
|
138
|
+
]);
|
|
139
|
+
/** Operator characters next to each other are one operator, so the layout never puts a space inside `>=` or `->>`. */
|
|
140
|
+
function mergeOperators(text, tokens) {
|
|
141
|
+
const merged = [];
|
|
142
|
+
for (const token of tokens) {
|
|
143
|
+
const last = merged.at(-1);
|
|
144
|
+
if (last !== undefined &&
|
|
145
|
+
token.kind === "symbol" &&
|
|
146
|
+
last.kind === "symbol" &&
|
|
147
|
+
last.to === token.from &&
|
|
148
|
+
OPERATOR_CHARS.has(text.charAt(token.from)) &&
|
|
149
|
+
OPERATOR_CHARS.has(text.charAt(last.from))) {
|
|
150
|
+
merged[merged.length - 1] = { kind: "symbol", from: last.from, to: token.to };
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
merged.push(token);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return merged;
|
|
157
|
+
}
|
|
158
|
+
export function isLineComment(comment) {
|
|
159
|
+
return !comment.startsWith("/*");
|
|
160
|
+
}
|
|
161
|
+
function flatten(text, dialect) {
|
|
162
|
+
const lexer = new Lexer(text, dialect);
|
|
163
|
+
const significant = [];
|
|
164
|
+
const comments = [];
|
|
165
|
+
for (let token = lexer.next(); token !== null; token = lexer.next()) {
|
|
166
|
+
if (token.kind === "delimiter")
|
|
167
|
+
throw new Unsupported("a statement holding a delimiter");
|
|
168
|
+
(token.kind === "comment" ? comments : significant).push(token);
|
|
169
|
+
}
|
|
170
|
+
const tokens = mergeOperators(text, significant);
|
|
171
|
+
const flat = tokens.map((token, index) => {
|
|
172
|
+
const before = tokens[index - 1];
|
|
173
|
+
return {
|
|
174
|
+
token,
|
|
175
|
+
text: text.slice(token.from, token.to),
|
|
176
|
+
spaced: before === undefined || before.to < token.from,
|
|
177
|
+
leading: [],
|
|
178
|
+
trailing: [],
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
const tail = [];
|
|
182
|
+
let next = 0;
|
|
183
|
+
for (const comment of comments) {
|
|
184
|
+
while (next < flat.length && (flat[next]?.token.from ?? 0) < comment.from)
|
|
185
|
+
next += 1;
|
|
186
|
+
const content = text.slice(comment.from, comment.to);
|
|
187
|
+
const previous = flat[next - 1];
|
|
188
|
+
if (previous !== undefined && !text.slice(previous.token.to, comment.from).includes(LINE_BREAK)) {
|
|
189
|
+
previous.trailing.push(content);
|
|
190
|
+
}
|
|
191
|
+
else if (next < flat.length) {
|
|
192
|
+
flat[next]?.leading.push(content);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
tail.push(content);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
moveCommentsPastCommas(flat);
|
|
199
|
+
return { flat, tail };
|
|
200
|
+
}
|
|
201
|
+
/** `a -- note` then `, b` reads as `a, -- note`; a comment on its own line before a comma goes after it. */
|
|
202
|
+
function moveCommentsPastCommas(flat) {
|
|
203
|
+
flat.forEach((entry, index) => {
|
|
204
|
+
if (entry.text !== COMMA)
|
|
205
|
+
return;
|
|
206
|
+
const previous = flat[index - 1];
|
|
207
|
+
if (previous !== undefined && previous.trailing.some(isLineComment)) {
|
|
208
|
+
entry.trailing.unshift(...previous.trailing.splice(0));
|
|
209
|
+
}
|
|
210
|
+
const following = flat[index + 1];
|
|
211
|
+
if (following !== undefined && entry.leading.length > 0)
|
|
212
|
+
following.leading.unshift(...entry.leading.splice(0));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function keywordFlags(flat) {
|
|
216
|
+
const uppers = flat.map((entry) => (entry.token.kind === "word" ? entry.text.toUpperCase() : ""));
|
|
217
|
+
const flags = flat.map((entry, index) => {
|
|
218
|
+
const upper = uppers[index] ?? "";
|
|
219
|
+
if (!RESERVED.has(upper))
|
|
220
|
+
return false;
|
|
221
|
+
const previous = flat[index - 1];
|
|
222
|
+
if (previous?.text === CAST || (previous?.text === DOT && !entry.spaced))
|
|
223
|
+
return false;
|
|
224
|
+
const next = flat[index + 1];
|
|
225
|
+
return !(FUNCTION_KEYWORDS.has(upper) && next?.text === OPEN && !next.spaced);
|
|
226
|
+
});
|
|
227
|
+
uppers.forEach((upper, index) => {
|
|
228
|
+
const next = uppers[index + 1] ?? "";
|
|
229
|
+
for (const [first, second] of CONTEXTUAL_PAIRS) {
|
|
230
|
+
if (!first.has(upper) || !second.has(next))
|
|
231
|
+
continue;
|
|
232
|
+
flags[index + 1] = true;
|
|
233
|
+
if (CONTEXTUAL_LEADS.has(upper))
|
|
234
|
+
flags[index] = true;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
const overDepths = [];
|
|
238
|
+
let depth = 0;
|
|
239
|
+
flat.forEach((entry, index) => {
|
|
240
|
+
if (entry.text === OPEN) {
|
|
241
|
+
depth += 1;
|
|
242
|
+
if (uppers[index - 1] === "OVER")
|
|
243
|
+
overDepths.push(depth);
|
|
244
|
+
}
|
|
245
|
+
else if (entry.text === CLOSE) {
|
|
246
|
+
if (overDepths.at(-1) === depth)
|
|
247
|
+
overDepths.pop();
|
|
248
|
+
depth -= 1;
|
|
249
|
+
}
|
|
250
|
+
else if (overDepths.at(-1) === depth && WINDOW_WORDS.has(uppers[index] ?? "")) {
|
|
251
|
+
flags[index] = true;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return flags;
|
|
255
|
+
}
|
|
256
|
+
/** The statement as tokens nested by parentheses and `CASE … END`; throws `Unsupported` when they do not balance. */
|
|
257
|
+
export function parseStatement(text, dialect) {
|
|
258
|
+
const { flat, tail } = flatten(text, dialect);
|
|
259
|
+
const flags = keywordFlags(flat);
|
|
260
|
+
const stack = [{ opener: null, kind: "root", children: [] }];
|
|
261
|
+
const top = () => stack[stack.length - 1] ?? { opener: null, kind: "root", children: [] };
|
|
262
|
+
flat.forEach((entry, index) => {
|
|
263
|
+
const kind = entry.token.kind === "comment" || entry.token.kind === "delimiter" ? "symbol" : entry.token.kind;
|
|
264
|
+
const tok = {
|
|
265
|
+
type: "tok",
|
|
266
|
+
kind,
|
|
267
|
+
text: entry.text,
|
|
268
|
+
upper: entry.text.toUpperCase(),
|
|
269
|
+
spaced: entry.spaced,
|
|
270
|
+
keyword: flags[index] ?? false,
|
|
271
|
+
leading: entry.leading,
|
|
272
|
+
trailing: entry.trailing,
|
|
273
|
+
};
|
|
274
|
+
if (tok.text === OPEN) {
|
|
275
|
+
stack.push({ opener: tok, kind: "group", children: [] });
|
|
276
|
+
}
|
|
277
|
+
else if (tok.text === CLOSE) {
|
|
278
|
+
const frame = stack.pop();
|
|
279
|
+
if (frame?.kind !== "group" || frame.opener === null)
|
|
280
|
+
throw new Unsupported("an unbalanced parenthesis");
|
|
281
|
+
top().children.push({ type: "group", open: frame.opener, children: frame.children, close: tok });
|
|
282
|
+
}
|
|
283
|
+
else if (tok.keyword && tok.upper === CASE) {
|
|
284
|
+
stack.push({ opener: tok, kind: "case", children: [] });
|
|
285
|
+
}
|
|
286
|
+
else if (tok.keyword && tok.upper === END) {
|
|
287
|
+
const frame = stack.pop();
|
|
288
|
+
if (frame?.kind !== "case" || frame.opener === null)
|
|
289
|
+
throw new Unsupported("an END without its CASE");
|
|
290
|
+
top().children.push({ type: "case", head: frame.opener, children: frame.children, end: tok });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
top().children.push(tok);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
if (stack.length !== 1)
|
|
297
|
+
throw new Unsupported("an unclosed parenthesis or CASE");
|
|
298
|
+
return { nodes: top().children, tail };
|
|
299
|
+
}
|
|
300
|
+
export function firstTok(node) {
|
|
301
|
+
if (node.type === "tok")
|
|
302
|
+
return node;
|
|
303
|
+
return node.type === "group" ? node.open : node.head;
|
|
304
|
+
}
|
|
305
|
+
export function lastTok(node) {
|
|
306
|
+
if (node.type === "tok")
|
|
307
|
+
return node;
|
|
308
|
+
return node.type === "group" ? node.close : node.end;
|
|
309
|
+
}
|
|
310
|
+
/** `node` is the keyword `word`. */
|
|
311
|
+
export function isWord(node, word) {
|
|
312
|
+
return node?.type === "tok" && node.keyword && node.upper === word;
|
|
313
|
+
}
|
|
314
|
+
/** `node` is a word spelled `word`, keyword or not (a clause the layout reads structurally). */
|
|
315
|
+
export function spells(node, word) {
|
|
316
|
+
return node?.type === "tok" && node.kind === "word" && node.upper === word;
|
|
317
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FormatOptions } from "@tablecraft-app/plugin-api";
|
|
2
|
+
import { type Node, type Tok } from "./layoutNodes.js";
|
|
3
|
+
import { Writer } from "./layoutWriter.js";
|
|
4
|
+
/** Past this width an aliased expression neither pads to the alias column nor pushes it right. */
|
|
5
|
+
export declare const ALIAS_ALIGN_LIMIT = 80;
|
|
6
|
+
/** A `CASE` up to this width stays on one line. */
|
|
7
|
+
export declare const CASE_INLINE_LIMIT = 60;
|
|
8
|
+
/** A parenthesised condition group ending past this column opens one condition per line (DataGrip's margin). */
|
|
9
|
+
export declare const RIGHT_MARGIN = 120;
|
|
10
|
+
interface Clause {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly keywords: Tok[];
|
|
13
|
+
readonly body: Node[];
|
|
14
|
+
}
|
|
15
|
+
interface InlineOptions {
|
|
16
|
+
/** The source's own spacing between every two tokens (the `EXPLAIN` prefix, where `FORMAT=JSON` stays). */
|
|
17
|
+
readonly preserve?: boolean;
|
|
18
|
+
/** A `(` after a name gets a space (`INSERT INTO t (a, b)`, `WITH x (a) AS`). */
|
|
19
|
+
readonly spaceBeforeGroup?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** The aligned layout of one statement (decision 061). Throws `Unsupported` for what it does not read. */
|
|
22
|
+
export declare class AlignedPrinter {
|
|
23
|
+
#private;
|
|
24
|
+
readonly options: FormatOptions;
|
|
25
|
+
constructor(options: FormatOptions);
|
|
26
|
+
text(tok: Tok): string;
|
|
27
|
+
print(nodes: readonly Node[], tail: readonly string[]): {
|
|
28
|
+
text: string;
|
|
29
|
+
endsInLineComment: boolean;
|
|
30
|
+
};
|
|
31
|
+
statement(writer: Writer, nodes: readonly Node[], base: number): void;
|
|
32
|
+
withQuery(writer: Writer, nodes: readonly Node[], base: number): void;
|
|
33
|
+
setQuery(writer: Writer, nodes: readonly Node[], base: number): void;
|
|
34
|
+
select(writer: Writer, nodes: readonly Node[], base: number): void;
|
|
35
|
+
clause(writer: Writer, clause: Clause): void;
|
|
36
|
+
selectClause(writer: Writer, clause: Clause): void;
|
|
37
|
+
/** One item per line under the first; aliases after `AS` line up (rule 5). */
|
|
38
|
+
selectList(writer: Writer, nodes: readonly Node[], column: number): void;
|
|
39
|
+
measure(column: number, render: (writer: Writer) => void): {
|
|
40
|
+
width: number;
|
|
41
|
+
multiline: boolean;
|
|
42
|
+
};
|
|
43
|
+
/** `SET a = 1,` with each further assignment under the first and the `=` lined up like aliases. */
|
|
44
|
+
assignments(writer: Writer, nodes: readonly Node[], column: number): void;
|
|
45
|
+
/** Tables one per line under the first; each join on its own line at the same column (rules 7-8). */
|
|
46
|
+
fromItems(writer: Writer, nodes: readonly Node[], column: number): void;
|
|
47
|
+
/**
|
|
48
|
+
* The first condition on the keyword's line, each further `AND` / `OR` on its own line ending
|
|
49
|
+
* where the keyword ends, or starting under it when the keyword is shorter (rule 9).
|
|
50
|
+
*/
|
|
51
|
+
conditions(writer: Writer, nodes: readonly Node[], keywordStart: number, keywordEnd: number): void;
|
|
52
|
+
/** A parenthesised group of conditions too wide for its line opens one condition per line (rule 10). */
|
|
53
|
+
condition(writer: Writer, nodes: readonly Node[], anchor: number, leadSpace: boolean): void;
|
|
54
|
+
insert(writer: Writer, nodes: readonly Node[], base: number): void;
|
|
55
|
+
update(writer: Writer, nodes: readonly Node[], base: number): void;
|
|
56
|
+
delete(writer: Writer, nodes: readonly Node[], base: number): void;
|
|
57
|
+
inline(writer: Writer, nodes: readonly Node[], leadSpace: boolean, options?: InlineOptions): void;
|
|
58
|
+
node(writer: Writer, node: Node, space: boolean, options?: InlineOptions): void;
|
|
59
|
+
}
|
|
60
|
+
export {};
|