@xano/xanoscript-language-server 11.8.5 → 11.9.1

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/lexer/comment.js CHANGED
@@ -8,36 +8,26 @@ export const CommentToken = createUniqToken({
8
8
  pattern: {
9
9
  exec: (text, offset) => {
10
10
  // Look for the `//` marker at the current offset
11
- if (!text.slice(offset).startsWith("//")) {
11
+ if (text.charCodeAt(offset) !== 47 || text.charCodeAt(offset + 1) !== 47) {
12
12
  return null;
13
13
  }
14
14
 
15
- // Allow if at the start of input
16
- if (offset === 0) {
17
- return text.slice(offset).match(/.*(?=\n|$)/);
18
- }
19
-
20
- // Look backwards from offset to check what comes before
21
- // We need to find only whitespace (spaces/tabs) or a newline before the comment
22
- let checkOffset = offset - 1;
23
- while (checkOffset >= 0) {
24
- const char = text[checkOffset];
25
-
26
- // If we find a newline, the comment is on its own line (possibly with leading whitespace)
27
- if (char === "\n" || char === "\r") {
28
- return text.slice(offset).match(/.*(?=\n|$)/);
15
+ // Check that the comment is on its own line (only whitespace before it)
16
+ if (offset > 0) {
17
+ let checkOffset = offset - 1;
18
+ while (checkOffset >= 0) {
19
+ const char = text.charCodeAt(checkOffset);
20
+ if (char === 10 || char === 13) break; // \n or \r → own line
21
+ if (char !== 32 && char !== 9) return null; // not space/tab → reject
22
+ checkOffset--;
29
23
  }
30
-
31
- // If we find anything other than space or tab, reject
32
- if (char !== " " && char !== "\t") {
33
- return null;
34
- }
35
-
36
- checkOffset--;
37
24
  }
38
25
 
39
- // If we've checked all the way to the start and only found whitespace, allow it
40
- return text.slice(offset).match(/.*(?=\n|$)/);
26
+ // Find end of line without allocating a substring
27
+ let end = text.indexOf("\n", offset);
28
+ if (end === -1) end = text.length;
29
+ const match = text.slice(offset, end);
30
+ return [match];
41
31
  },
42
32
  },
43
33
  // we don't capture more than one line
@@ -59,7 +59,7 @@ function createDiagnostics(parser, document) {
59
59
  * @param {import('vscode-languageserver').Connection} connection
60
60
  * @returns
61
61
  */
62
- export function onDidChangeContent(params, connection) {
62
+ export function onDidChangeContent(params, connection, features = {}) {
63
63
  const document = params.document;
64
64
 
65
65
  if (!document) {
@@ -93,7 +93,7 @@ export function onDidChangeContent(params, connection) {
93
93
  const diagnostics = createDiagnostics(parser, document);
94
94
 
95
95
  // Run cross-file validation and append as warnings
96
- if (parser.__symbolTable?.references) {
96
+ if (features.crossFileValidation !== false && parser.__symbolTable?.references) {
97
97
  const crossFileWarnings = crossFileValidate(
98
98
  parser.__symbolTable.references,
99
99
  workspaceIndex,
@@ -111,7 +111,7 @@ export function onDidChangeContent(params, connection) {
111
111
  }
112
112
 
113
113
  // Run variable validation and append warnings/hints
114
- if (parser.__symbolTable?.varDeclarations) {
114
+ if (features.variableValidation !== false && parser.__symbolTable?.varDeclarations) {
115
115
  const varResult = validateVariables(
116
116
  parser.__symbolTable,
117
117
  lexResult.tokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "11.8.5",
3
+ "version": "11.9.1",
4
4
  "description": "Language Server Protocol implementation for XanoScript",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -83,6 +83,13 @@ describe("dbQueryFn", () => {
83
83
  expect(parser.errors).to.be.empty;
84
84
  });
85
85
 
86
+ it("dbQueryFn accepts a search expression with dynamic field", () => {
87
+ const parser = parse(`query user {
88
+ where = $db.my_table["$search value"] search? $input.values
89
+ } as $user`);
90
+ expect(parser.errors).to.be.empty;
91
+ });
92
+
86
93
  it("dbQueryFn accepts sub addon", () => {
87
94
  const parser = parse(`query user {
88
95
  where = $db.array_columns @> $db.array_columns.id
@@ -98,6 +98,7 @@ const OperatorAcceptingOptionalIfNull = [
98
98
  JsonNotOverlapsToken,
99
99
  JsonNotRegexToken,
100
100
  JsonOverlapsToken,
101
+ JsonSearchToken,
101
102
  ];
102
103
 
103
104
  /**
@@ -1,4 +1,5 @@
1
1
  import { ActiveToken } from "../lexer/api_group.js";
2
+ import { CommentToken } from "../lexer/comment.js";
2
3
  import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
3
4
  import { StringLiteral } from "../lexer/literal.js";
4
5
  import { DataSourceToken, TaskToken } from "../lexer/task.js";
@@ -32,7 +33,7 @@ export function taskDeclaration($) {
32
33
  $.MANY(() => {
33
34
  $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
34
35
  $.OR2([
35
- { ALT: () => $.SUBRULE($.commentBlockFn) },
36
+ { ALT: () => $.CONSUME(CommentToken) },
36
37
  {
37
38
  GATE: () => !hasActive,
38
39
  ALT: () => {
@@ -0,0 +1,165 @@
1
+ task claim_reminders {
2
+ description = "Runs every 6 hours to send reminders for claims stalled at the assessment, review, or approval workflow stages."
3
+
4
+ stack {
5
+ debug.log {
6
+ value = "Claim reminders task started"
7
+ }
8
+
9
+ // --- Cutoff timestamps ---
10
+
11
+ var $forty_eight_hours_ago {
12
+ value = now|transform_timestamp:"-48 hours"
13
+ }
14
+
15
+ var $twenty_four_hours_ago {
16
+ value = now|transform_timestamp:"-24 hours"
17
+ }
18
+
19
+ var $twelve_hours_ago {
20
+ value = now|transform_timestamp:"-12 hours"
21
+ }
22
+
23
+ var $assessment_reminder_count {
24
+ value = 0
25
+ }
26
+
27
+ var $review_reminder_count {
28
+ value = 0
29
+ }
30
+
31
+ var $approval_reminder_count {
32
+ value = 0
33
+ }
34
+
35
+ // === Step 1: Garage Assessment Reminders (stalled > 48h) ===
36
+
37
+ db.query claim {
38
+ where = ($db.claim.workflow_status == "assessment" && $db.claim.updated_at < $forty_eight_hours_ago)
39
+ } as $assessment_claims
40
+
41
+ foreach ($assessment_claims) {
42
+ each as $claim {
43
+ try_catch {
44
+ try {
45
+ function.run "communication/send_notification" {
46
+ input = {
47
+ user_id : $claim.garage_id
48
+ claim_id: $claim.id
49
+ message : "Reminder: Vehicle assessment for claim " ~ $claim.claim_number ~ " is awaiting your action. Please complete the assessment report at your earliest convenience."
50
+ subject : "Assessment Reminder - Claim " ~ $claim.claim_number
51
+ }
52
+ }
53
+
54
+ var.update $assessment_reminder_count {
55
+ value = $assessment_reminder_count + 1
56
+ }
57
+ }
58
+ catch {
59
+ debug.log {
60
+ value = {
61
+ error : "Failed to send assessment reminder"
62
+ claim_number: $claim.claim_number
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ debug.log {
71
+ value = "Assessment reminders sent: " ~ $assessment_reminder_count
72
+ }
73
+
74
+ // === Step 2: Agent Review Reminders (stalled > 24h) ===
75
+
76
+ db.query claim {
77
+ where = ($db.claim.workflow_status == "review" && $db.claim.updated_at < $twenty_four_hours_ago)
78
+ } as $review_claims
79
+
80
+ foreach ($review_claims) {
81
+ each as $claim {
82
+ try_catch {
83
+ try {
84
+ function.run "communication/send_notification" {
85
+ input = {
86
+ user_id : $claim.assigned_agent_id
87
+ claim_id: $claim.id
88
+ message : "Reminder: Claim " ~ $claim.claim_number ~ " is awaiting your review. Please complete your review to keep the claim moving forward."
89
+ subject : "Review Reminder - Claim " ~ $claim.claim_number
90
+ }
91
+ }
92
+
93
+ var.update $review_reminder_count {
94
+ value = $review_reminder_count + 1
95
+ }
96
+ }
97
+ catch {
98
+ debug.log {
99
+ value = {
100
+ error : "Failed to send review reminder"
101
+ claim_number: $claim.claim_number
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ debug.log {
110
+ value = "Review reminders sent: " ~ $review_reminder_count
111
+ }
112
+
113
+ // === Step 3: Agent Approval Reminders - URGENT (stalled > 12h) ===
114
+
115
+ db.query claim {
116
+ where = ($db.claim.workflow_status == "approval" && $db.claim.updated_at < $twelve_hours_ago)
117
+ } as $approval_claims
118
+
119
+ foreach ($approval_claims) {
120
+ each as $claim {
121
+ try_catch {
122
+ try {
123
+ function.run "communication/send_notification" {
124
+ input = {
125
+ user_id : $claim.assigned_agent_id
126
+ claim_id: $claim.id
127
+ message : "URGENT: Claim " ~ $claim.claim_number ~ " requires your immediate approval. This claim has been pending approval for over 12 hours. Please review and take action as soon as possible."
128
+ subject : "URGENT: Approval Required - Claim " ~ $claim.claim_number
129
+ }
130
+ }
131
+
132
+ var.update $approval_reminder_count {
133
+ value = $approval_reminder_count + 1
134
+ }
135
+ }
136
+ catch {
137
+ debug.log {
138
+ value = {
139
+ error : "Failed to send approval reminder"
140
+ claim_number: $claim.claim_number
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+
148
+ debug.log {
149
+ value = "Approval reminders sent: " ~ $approval_reminder_count
150
+ }
151
+
152
+ // === Step 4: Final summary ===
153
+
154
+ debug.log {
155
+ value = {
156
+ assessment_reminders_sent: $assessment_reminder_count
157
+ review_reminders_sent : $review_reminder_count
158
+ approval_reminders_sent : $approval_reminder_count
159
+ total_reminders_sent : ($assessment_reminder_count + $review_reminder_count + $approval_reminder_count)
160
+ }
161
+ }
162
+ }
163
+
164
+ schedule = [{starts_on: 2026-01-01 00:00:00+0000, freq: 21600}]
165
+ }
package/server.js CHANGED
@@ -20,6 +20,20 @@ import { xanoscriptParser } from "./parser/parser.js";
20
20
  import { getSchemeFromContent } from "./utils.js";
21
21
  import { workspaceIndex } from "./workspace/workspaceIndex.js";
22
22
 
23
+ // Feature toggles for debugging memory issues.
24
+ // Disable one at a time to isolate the culprit.
25
+ const FEATURES = {
26
+ workspaceScan: true, // initial .xs file scan on startup
27
+ fileWatcher: true, // onDidChangeWatchedFiles
28
+ completion: true, // onCompletion (content assist)
29
+ semanticTokens: true, // semantic highlighting
30
+ hover: true, // onHover
31
+ definition: true, // onDefinition (go-to-definition)
32
+ diagnostics: true, // onDidChangeContent (parse + diagnostics)
33
+ crossFileValidation: true, // cross-file reference validation
34
+ variableValidation: true, // variable usage validation
35
+ };
36
+
23
37
  // Create a connection to the VS Code client
24
38
  const connection = createConnection(ProposedFeatures.all);
25
39
 
@@ -31,12 +45,14 @@ connection.onInitialize((params) => {
31
45
  connection.console.log("XanoScript Language Server initialized");
32
46
 
33
47
  // Scan workspace for .xs files to populate the index
34
- if (params.workspaceFolders) {
35
- for (const folder of params.workspaceFolders) {
36
- scanWorkspaceFolder(folder.uri);
48
+ if (FEATURES.workspaceScan) {
49
+ if (params.workspaceFolders) {
50
+ for (const folder of params.workspaceFolders) {
51
+ scanWorkspaceFolder(folder.uri);
52
+ }
53
+ } else if (params.rootUri) {
54
+ scanWorkspaceFolder(params.rootUri);
37
55
  }
38
- } else if (params.rootUri) {
39
- scanWorkspaceFolder(params.rootUri);
40
56
  }
41
57
 
42
58
  return {
@@ -142,6 +158,8 @@ function indexFilesInBatches(filePaths) {
142
158
  }
143
159
 
144
160
  connection.onDidChangeWatchedFiles((params) => {
161
+ if (!FEATURES.fileWatcher) return;
162
+ connection.console.log(`[FILEWATCHER] ${params.changes.length} changes`);
145
163
  for (const change of params.changes) {
146
164
  if (!change.uri.endsWith(".xs")) continue;
147
165
  switch (change.type) {
@@ -164,19 +182,29 @@ connection.onDidChangeWatchedFiles((params) => {
164
182
  }
165
183
  });
166
184
 
167
- connection.onHover((params) => onHover(params, documents));
168
- connection.onCompletion((params) => onCompletion(params, documents));
185
+ connection.onHover((params) => {
186
+ if (!FEATURES.hover) return null;
187
+ connection.console.log(
188
+ `[HOVER] ${params.textDocument.uri} pos=${JSON.stringify(params.position)}`,
189
+ );
190
+ return onHover(params, documents);
191
+ });
192
+ connection.onCompletion((params) => {
193
+ if (!FEATURES.completion) return null;
194
+ connection.console.log(
195
+ `[COMPLETION] ${params.textDocument.uri} pos=${JSON.stringify(params.position)}`,
196
+ );
197
+ return onCompletion(params, documents);
198
+ });
169
199
  connection.onDefinition((params) => {
200
+ if (!FEATURES.definition) return null;
201
+ connection.console.log(`[DEFINITION] ${params.textDocument.uri}`);
170
202
  const document = documents.get(params.textDocument.uri);
171
203
  if (!document) return null;
172
204
 
173
205
  const text = document.getText();
174
206
  const offset = document.offsetAt(params.position);
175
- const cached = documentCache.getOrParse(
176
- document.uri,
177
- document.version,
178
- text,
179
- );
207
+ const cached = documentCache.getOrParse(document.uri, document.version, text);
180
208
  const symbolTable = cached.parser?.__symbolTable;
181
209
  const result = findDefinition(text, offset, workspaceIndex, symbolTable);
182
210
 
@@ -197,12 +225,18 @@ connection.onDefinition((params) => {
197
225
  range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
198
226
  };
199
227
  });
200
- connection.onRequest("textDocument/semanticTokens/full", (params) =>
201
- onSemanticCheck(params, documents, SemanticTokensBuilder),
202
- );
203
- documents.onDidChangeContent((params) =>
204
- onDidChangeContent(params, connection),
205
- );
228
+ connection.onRequest("textDocument/semanticTokens/full", (params) => {
229
+ if (!FEATURES.semanticTokens) return null;
230
+ connection.console.log(`[SEMANTIC] ${params.textDocument.uri}`);
231
+ return onSemanticCheck(params, documents, SemanticTokensBuilder);
232
+ });
233
+ documents.onDidChangeContent((params) => {
234
+ if (!FEATURES.diagnostics) return;
235
+ connection.console.log(
236
+ `[DIAGNOSTICS] ${params.document.uri} v${params.document.version}`,
237
+ );
238
+ onDidChangeContent(params, connection, FEATURES);
239
+ });
206
240
  documents.onDidClose((params) => {
207
241
  const uri = params.document.uri;
208
242
  documentCache.invalidate(uri);