@xano/xanoscript-language-server 11.8.3 → 11.8.5

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.
Files changed (58) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/cache/documentCache.js +58 -10
  3. package/lexer/db.js +9 -1
  4. package/lexer/security.js +16 -0
  5. package/onCompletion/onCompletion.js +61 -1
  6. package/onDefinition/onDefinition.js +150 -0
  7. package/onDefinition/onDefinition.spec.js +313 -0
  8. package/onDidChangeContent/onDidChangeContent.js +52 -5
  9. package/onHover/functions.md +28 -0
  10. package/package.json +1 -1
  11. package/parser/base_parser.js +61 -3
  12. package/parser/clauses/middlewareClause.js +16 -0
  13. package/parser/definitions/columnDefinition.js +5 -0
  14. package/parser/functions/api/apiCallFn.js +5 -3
  15. package/parser/functions/controls/functionCallFn.js +5 -3
  16. package/parser/functions/controls/functionRunFn.js +61 -5
  17. package/parser/functions/controls/taskCallFn.js +5 -3
  18. package/parser/functions/db/captureFieldName.js +63 -0
  19. package/parser/functions/db/dbAddFn.js +5 -3
  20. package/parser/functions/db/dbAddOrEditFn.js +13 -3
  21. package/parser/functions/db/dbBulkAddFn.js +5 -3
  22. package/parser/functions/db/dbBulkDeleteFn.js +5 -3
  23. package/parser/functions/db/dbBulkPatchFn.js +5 -3
  24. package/parser/functions/db/dbBulkUpdateFn.js +5 -3
  25. package/parser/functions/db/dbDelFn.js +10 -3
  26. package/parser/functions/db/dbEditFn.js +13 -3
  27. package/parser/functions/db/dbGetFn.js +10 -3
  28. package/parser/functions/db/dbHasFn.js +9 -3
  29. package/parser/functions/db/dbPatchFn.js +10 -3
  30. package/parser/functions/db/dbQueryFn.js +29 -3
  31. package/parser/functions/db/dbSchemaFn.js +5 -3
  32. package/parser/functions/db/dbTruncateFn.js +5 -3
  33. package/parser/functions/middlewareCallFn.js +3 -1
  34. package/parser/functions/security/register.js +19 -9
  35. package/parser/functions/security/securityCreateAuthTokenFn.js +22 -0
  36. package/parser/functions/security/securityJweDecodeLegacyFn.js +24 -0
  37. package/parser/functions/security/securityJweDecodeLegacyFn.spec.js +26 -0
  38. package/parser/functions/security/securityJweEncodeLegacyFn.js +24 -0
  39. package/parser/functions/security/securityJweEncodeLegacyFn.spec.js +25 -0
  40. package/parser/functions/securityFn.js +2 -0
  41. package/parser/functions/varFn.js +1 -1
  42. package/parser/generic/asVariable.js +2 -0
  43. package/parser/generic/assignableVariableAs.js +1 -0
  44. package/parser/generic/assignableVariableProperty.js +5 -2
  45. package/parser/table_trigger_parser.js +21 -0
  46. package/parser/table_trigger_parser.spec.js +29 -0
  47. package/parser/tests/variable_test/coverage_check.xs +293 -0
  48. package/parser/variableScanner.js +64 -0
  49. package/parser/variableValidator.js +44 -0
  50. package/parser/variableValidator.spec.js +179 -0
  51. package/server.js +164 -10
  52. package/utils.js +32 -0
  53. package/utils.spec.js +93 -1
  54. package/workspace/crossFileValidator.js +166 -0
  55. package/workspace/crossFileValidator.spec.js +654 -0
  56. package/workspace/referenceTracking.spec.js +420 -0
  57. package/workspace/workspaceIndex.js +149 -0
  58. package/workspace/workspaceIndex.spec.js +189 -0
@@ -19,7 +19,8 @@
19
19
  "Bash(then)",
20
20
  "Bash(echo:*)",
21
21
  "Bash(fi)",
22
- "Bash(done)"
22
+ "Bash(done)",
23
+ "Bash(npx eslint:*)"
23
24
  ]
24
25
  }
25
26
  }
@@ -7,10 +7,13 @@ import { getSchemeFromContent } from "../utils.js";
7
7
  * Stores parse results keyed by document URI + version.
8
8
  * Note: We cache a snapshot of parser state, not the parser instance itself,
9
9
  * since the parser is a singleton that gets mutated on each parse.
10
+ *
11
+ * lexResult (token arrays) are NOT cached to reduce memory footprint.
12
+ * Lexing is fast (~1ms) and can be repeated when needed.
10
13
  */
11
14
  class DocumentCache {
12
15
  constructor() {
13
- // Map<uri, { version: number, lexResult: Object, parserState: Object, scheme: string }>
16
+ // Map<uri, { version: number, textLength: number, parserState: Object, scheme: string }>
14
17
  this.cache = new Map();
15
18
  }
16
19
 
@@ -31,9 +34,9 @@ class DocumentCache {
31
34
  cached.version === version &&
32
35
  cached.textLength === textLength
33
36
  ) {
34
- // Return cached result with a proxy parser object containing cached state
37
+ // Re-lex (cheap) to provide tokens; return cached parser state
35
38
  return {
36
- lexResult: cached.lexResult,
39
+ lexResult: lexDocument(text),
37
40
  parser: cached.parserState,
38
41
  scheme: cached.scheme,
39
42
  };
@@ -44,21 +47,66 @@ class DocumentCache {
44
47
  const lexResult = lexDocument(text);
45
48
  const parser = xanoscriptParser(text, scheme, lexResult);
46
49
 
47
- // Create a snapshot of the parser's state including symbol table
50
+ // Snapshot only the parser state we need - not the token arrays.
51
+ // Use structured clone to avoid retaining references to parser internals.
48
52
  const parserState = {
49
- errors: [...parser.errors],
50
- warnings: [...parser.warnings],
51
- informations: [...parser.informations],
52
- hints: [...parser.hints],
53
+ errors: parser.errors.map((e) => ({
54
+ message: e.message,
55
+ token: e.token
56
+ ? {
57
+ startOffset: e.token.startOffset,
58
+ endOffset: e.token.endOffset,
59
+ }
60
+ : null,
61
+ })),
62
+ warnings: parser.warnings.map((w) => ({
63
+ message: w.message,
64
+ token: w.token
65
+ ? {
66
+ startOffset: w.token.startOffset,
67
+ endOffset: w.token.endOffset,
68
+ }
69
+ : null,
70
+ })),
71
+ informations: parser.informations.map((i) => ({
72
+ message: i.message,
73
+ token: i.token
74
+ ? {
75
+ startOffset: i.token.startOffset,
76
+ endOffset: i.token.endOffset,
77
+ }
78
+ : null,
79
+ })),
80
+ hints: parser.hints.map((h) => ({
81
+ message: h.message,
82
+ token: h.token
83
+ ? {
84
+ startOffset: h.token.startOffset,
85
+ endOffset: h.token.endOffset,
86
+ }
87
+ : null,
88
+ })),
53
89
  __symbolTable: parser.__symbolTable
54
- ? JSON.parse(JSON.stringify(parser.__symbolTable))
90
+ ? {
91
+ input: { ...parser.__symbolTable.input },
92
+ var: { ...parser.__symbolTable.var },
93
+ auth: { ...parser.__symbolTable.auth },
94
+ env: { ...parser.__symbolTable.env },
95
+ references: parser.__symbolTable.references.map((r) => ({
96
+ ...r,
97
+ args: r.args ? { ...r.args } : undefined,
98
+ dataKeys: r.dataKeys ? [...r.dataKeys] : undefined,
99
+ })),
100
+ varDeclarations: parser.__symbolTable.varDeclarations.map((d) => ({
101
+ ...d,
102
+ })),
103
+ }
55
104
  : null,
56
105
  };
57
106
 
58
107
  const cacheEntry = {
59
108
  version,
60
109
  textLength,
61
- lexResult,
62
110
  parserState,
63
111
  scheme,
64
112
  };
package/lexer/db.js CHANGED
@@ -13,6 +13,12 @@ export const QueryToken = createTokenByName("query", {
13
13
  categories: [Identifier],
14
14
  });
15
15
 
16
+ // where
17
+ export const WhereToken = createTokenByName("where", {
18
+ longer_alt: Identifier,
19
+ categories: [Identifier],
20
+ });
21
+
16
22
  // get
17
23
  export const GetToken = createTokenByName("get", {
18
24
  longer_alt: Identifier,
@@ -156,6 +162,7 @@ export const DbTokens = [
156
162
  MysqlToken,
157
163
  PostgresToken,
158
164
  OracleToken,
165
+ WhereToken,
159
166
  ];
160
167
 
161
168
  export function mapTokenToType(token) {
@@ -183,7 +190,8 @@ export function mapTokenToType(token) {
183
190
  case TruncateToken.name:
184
191
  case DirectQueryToken.name:
185
192
  case SetDatasourceToken.name:
186
- return "function";
193
+ case WhereToken.name:
194
+ return "variable";
187
195
  default:
188
196
  return null;
189
197
  }
package/lexer/security.js CHANGED
@@ -85,6 +85,18 @@ export const JweEncodeToken = createTokenByName("jwe_encode", {
85
85
  categories: [Identifier],
86
86
  });
87
87
 
88
+ // jwe_decode_legacy
89
+ export const JweDecodeLegacyToken = createTokenByName("jwe_decode_legacy", {
90
+ longer_alt: Identifier,
91
+ categories: [Identifier],
92
+ });
93
+
94
+ // jwe_encode_legacy
95
+ export const JweEncodeLegacyToken = createTokenByName("jwe_encode_legacy", {
96
+ longer_alt: Identifier,
97
+ categories: [Identifier],
98
+ });
99
+
88
100
  // jws_decode
89
101
  export const JwsDecodeToken = createTokenByName("jws_decode", {
90
102
  longer_alt: Identifier,
@@ -121,6 +133,8 @@ export const SecurityTokens = [
121
133
  EncryptToken,
122
134
  GeneratePassToken,
123
135
  GenerateUuidToken,
136
+ JweDecodeLegacyToken,
137
+ JweEncodeLegacyToken,
124
138
  JweDecodeToken,
125
139
  JweEncodeToken,
126
140
  JwsDecodeToken,
@@ -150,6 +164,8 @@ export function mapTokenToType(token) {
150
164
  case GenerateUuidToken.name:
151
165
  case JweDecodeToken.name:
152
166
  case JweEncodeToken.name:
167
+ case JweDecodeLegacyToken.name:
168
+ case JweEncodeLegacyToken.name:
153
169
  case JwsDecodeToken.name:
154
170
  case JwsEncodeToken.name:
155
171
  case RandomBytesToken.name:
@@ -1,5 +1,6 @@
1
1
  import { mapToVirtualJS } from "../embedded/embeddedContent.js";
2
2
  import { getSchemeFromContent } from "../utils.js";
3
+ import { workspaceIndex } from "../workspace/workspaceIndex.js";
3
4
  import { getContentAssistSuggestions } from "./contentAssist.js";
4
5
 
5
6
  /**
@@ -33,6 +34,65 @@ export function onCompletion(params, documents) {
33
34
 
34
35
  // Otherwise, handle as regular XanoScript
35
36
  const scheme = getSchemeFromContent(text);
37
+ const prefix = text.slice(0, offset);
36
38
 
37
- return getContentAssistSuggestions(text.slice(0, offset), scheme);
39
+ const suggestions = getContentAssistSuggestions(prefix, scheme);
40
+
41
+ // Add workspace object name completions
42
+ const workspaceCompletions = getWorkspaceCompletions(prefix);
43
+ if (workspaceCompletions) {
44
+ const existing = suggestions || [];
45
+ return [...existing, ...workspaceCompletions];
46
+ }
47
+
48
+ return suggestions;
49
+ }
50
+
51
+ /**
52
+ * Get completions for cross-file references based on cursor context.
53
+ * @param {string} prefix - Text before cursor
54
+ * @returns {Array|null} Completion items or null if not in a reference context
55
+ */
56
+ function getWorkspaceCompletions(prefix) {
57
+ // function.run " or function.call " context
58
+ if (/function\.(run|call)\s+"[^"]*$/.test(prefix)) {
59
+ return workspaceIndex.getAllNames("function").map((name) => ({
60
+ label: name,
61
+ kind: 3, // Function
62
+ detail: "function",
63
+ }));
64
+ }
65
+
66
+ // task.call " context
67
+ if (/task\.call\s+"[^"]*$/.test(prefix)) {
68
+ return workspaceIndex.getAllNames("task").map((name) => ({
69
+ label: name,
70
+ kind: 3,
71
+ detail: "task",
72
+ }));
73
+ }
74
+
75
+ // db.* table name context
76
+ if (
77
+ /\b(get|add|edit|delete|query|patch|has|truncate|add_or_edit|schema)\s+("?[^"\s{]*$)/.test(
78
+ prefix
79
+ )
80
+ ) {
81
+ return workspaceIndex.getAllNames("table").map((name) => ({
82
+ label: name,
83
+ kind: 7, // Class (for table)
84
+ detail: "table",
85
+ }));
86
+ }
87
+
88
+ // api.call " context
89
+ if (/api\.call\s+"[^"]*$/.test(prefix)) {
90
+ return workspaceIndex.getAllNames("query").map((name) => ({
91
+ label: name,
92
+ kind: 3,
93
+ detail: "query",
94
+ }));
95
+ }
96
+
97
+ return null;
38
98
  }
@@ -0,0 +1,150 @@
1
+ import { lexDocument } from "../lexer/lexer.js";
2
+ import { LongFormVariable, ShortFormVariable } from "../lexer/variables.js";
3
+ import { getVarName } from "../parser/generic/utils.js";
4
+ import { xanoscriptParser } from "../parser/parser.js";
5
+
6
+ // Token names that precede a table name reference
7
+ const DB_OPERATION_TOKENS = new Set([
8
+ "get",
9
+ "add",
10
+ "edit",
11
+ "delete",
12
+ "query",
13
+ "patch",
14
+ "has",
15
+ "truncate",
16
+ "add_or_edit",
17
+ "schema",
18
+ ]);
19
+
20
+ const SHORT_FORM = ShortFormVariable.name;
21
+ const LONG_FORM = LongFormVariable.name;
22
+
23
+ /**
24
+ * Find the definition location for the symbol at the given offset.
25
+ * @param {string} text - Document text
26
+ * @param {number} offset - Cursor offset in document
27
+ * @param {import('../workspace/workspaceIndex.js').WorkspaceIndex} index
28
+ * @param {Object} [symbolTable] - Parsed symbol table (optional, for variable lookups)
29
+ * @returns {{ uri: string, offset?: number } | null}
30
+ */
31
+ export function findDefinition(text, offset, index, symbolTable) {
32
+ const lexResult = lexDocument(text);
33
+ const tokens = lexResult.tokens;
34
+
35
+ // Find the token at the cursor position
36
+ let tokenIndex = -1;
37
+ for (let i = 0; i < tokens.length; i++) {
38
+ if (tokens[i].startOffset <= offset && offset <= tokens[i].endOffset) {
39
+ tokenIndex = i;
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (tokenIndex < 0) return null;
45
+
46
+ const token = tokens[tokenIndex];
47
+ const typeName = token.tokenType.name;
48
+
49
+ // Variable go-to-definition
50
+ if (typeName === SHORT_FORM || typeName === LONG_FORM) {
51
+ // Parse if no symbol table provided
52
+ const st =
53
+ symbolTable ||
54
+ xanoscriptParser(text, undefined, lexResult).__symbolTable;
55
+ return findVariableDefinition(tokens, tokenIndex, st);
56
+ }
57
+
58
+ // For LongFormVariable ($var), check if cursor is on the identifier after $var.
59
+ // e.g., "$var.foo" — cursor on "foo" (an Identifier token after DotToken after LongFormVariable)
60
+ if (
61
+ tokenIndex >= 2 &&
62
+ tokens[tokenIndex - 1].image === "." &&
63
+ tokens[tokenIndex - 2].tokenType.name === LONG_FORM
64
+ ) {
65
+ const st =
66
+ symbolTable ||
67
+ xanoscriptParser(text, undefined, lexResult).__symbolTable;
68
+ return findVariableDefinition(tokens, tokenIndex, st);
69
+ }
70
+
71
+ const name = getVarName(token);
72
+
73
+ // Look backwards to determine context (cross-file references)
74
+ for (let i = tokenIndex - 1; i >= 0 && i >= tokenIndex - 3; i--) {
75
+ const prevImage = tokens[i].image.toLowerCase();
76
+
77
+ if (prevImage === "run" || prevImage === "call") {
78
+ // Check what's before: function. or task. or api.
79
+ if (i >= 2 && tokens[i - 1].image === ".") {
80
+ const prefix = tokens[i - 2].image.toLowerCase();
81
+ if (prefix === "function") {
82
+ const entry = index.get("function", name);
83
+ return entry ? { uri: entry.uri } : null;
84
+ }
85
+ if (prefix === "task") {
86
+ const entry = index.get("task", name);
87
+ return entry ? { uri: entry.uri } : null;
88
+ }
89
+ if (prefix === "api") {
90
+ const entry = index.get("query", name);
91
+ return entry ? { uri: entry.uri } : null;
92
+ }
93
+ }
94
+ }
95
+
96
+ if (DB_OPERATION_TOKENS.has(prevImage)) {
97
+ const entry = index.get("table", name);
98
+ return entry ? { uri: entry.uri } : null;
99
+ }
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Find the declaration offset for a variable token.
107
+ * @param {import('chevrotain').IToken[]} tokens
108
+ * @param {number} tokenIndex - Index of the token the cursor is on
109
+ * @param {Object} symbolTable
110
+ * @returns {{ offset: number } | null}
111
+ */
112
+ function findVariableDefinition(tokens, tokenIndex, symbolTable) {
113
+ const declarations = symbolTable?.varDeclarations;
114
+ if (!declarations || declarations.length === 0) return null;
115
+
116
+ const token = tokens[tokenIndex];
117
+ const typeName = token.tokenType.name;
118
+
119
+ let varName;
120
+ if (typeName === SHORT_FORM) {
121
+ varName = token.image; // "$foo"
122
+ } else if (typeName === LONG_FORM) {
123
+ // $var.foo → resolve to $foo
124
+ if (tokenIndex + 2 < tokens.length) {
125
+ varName = `$${tokens[tokenIndex + 2].image}`;
126
+ } else {
127
+ return null;
128
+ }
129
+ } else {
130
+ // Cursor is on the identifier after "$var." (e.g., "foo" in "$var.foo")
131
+ varName = `$${token.image}`;
132
+ }
133
+
134
+ // Find the first declaration for this variable
135
+ const decl = declarations.find((d) => d.name === varName);
136
+ if (!decl || decl.startOffset == null) return null;
137
+
138
+ // Don't navigate if cursor is already on the declaration
139
+ if (token.startOffset === decl.startOffset) return null;
140
+ // For LongFormVariable, also check if the identifier token matches
141
+ if (
142
+ typeName === LONG_FORM &&
143
+ tokenIndex + 2 < tokens.length &&
144
+ tokens[tokenIndex + 2].startOffset === decl.startOffset
145
+ ) {
146
+ return null;
147
+ }
148
+
149
+ return { offset: decl.startOffset };
150
+ }