@xano/xanoscript-language-server 11.0.5 → 11.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/CLAUDE.md +140 -5
- package/cache/documentCache.js +91 -0
- package/debug.js +18 -0
- package/lexer/run.js +37 -0
- package/lexer/tokens.js +84 -70
- package/onCompletion/contentAssist.js +10 -9
- package/onDidChangeContent/onDidChangeContent.js +66 -76
- package/onHover/functions.md +56 -0
- package/onHover/onHoverDocument.js +39 -15
- package/onSemanticCheck/highlight.js +2 -1
- package/onSemanticCheck/onSemanticCheck.js +2 -1
- package/package.json +4 -4
- package/parser/attributes/docsFieldAttribute.spec.js +5 -0
- package/parser/functions/api/apiCallFn.spec.js +2 -2
- package/parser/functions/controls/tryCatchFn.spec.js +0 -1
- package/parser/generic/objectWithAttributes.js +1 -0
- package/parser/parser.js +7 -3
- package/parser/register.js +8 -0
- package/parser/run_parser.js +70 -0
- package/parser/run_parser.spec.js +100 -0
- package/server.js +2 -1
- package/utils.js +1 -0
- package/.claude/settings.local.json +0 -34
package/CLAUDE.md
CHANGED
|
@@ -30,12 +30,14 @@ This is a Language Server Protocol (LSP) implementation for XanoScript, built on
|
|
|
30
30
|
### Core Components
|
|
31
31
|
|
|
32
32
|
**Lexer (`lexer/`)**: Token definitions and lexical analysis
|
|
33
|
+
|
|
33
34
|
- `tokens.js` - Main token registry
|
|
34
35
|
- `lexer.js` - Chevrotain lexer implementation
|
|
35
36
|
- Domain tokens: `api.js`, `db.js`, `cloud.js`, `function.js`, etc.
|
|
36
37
|
- `utils.js` - Token creation utilities (`createToken`, `createTokenByName`)
|
|
37
38
|
|
|
38
39
|
**Parser (`parser/`)**: Grammar rules and parsing logic
|
|
40
|
+
|
|
39
41
|
- `base_parser.js` - Core XanoBaseParser extending Chevrotain
|
|
40
42
|
- Main parsers: `query_parser.js`, `function_parser.js`, `task_parser.js`, `api_group_parser.js`, `table_parser.js`, `workflow_test_parser.js`, `table_trigger_parser.js`
|
|
41
43
|
- `attributes/` - Field attributes (description, disabled, sensitive)
|
|
@@ -45,6 +47,7 @@ This is a Language Server Protocol (LSP) implementation for XanoScript, built on
|
|
|
45
47
|
- `generic/` - Reusable parsing components
|
|
46
48
|
|
|
47
49
|
**Language Server (`server.js` + feature directories)**:
|
|
50
|
+
|
|
48
51
|
- `onCompletion/` - Auto-completion logic
|
|
49
52
|
- `onDidChangeContent/` - Live diagnostics and error reporting
|
|
50
53
|
- `onHover/` - Documentation and hover information
|
|
@@ -53,6 +56,7 @@ This is a Language Server Protocol (LSP) implementation for XanoScript, built on
|
|
|
53
56
|
### XanoScript Object Types
|
|
54
57
|
|
|
55
58
|
Primary constructs parsed by dedicated parsers:
|
|
59
|
+
|
|
56
60
|
- **query** - API endpoints with HTTP verbs, input validation, processing logic, responses
|
|
57
61
|
- **function** - Reusable logic blocks with testing capabilities
|
|
58
62
|
- **task** - Scheduled operations with cron-like triggers
|
|
@@ -63,18 +67,147 @@ Primary constructs parsed by dedicated parsers:
|
|
|
63
67
|
|
|
64
68
|
## Adding New Features
|
|
65
69
|
|
|
66
|
-
### New
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
### New Top-Level Parser (e.g., `run.job`, `run.service`)
|
|
71
|
+
|
|
72
|
+
Follow this step-by-step process:
|
|
73
|
+
|
|
74
|
+
**1. Create lexer tokens (`lexer/my_feature.js`)**
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
import { Identifier } from "./identifier.js";
|
|
78
|
+
import { createTokenByName } from "./utils.js";
|
|
79
|
+
|
|
80
|
+
export const MyToken = createTokenByName("my_keyword", {
|
|
81
|
+
longer_alt: Identifier,
|
|
82
|
+
categories: [Identifier],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const MyFeatureTokens = [MyToken];
|
|
86
|
+
|
|
87
|
+
export function mapTokenToType(token) {
|
|
88
|
+
switch (token) {
|
|
89
|
+
case MyToken.name:
|
|
90
|
+
return "keyword";
|
|
91
|
+
default:
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**2. Register tokens in `lexer/tokens.js`**
|
|
98
|
+
|
|
99
|
+
- Import tokens and mapper at the top
|
|
100
|
+
- Add `...MyFeatureTokens` to `allTokens` array
|
|
101
|
+
- Add `mapMyFeatureTokenToType` to `tokenMappers` array
|
|
102
|
+
|
|
103
|
+
**3. Create parser (`parser/my_feature_parser.js`)**
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
import { StringLiteral } from "../lexer/literal.js";
|
|
107
|
+
import { MyToken } from "../lexer/my_feature.js";
|
|
108
|
+
import { Identifier, NewlineToken } from "../lexer/tokens.js";
|
|
109
|
+
|
|
110
|
+
export function myFeatureDeclaration($) {
|
|
111
|
+
return () => {
|
|
112
|
+
$.sectionStack.push("myFeatureDeclaration");
|
|
113
|
+
$.SUBRULE($.optionalCommentBlockFn);
|
|
114
|
+
|
|
115
|
+
const parent = $.CONSUME(MyToken);
|
|
116
|
+
$.OR([
|
|
117
|
+
{ ALT: () => $.CONSUME(StringLiteral) },
|
|
118
|
+
{ ALT: () => $.CONSUME(Identifier) },
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
// Use schemaParseAttributeFn for body with declarative schema
|
|
122
|
+
$.SUBRULE($.schemaParseAttributeFn, {
|
|
123
|
+
ARGS: [
|
|
124
|
+
parent,
|
|
125
|
+
{
|
|
126
|
+
required_attr: "[string]",
|
|
127
|
+
"optional_attr?": "[boolean]",
|
|
128
|
+
"nested?": {
|
|
129
|
+
name: "[string]",
|
|
130
|
+
"value?": { "[string]": "[constant]" },
|
|
131
|
+
},
|
|
132
|
+
"array_of_strings?": ["[string]"],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
$.MANY2(() => $.CONSUME2(NewlineToken));
|
|
138
|
+
$.sectionStack.pop();
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**4. Register parser in `parser/register.js`**
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
import { myFeatureDeclaration } from "./my_feature_parser.js";
|
|
147
|
+
// In register function:
|
|
148
|
+
$.myFeatureDeclaration = $.RULE(
|
|
149
|
+
"myFeatureDeclaration",
|
|
150
|
+
myFeatureDeclaration($)
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**5. Add scheme detection in `utils.js`**
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
const schemeByFirstWord = {
|
|
158
|
+
// ... existing entries
|
|
159
|
+
my_feature: "my_feature",
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**6. Route scheme in `parser/parser.js`**
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
case "my_feature":
|
|
167
|
+
parser.myFeatureDeclaration();
|
|
168
|
+
return parser;
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**7. Write tests (`parser/my_feature_parser.spec.js`)**
|
|
172
|
+
|
|
173
|
+
```javascript
|
|
174
|
+
import { expect } from "chai";
|
|
175
|
+
import { describe, it } from "mocha";
|
|
176
|
+
import { xanoscriptParser } from "./parser.js";
|
|
177
|
+
|
|
178
|
+
describe("my_feature", () => {
|
|
179
|
+
it("should parse a basic my_feature", () => {
|
|
180
|
+
const parser = xanoscriptParser(`my_feature "name" {
|
|
181
|
+
required_attr = "value"
|
|
182
|
+
}`);
|
|
183
|
+
expect(parser.errors).to.be.empty;
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Schema Definition Types (for `schemaParseAttributeFn`)
|
|
189
|
+
|
|
190
|
+
| Schema | Description | Example |
|
|
191
|
+
| ------------------------------ | ---------------------------------- | ------------------------- |
|
|
192
|
+
| `"[string]"` | String literal | `"hello"` |
|
|
193
|
+
| `"[number]"` | Number literal | `123` |
|
|
194
|
+
| `"[boolean]"` | Boolean value | `true` / `false` |
|
|
195
|
+
| `"[constant]"` | Value expression (no variables) | `"text"`, `123`, `{a: 1}` |
|
|
196
|
+
| `"[expression]"` | Any expression including variables | `$var`, `$input.name` |
|
|
197
|
+
| `["[string]"]` | Array of strings | `["a", "b"]` |
|
|
198
|
+
| `{ "[string]": "[constant]" }` | Object with string keys | `{key: "value"}` |
|
|
199
|
+
| `"attr?"` | Optional attribute | May be omitted |
|
|
200
|
+
| `"!attr"` | Can be disabled | `!attr = value` |
|
|
70
201
|
|
|
71
202
|
### New Keyword/Function
|
|
203
|
+
|
|
72
204
|
1. Add token definition in appropriate `lexer/` file
|
|
73
205
|
2. Create function implementation in `parser/functions/[domain]/`
|
|
74
206
|
3. Register in parent clause or parser
|
|
75
207
|
4. Add comprehensive tests in corresponding `.spec.js` file
|
|
76
208
|
|
|
77
209
|
### Token Creation Pattern
|
|
210
|
+
|
|
78
211
|
```javascript
|
|
79
212
|
export const MyToken = createTokenByName("keyword", {
|
|
80
213
|
longer_alt: Identifier,
|
|
@@ -83,6 +216,7 @@ export const MyToken = createTokenByName("keyword", {
|
|
|
83
216
|
```
|
|
84
217
|
|
|
85
218
|
### Parser Rule Pattern
|
|
219
|
+
|
|
86
220
|
```javascript
|
|
87
221
|
myRule = this.RULE("myRule", () => {
|
|
88
222
|
this.CONSUME(MyToken);
|
|
@@ -100,6 +234,7 @@ myRule = this.RULE("myRule", () => {
|
|
|
100
234
|
- CI requires all tests to pass
|
|
101
235
|
|
|
102
236
|
### Test Pattern
|
|
237
|
+
|
|
103
238
|
```javascript
|
|
104
239
|
function parse(inputText) {
|
|
105
240
|
const lexResult = lexDocument(inputText);
|
|
@@ -118,4 +253,4 @@ function parse(inputText) {
|
|
|
118
253
|
- Follow existing code conventions when adding features
|
|
119
254
|
- Check neighboring files for patterns and conventions
|
|
120
255
|
- Never assume a library is available - check `package.json` first
|
|
121
|
-
- 3 low-severity npm audit warnings are known and acceptable
|
|
256
|
+
- 3 low-severity npm audit warnings are known and acceptable
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { lexDocument } from "../lexer/lexer.js";
|
|
2
|
+
import { xanoscriptParser } from "../parser/parser.js";
|
|
3
|
+
import { getSchemeFromContent } from "../utils.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cache for parsed documents to avoid redundant parsing.
|
|
7
|
+
* Stores parse results keyed by document URI + version.
|
|
8
|
+
* Note: We cache a snapshot of parser state, not the parser instance itself,
|
|
9
|
+
* since the parser is a singleton that gets mutated on each parse.
|
|
10
|
+
*/
|
|
11
|
+
class DocumentCache {
|
|
12
|
+
constructor() {
|
|
13
|
+
// Map<uri, { version: number, lexResult: Object, parserState: Object, scheme: string }>
|
|
14
|
+
this.cache = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get cached parse result or parse and cache if not available.
|
|
19
|
+
* @param {string} uri - Document URI
|
|
20
|
+
* @param {number} version - Document version
|
|
21
|
+
* @param {string} text - Document text
|
|
22
|
+
* @returns {{ lexResult: Object, parser: Object, scheme: string }}
|
|
23
|
+
*/
|
|
24
|
+
getOrParse(uri, version, text) {
|
|
25
|
+
const cached = this.cache.get(uri);
|
|
26
|
+
const textLength = text.length;
|
|
27
|
+
|
|
28
|
+
// Check version and text length to detect same URI with different content (e.g., in tests)
|
|
29
|
+
if (
|
|
30
|
+
cached &&
|
|
31
|
+
cached.version === version &&
|
|
32
|
+
cached.textLength === textLength
|
|
33
|
+
) {
|
|
34
|
+
// Return cached result with a proxy parser object containing cached state
|
|
35
|
+
return {
|
|
36
|
+
lexResult: cached.lexResult,
|
|
37
|
+
parser: cached.parserState,
|
|
38
|
+
scheme: cached.scheme,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse and cache a snapshot of the parser state
|
|
43
|
+
const scheme = getSchemeFromContent(text);
|
|
44
|
+
const lexResult = lexDocument(text);
|
|
45
|
+
const parser = xanoscriptParser(text, scheme, lexResult);
|
|
46
|
+
|
|
47
|
+
// Create a snapshot of the parser's state including symbol table
|
|
48
|
+
const parserState = {
|
|
49
|
+
errors: [...parser.errors],
|
|
50
|
+
warnings: [...parser.warnings],
|
|
51
|
+
informations: [...parser.informations],
|
|
52
|
+
hints: [...parser.hints],
|
|
53
|
+
__symbolTable: parser.__symbolTable
|
|
54
|
+
? JSON.parse(JSON.stringify(parser.__symbolTable))
|
|
55
|
+
: null,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const cacheEntry = {
|
|
59
|
+
version,
|
|
60
|
+
textLength,
|
|
61
|
+
lexResult,
|
|
62
|
+
parserState,
|
|
63
|
+
scheme,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.cache.set(uri, cacheEntry);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
lexResult,
|
|
70
|
+
parser: parserState,
|
|
71
|
+
scheme,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Invalidate cache for a document.
|
|
77
|
+
* @param {string} uri - Document URI
|
|
78
|
+
*/
|
|
79
|
+
invalidate(uri) {
|
|
80
|
+
this.cache.delete(uri);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clear all cached documents.
|
|
85
|
+
*/
|
|
86
|
+
clear() {
|
|
87
|
+
this.cache.clear();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const documentCache = new DocumentCache();
|
package/debug.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* global process */
|
|
2
|
+
/**
|
|
3
|
+
* Debug logging utility.
|
|
4
|
+
* Set XS_DEBUG=1 environment variable to enable debug logging.
|
|
5
|
+
*/
|
|
6
|
+
const isDebug = process.env.XS_DEBUG === "1";
|
|
7
|
+
|
|
8
|
+
export function debugLog(...args) {
|
|
9
|
+
if (isDebug) {
|
|
10
|
+
console.log(...args);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function debugError(...args) {
|
|
15
|
+
if (isDebug) {
|
|
16
|
+
console.error(...args);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/lexer/run.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Identifier } from "./identifier.js";
|
|
2
|
+
import { createTokenByName } from "./utils.js";
|
|
3
|
+
|
|
4
|
+
// "run"
|
|
5
|
+
export const RunToken = createTokenByName("run", {
|
|
6
|
+
longer_alt: Identifier,
|
|
7
|
+
categories: [Identifier],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// "job"
|
|
11
|
+
export const JobToken = createTokenByName("job", {
|
|
12
|
+
longer_alt: Identifier,
|
|
13
|
+
categories: [Identifier],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// "service"
|
|
17
|
+
export const ServiceToken = createTokenByName("service", {
|
|
18
|
+
longer_alt: Identifier,
|
|
19
|
+
categories: [Identifier],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const RunTokens = [RunToken, JobToken, ServiceToken];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maps a token name to a type
|
|
26
|
+
* @param {string} token the token name
|
|
27
|
+
*/
|
|
28
|
+
export function mapTokenToType(token) {
|
|
29
|
+
switch (token) {
|
|
30
|
+
case RunToken.name:
|
|
31
|
+
case JobToken.name:
|
|
32
|
+
case ServiceToken.name:
|
|
33
|
+
return "keyword";
|
|
34
|
+
default:
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/lexer/tokens.js
CHANGED
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
RealtimeTriggerTokens,
|
|
94
94
|
} from "./realtime_trigger.js";
|
|
95
95
|
import { mapTokenToType as mapRedisTokenToType, RedisTokens } from "./redis.js";
|
|
96
|
+
import { mapTokenToType as mapRunTokenToType, RunTokens } from "./run.js";
|
|
96
97
|
import {
|
|
97
98
|
mapTokenToType as mapSecurityTokenToType,
|
|
98
99
|
SecurityTokens,
|
|
@@ -382,6 +383,7 @@ export const allTokens = uniq([
|
|
|
382
383
|
...DbTokens,
|
|
383
384
|
...RedisTokens,
|
|
384
385
|
...TextTokens,
|
|
386
|
+
...RunTokens,
|
|
385
387
|
...ObjectTokens,
|
|
386
388
|
...StreamTokens,
|
|
387
389
|
...DebugTokens,
|
|
@@ -430,6 +432,7 @@ const tokenMappers = [
|
|
|
430
432
|
mapQueryTokenToType,
|
|
431
433
|
mapRealtimeTriggerTokenToType,
|
|
432
434
|
mapRedisTokenToType,
|
|
435
|
+
mapRunTokenToType,
|
|
433
436
|
mapSecurityTokenToType,
|
|
434
437
|
mapStorageTokenToType,
|
|
435
438
|
mapStreamTokenToType,
|
|
@@ -447,80 +450,91 @@ const tokenMappers = [
|
|
|
447
450
|
mapZipTokenToType,
|
|
448
451
|
];
|
|
449
452
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
case ViewToken.name:
|
|
474
|
-
case SchemaToken.name:
|
|
475
|
-
case SecurityToken.name:
|
|
476
|
-
case StackToken.name:
|
|
477
|
-
case TestToken.name:
|
|
478
|
-
case FiltersToken.name:
|
|
479
|
-
return "keyword";
|
|
480
|
-
|
|
481
|
-
case DbLinkToken.name:
|
|
482
|
-
return "function";
|
|
453
|
+
// Pre-built token type map for O(1) lookups
|
|
454
|
+
const tokenTypeMap = new Map();
|
|
455
|
+
|
|
456
|
+
// Build the map at module initialization time
|
|
457
|
+
function buildTokenTypeMap() {
|
|
458
|
+
// Add local token mappings first
|
|
459
|
+
const localMappings = {
|
|
460
|
+
// Structural and control keywords
|
|
461
|
+
[Cachetoken.name]: "keyword",
|
|
462
|
+
[HistoryToken.name]: "keyword",
|
|
463
|
+
[IndexToken.name]: "keyword",
|
|
464
|
+
[InputToken.name]: "keyword",
|
|
465
|
+
[MiddlewareToken.name]: "keyword",
|
|
466
|
+
[MockToken.name]: "keyword",
|
|
467
|
+
[ResponseToken.name]: "keyword",
|
|
468
|
+
[ViewToken.name]: "keyword",
|
|
469
|
+
[SchemaToken.name]: "keyword",
|
|
470
|
+
[SecurityToken.name]: "keyword",
|
|
471
|
+
[StackToken.name]: "keyword",
|
|
472
|
+
[TestToken.name]: "keyword",
|
|
473
|
+
[FiltersToken.name]: "keyword",
|
|
474
|
+
|
|
475
|
+
[DbLinkToken.name]: "function",
|
|
483
476
|
|
|
484
477
|
// Variable-related tokens
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
case DotToken.name:
|
|
510
|
-
return "punctuation";
|
|
511
|
-
|
|
512
|
-
case RegExpToken.name:
|
|
513
|
-
return "regexp";
|
|
514
|
-
|
|
515
|
-
case JsonInToken.name:
|
|
516
|
-
return "operator";
|
|
478
|
+
[AuthToken.name]: "variable",
|
|
479
|
+
[DbIdentifier.name]: "variable",
|
|
480
|
+
[DbReturnAggregateToken.name]: "variable",
|
|
481
|
+
[DescriptionToken.name]: "variable",
|
|
482
|
+
[DisabledToken.name]: "variable",
|
|
483
|
+
[DocsToken.name]: "variable",
|
|
484
|
+
[FieldToken.name]: "variable",
|
|
485
|
+
[GuidToken.name]: "variable",
|
|
486
|
+
[SensitiveToken.name]: "variable",
|
|
487
|
+
[TagsToken.name]: "variable",
|
|
488
|
+
[TypeToken.name]: "variable",
|
|
489
|
+
[ValueToken.name]: "variable",
|
|
490
|
+
[ValuesToken.name]: "variable",
|
|
491
|
+
|
|
492
|
+
[Identifier.name]: "property",
|
|
493
|
+
|
|
494
|
+
[FalseToken.name]: "enumMember",
|
|
495
|
+
[NowToken.name]: "enumMember",
|
|
496
|
+
[NullToken.name]: "enumMember",
|
|
497
|
+
[TrueToken.name]: "enumMember",
|
|
498
|
+
|
|
499
|
+
[DotToken.name]: "punctuation",
|
|
500
|
+
[RegExpToken.name]: "regexp",
|
|
501
|
+
[JsonInToken.name]: "operator",
|
|
517
502
|
|
|
518
503
|
// Skip whitespace and newlines
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
504
|
+
[NewlineToken.name]: null,
|
|
505
|
+
[WhiteSpace.name]: null,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
for (const [tokenName, type] of Object.entries(localMappings)) {
|
|
509
|
+
tokenTypeMap.set(tokenName, type);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Iterate through all tokens and populate the map using the mappers
|
|
513
|
+
for (const token of allTokens) {
|
|
514
|
+
if (tokenTypeMap.has(token.name)) continue;
|
|
522
515
|
|
|
523
|
-
|
|
524
|
-
|
|
516
|
+
for (const mapper of tokenMappers) {
|
|
517
|
+
const type = mapper(token.name);
|
|
518
|
+
if (type) {
|
|
519
|
+
tokenTypeMap.set(token.name, type);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Initialize the map at module load
|
|
527
|
+
buildTokenTypeMap();
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Map a token to a type (e.g., keyword, variable, etc.)
|
|
531
|
+
* Uses pre-built map for O(1) lookup instead of O(n) iteration.
|
|
532
|
+
* @param {string} token - Token name
|
|
533
|
+
* @returns {string | null | undefined} The type of the token
|
|
534
|
+
*/
|
|
535
|
+
export function mapTokenToType(token) {
|
|
536
|
+
if (tokenTypeMap.has(token)) {
|
|
537
|
+
return tokenTypeMap.get(token);
|
|
525
538
|
}
|
|
539
|
+
return undefined; // Skip unmapped or unknown tokens
|
|
526
540
|
}
|
|
@@ -109,22 +109,23 @@ function isAfterPipeToken(tokens) {
|
|
|
109
109
|
return lastToken.tokenType === PipeToken;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
// Pre-computed filter suggestions - frozen to prevent accidental mutations
|
|
113
|
+
const filterSuggestions = Object.freeze(
|
|
114
|
+
filterNames.map((filterName) => {
|
|
114
115
|
const documentation = filterMessageProvider.__filterDoc[filterName];
|
|
115
116
|
|
|
116
|
-
return {
|
|
117
|
+
return Object.freeze({
|
|
117
118
|
label: filterName,
|
|
118
119
|
kind: encodeTokenType("function"), // Filters are function-like
|
|
119
120
|
documentation: documentation
|
|
120
|
-
? {
|
|
121
|
+
? Object.freeze({
|
|
121
122
|
kind: "markdown",
|
|
122
123
|
value: documentation,
|
|
123
|
-
}
|
|
124
|
+
})
|
|
124
125
|
: undefined,
|
|
125
|
-
};
|
|
126
|
-
})
|
|
127
|
-
|
|
126
|
+
});
|
|
127
|
+
})
|
|
128
|
+
);
|
|
128
129
|
|
|
129
130
|
export function getContentAssistSuggestions(text, scheme) {
|
|
130
131
|
try {
|
|
@@ -134,7 +135,7 @@ export function getContentAssistSuggestions(text, scheme) {
|
|
|
134
135
|
|
|
135
136
|
// Check if we're after a pipe token - if so, suggest filters
|
|
136
137
|
if (isAfterPipeToken(partialTokenVector)) {
|
|
137
|
-
return
|
|
138
|
+
return filterSuggestions;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
let syntacticSuggestions;
|
|
@@ -1,5 +1,55 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { documentCache } from "../cache/documentCache.js";
|
|
2
|
+
import { debugError, debugLog } from "../debug.js";
|
|
3
|
+
|
|
4
|
+
// Diagnostic severity constants
|
|
5
|
+
const SEVERITY = {
|
|
6
|
+
ERROR: 1,
|
|
7
|
+
WARNING: 2,
|
|
8
|
+
INFORMATION: 3,
|
|
9
|
+
HINT: 4,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates diagnostics from parser results in a single pass.
|
|
14
|
+
* @param {Object} parser - The parser with errors, warnings, informations, hints
|
|
15
|
+
* @param {Object} document - The text document for position conversion
|
|
16
|
+
* @returns {Array} Array of diagnostic objects
|
|
17
|
+
*/
|
|
18
|
+
function createDiagnostics(parser, document) {
|
|
19
|
+
const diagnostics = [];
|
|
20
|
+
const defaultRange = {
|
|
21
|
+
start: { line: 0, character: 0 },
|
|
22
|
+
end: { line: 0, character: 1 },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const addDiagnostic = (item, severity) => {
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
severity,
|
|
28
|
+
range: item.token
|
|
29
|
+
? {
|
|
30
|
+
start: document.positionAt(item.token.startOffset),
|
|
31
|
+
end: document.positionAt(item.token.endOffset + 1),
|
|
32
|
+
}
|
|
33
|
+
: defaultRange,
|
|
34
|
+
message: item.message,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (const error of parser.errors) {
|
|
39
|
+
addDiagnostic(error, SEVERITY.ERROR);
|
|
40
|
+
}
|
|
41
|
+
for (const warning of parser.warnings) {
|
|
42
|
+
addDiagnostic(warning, SEVERITY.WARNING);
|
|
43
|
+
}
|
|
44
|
+
for (const info of parser.informations) {
|
|
45
|
+
addDiagnostic(info, SEVERITY.INFORMATION);
|
|
46
|
+
}
|
|
47
|
+
for (const hint of parser.hints) {
|
|
48
|
+
addDiagnostic(hint, SEVERITY.HINT);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return diagnostics;
|
|
52
|
+
}
|
|
3
53
|
|
|
4
54
|
/**
|
|
5
55
|
*
|
|
@@ -11,7 +61,7 @@ export function onDidChangeContent(params, connection) {
|
|
|
11
61
|
const document = params.document;
|
|
12
62
|
|
|
13
63
|
if (!document) {
|
|
14
|
-
|
|
64
|
+
debugError(
|
|
15
65
|
"onDidChangeContent(): Document not found for URI:",
|
|
16
66
|
params.textDocument.uri
|
|
17
67
|
);
|
|
@@ -20,96 +70,36 @@ export function onDidChangeContent(params, connection) {
|
|
|
20
70
|
|
|
21
71
|
const text = document.getText();
|
|
22
72
|
|
|
23
|
-
const scheme = getSchemeFromContent(text);
|
|
24
|
-
|
|
25
73
|
try {
|
|
26
|
-
// Parse the XanoScript file
|
|
27
|
-
const parser =
|
|
74
|
+
// Parse the XanoScript file using cache
|
|
75
|
+
const { parser, scheme } = documentCache.getOrParse(
|
|
76
|
+
document.uri,
|
|
77
|
+
document.version,
|
|
78
|
+
text
|
|
79
|
+
);
|
|
28
80
|
|
|
29
|
-
if (parser.errors.length
|
|
30
|
-
// If parsing succeeds, send an empty diagnostics array
|
|
81
|
+
if (parser.errors.length === 0) {
|
|
82
|
+
// If parsing succeeds with no errors, send an empty diagnostics array
|
|
31
83
|
connection.sendDiagnostics({ uri: document.uri, diagnostics: [] });
|
|
32
84
|
}
|
|
33
85
|
|
|
34
86
|
for (const error of parser.errors) {
|
|
35
|
-
|
|
87
|
+
debugError(
|
|
36
88
|
`onDidChangeContent(): Error parsing document: ${error.name}`
|
|
37
89
|
);
|
|
38
90
|
}
|
|
39
91
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
return {
|
|
43
|
-
severity: 1,
|
|
44
|
-
range: error.token
|
|
45
|
-
? {
|
|
46
|
-
start: document.positionAt(error.token.startOffset),
|
|
47
|
-
end: document.positionAt(error.token.endOffset + 1),
|
|
48
|
-
}
|
|
49
|
-
: {
|
|
50
|
-
start: { line: 0, character: 0 },
|
|
51
|
-
end: { line: 0, character: 1 },
|
|
52
|
-
},
|
|
53
|
-
message: error.message,
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const warnings = parser.warnings.map((warning) => {
|
|
58
|
-
return {
|
|
59
|
-
severity: 2,
|
|
60
|
-
range: warning.token
|
|
61
|
-
? {
|
|
62
|
-
start: document.positionAt(warning.token.startOffset),
|
|
63
|
-
end: document.positionAt(warning.token.endOffset + 1),
|
|
64
|
-
}
|
|
65
|
-
: {
|
|
66
|
-
start: { line: 0, character: 0 },
|
|
67
|
-
end: { line: 0, character: 1 },
|
|
68
|
-
},
|
|
69
|
-
message: warning.message,
|
|
70
|
-
};
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const informations = parser.informations.map((info) => {
|
|
74
|
-
return {
|
|
75
|
-
severity: 3,
|
|
76
|
-
range: info.token
|
|
77
|
-
? {
|
|
78
|
-
start: document.positionAt(info.token.startOffset),
|
|
79
|
-
end: document.positionAt(info.token.endOffset + 1),
|
|
80
|
-
}
|
|
81
|
-
: {
|
|
82
|
-
start: { line: 0, character: 0 },
|
|
83
|
-
end: { line: 0, character: 1 },
|
|
84
|
-
},
|
|
85
|
-
message: info.message,
|
|
86
|
-
};
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const hints = parser.hints.map((hint) => {
|
|
90
|
-
return {
|
|
91
|
-
severity: 4,
|
|
92
|
-
range: hint.token
|
|
93
|
-
? {
|
|
94
|
-
start: document.positionAt(hint.token.startOffset),
|
|
95
|
-
end: document.positionAt(hint.token.endOffset + 1),
|
|
96
|
-
}
|
|
97
|
-
: {
|
|
98
|
-
start: { line: 0, character: 0 },
|
|
99
|
-
end: { line: 0, character: 1 },
|
|
100
|
-
},
|
|
101
|
-
message: hint.message,
|
|
102
|
-
};
|
|
103
|
-
});
|
|
92
|
+
// Create diagnostics in a single pass
|
|
93
|
+
const diagnostics = createDiagnostics(parser, document);
|
|
104
94
|
|
|
105
|
-
|
|
95
|
+
debugLog(
|
|
106
96
|
`onDidChangeContent(): sending diagnostic (${parser.errors.length} errors) for scheme:`,
|
|
107
97
|
scheme
|
|
108
98
|
);
|
|
109
99
|
|
|
110
100
|
connection.sendDiagnostics({
|
|
111
101
|
uri: document.uri,
|
|
112
|
-
diagnostics
|
|
102
|
+
diagnostics,
|
|
113
103
|
});
|
|
114
104
|
} catch (error) {
|
|
115
105
|
// If parsing fails, create a diagnostic (error message) to display in VS Code
|
package/onHover/functions.md
CHANGED
|
@@ -287,6 +287,62 @@ A `task` file defines a scheduled job that runs automatically at specified times
|
|
|
287
287
|
|
|
288
288
|
Tasks are ideal for automating recurring operations like generating reports or syncing data.
|
|
289
289
|
|
|
290
|
+
# run.job
|
|
291
|
+
|
|
292
|
+
```xs
|
|
293
|
+
run.job "Gemini -> Image Understanding" {
|
|
294
|
+
main = {
|
|
295
|
+
name: "Gemini -> Image Understanding"
|
|
296
|
+
input: {
|
|
297
|
+
model: "gemini-1.5-flash"
|
|
298
|
+
prompt: "Describe what is happening in this image."
|
|
299
|
+
image: "(attach image file)"
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
env = ["gemini_api_key"]
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
A `run.job` file defines a job configuration for execution in the Xano Job Runner. It includes:
|
|
307
|
+
|
|
308
|
+
- A name (e.g., `"Gemini -> Image Understanding"`) to identify the job,
|
|
309
|
+
- A required `main` attribute specifying the function to execute:
|
|
310
|
+
- `name`: The name of the function to call,
|
|
311
|
+
- `input`: Optional input parameters to pass to the function,
|
|
312
|
+
- An optional `env` array listing environment variable names required by the job.
|
|
313
|
+
|
|
314
|
+
Jobs are used to run functions as standalone processes, typically for long-running or resource-intensive operations.
|
|
315
|
+
|
|
316
|
+
# run.service
|
|
317
|
+
|
|
318
|
+
```xs
|
|
319
|
+
run.service "email proxy" {
|
|
320
|
+
pre = {
|
|
321
|
+
name: "email_proxy_init"
|
|
322
|
+
input: {
|
|
323
|
+
config: "default"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
env = ["email_proxy_api_key"]
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
A `run.service` file defines a service configuration for the Xano Job Runner. It includes:
|
|
331
|
+
|
|
332
|
+
- A name (e.g., `"email proxy"`) to identify the service,
|
|
333
|
+
- An optional `pre` attribute specifying an initialization function to run before the service starts:
|
|
334
|
+
- `name`: The name of the initialization function,
|
|
335
|
+
- `input`: Optional input parameters for the initialization,
|
|
336
|
+
- An optional `env` array listing environment variable names required by the service.
|
|
337
|
+
|
|
338
|
+
Services can also be defined in minimal form without a body:
|
|
339
|
+
|
|
340
|
+
```xs
|
|
341
|
+
run.service "email proxy"
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Services are ideal for long-running background processes like proxies, webhooks listeners, or daemon-style operations.
|
|
345
|
+
|
|
290
346
|
# action.call
|
|
291
347
|
|
|
292
348
|
```xs
|
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { documentCache } from "../cache/documentCache.js";
|
|
2
|
+
import { debugError } from "../debug.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Binary search to find token index at the given offset.
|
|
6
|
+
* Reduces complexity from O(n) to O(log n).
|
|
7
|
+
* @param {Array} tokens - Array of tokens sorted by startOffset
|
|
8
|
+
* @param {number} offset - Cursor offset position
|
|
9
|
+
* @returns {number} Token index or -1 if not found
|
|
10
|
+
*/
|
|
11
|
+
function findTokenAtOffset(tokens, offset) {
|
|
12
|
+
let left = 0;
|
|
13
|
+
let right = tokens.length - 1;
|
|
14
|
+
|
|
15
|
+
while (left <= right) {
|
|
16
|
+
const mid = Math.floor((left + right) / 2);
|
|
17
|
+
const token = tokens[mid];
|
|
18
|
+
|
|
19
|
+
if (token.startOffset <= offset && token.endOffset >= offset) {
|
|
20
|
+
return mid;
|
|
21
|
+
} else if (token.endOffset < offset) {
|
|
22
|
+
left = mid + 1;
|
|
23
|
+
} else {
|
|
24
|
+
right = mid - 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return -1;
|
|
29
|
+
}
|
|
4
30
|
|
|
5
31
|
/**
|
|
6
32
|
*
|
|
@@ -12,7 +38,7 @@ export function onHoverDocument(params, documents, hoverProviders = []) {
|
|
|
12
38
|
const document = documents.get(params.textDocument.uri);
|
|
13
39
|
|
|
14
40
|
if (!document) {
|
|
15
|
-
|
|
41
|
+
debugError(
|
|
16
42
|
"onHover(): Document not found for URI:",
|
|
17
43
|
params.textDocument.uri
|
|
18
44
|
);
|
|
@@ -22,21 +48,19 @@ export function onHoverDocument(params, documents, hoverProviders = []) {
|
|
|
22
48
|
const text = document.getText();
|
|
23
49
|
const offset = document.offsetAt(params.position);
|
|
24
50
|
|
|
25
|
-
//
|
|
26
|
-
const lexResult =
|
|
27
|
-
|
|
51
|
+
// Get cached parse result or parse and cache
|
|
52
|
+
const { lexResult, parser } = documentCache.getOrParse(
|
|
53
|
+
params.textDocument.uri,
|
|
54
|
+
document.version,
|
|
55
|
+
text
|
|
56
|
+
);
|
|
28
57
|
|
|
29
|
-
|
|
30
|
-
const scheme = getSchemeFromContent(text);
|
|
58
|
+
if (lexResult.errors.length > 0) return null;
|
|
31
59
|
|
|
32
|
-
// Parse the XanoScript file
|
|
33
|
-
const parser = xanoscriptParser(text, scheme);
|
|
34
60
|
const tokens = lexResult.tokens;
|
|
35
61
|
|
|
36
|
-
// Find the token under the cursor
|
|
37
|
-
const tokenIdx = tokens
|
|
38
|
-
(token) => token.startOffset <= offset && token.endOffset >= offset
|
|
39
|
-
);
|
|
62
|
+
// Find the token under the cursor using binary search (O(log n))
|
|
63
|
+
const tokenIdx = findTokenAtOffset(tokens, offset);
|
|
40
64
|
|
|
41
65
|
if (tokenIdx === -1) {
|
|
42
66
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { debugLog } from "../debug.js";
|
|
1
2
|
import { lexDocument } from "../lexer/lexer.js";
|
|
2
3
|
import { mapTokenToType } from "../lexer/tokens.js";
|
|
3
4
|
import { encodeTokenType } from "./tokens.js";
|
|
@@ -33,7 +34,7 @@ function higlightDefault(text, SemanticTokensBuilder) {
|
|
|
33
34
|
0 // No modifiers for now
|
|
34
35
|
);
|
|
35
36
|
} else if (tokenType === undefined) {
|
|
36
|
-
|
|
37
|
+
debugLog(
|
|
37
38
|
`token type not mapped to a type: ${JSON.stringify(
|
|
38
39
|
token.tokenType.name
|
|
39
40
|
)}`
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { debugError } from "../debug.js";
|
|
1
2
|
import { getSchemeFromContent } from "../utils";
|
|
2
3
|
import { higlightText } from "./highlight";
|
|
3
4
|
|
|
@@ -11,7 +12,7 @@ export function onSemanticCheck(params, documents, SemanticTokensBuilder) {
|
|
|
11
12
|
const document = documents.get(params.textDocument.uri);
|
|
12
13
|
|
|
13
14
|
if (!document) {
|
|
14
|
-
|
|
15
|
+
debugError(
|
|
15
16
|
"onSemanticCheck(): Document not found for URI:",
|
|
16
17
|
params.textDocument.uri
|
|
17
18
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xano/xanoscript-language-server",
|
|
3
|
-
"version": "11.0
|
|
3
|
+
"version": "11.1.0",
|
|
4
4
|
"description": "Language Server Protocol implementation for XanoScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
@@ -24,10 +24,8 @@
|
|
|
24
24
|
"prepare": "husky"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"chai": "^5.2.0",
|
|
28
27
|
"chevrotain": "^11.0.3",
|
|
29
28
|
"lodash-es": "^4.17.21",
|
|
30
|
-
"mocha": "^11.1.0",
|
|
31
29
|
"vscode-languageserver": "^9.0.1",
|
|
32
30
|
"vscode-languageserver-textdocument": "^1.0.12"
|
|
33
31
|
},
|
|
@@ -38,11 +36,13 @@
|
|
|
38
36
|
},
|
|
39
37
|
"devDependencies": {
|
|
40
38
|
"@eslint/js": "^9.22.0",
|
|
39
|
+
"chai": "^5.2.0",
|
|
41
40
|
"eslint": "^9.22.0",
|
|
42
41
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
43
42
|
"eslint-plugin-unused-imports": "^4.2.0",
|
|
44
43
|
"globals": "^16.0.0",
|
|
45
44
|
"husky": "^9.1.7",
|
|
46
|
-
"lint-staged": "^16.2.4"
|
|
45
|
+
"lint-staged": "^16.2.4",
|
|
46
|
+
"mocha": "^11.1.0"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -32,6 +32,11 @@ describe("docsFieldAttribute", () => {
|
|
|
32
32
|
expect(parser.errors).to.be.empty;
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
it("docsFieldAttribute can be multiline", () => {
|
|
36
|
+
const parser = parse('docs="""\nanother docs\n"""');
|
|
37
|
+
expect(parser.errors).to.be.empty;
|
|
38
|
+
});
|
|
39
|
+
|
|
35
40
|
it("docsFieldAttribute does not require a new line", () => {
|
|
36
41
|
const parser = parse('docs="some docs"');
|
|
37
42
|
expect(parser.errors).to.be.empty;
|
|
@@ -47,12 +47,12 @@ describe("apiCallFn", () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it("apiCallFn requires a url and method field", () => {
|
|
50
|
-
let parser = parse(`
|
|
50
|
+
let parser = parse(`call foo {
|
|
51
51
|
url = "https://www.example.com"
|
|
52
52
|
} as $user`);
|
|
53
53
|
expect(parser.errors).to.not.be.empty;
|
|
54
54
|
|
|
55
|
-
parser = parse(`
|
|
55
|
+
parser = parse(`call foo {
|
|
56
56
|
method = "GET"
|
|
57
57
|
} as $user`);
|
|
58
58
|
expect(parser.errors).to.not.be.empty;
|
|
@@ -5,6 +5,7 @@ import { Identifier, NewlineToken } from "../../lexer/tokens.js";
|
|
|
5
5
|
import { getVarName } from "./utils.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
+
* @deprecated use parser/functions/schema/schemaParseAttributeFn.js instead
|
|
8
9
|
* @param {import('../base_parser.js').XanoBaseParser} $
|
|
9
10
|
*/
|
|
10
11
|
export function objectWithAttributes($) {
|
package/parser/parser.js
CHANGED
|
@@ -5,15 +5,16 @@ import { XanoBaseParser } from "./base_parser.js";
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Will parse the content of the file based on its scheme (db:/, task:/, api:/...)
|
|
8
|
-
* @param {string} scheme
|
|
9
8
|
* @param {string} text
|
|
9
|
+
* @param {string} scheme
|
|
10
|
+
* @param {Object} [preTokenized] - Optional pre-tokenized result from lexDocument to avoid re-lexing
|
|
10
11
|
* @returns
|
|
11
12
|
*/
|
|
12
|
-
export function xanoscriptParser(text, scheme) {
|
|
13
|
+
export function xanoscriptParser(text, scheme, preTokenized = null) {
|
|
13
14
|
if (!scheme) {
|
|
14
15
|
scheme = getSchemeFromContent(text);
|
|
15
16
|
}
|
|
16
|
-
const lexResult = lexDocument(text);
|
|
17
|
+
const lexResult = preTokenized || lexDocument(text);
|
|
17
18
|
parser.input = lexResult.tokens;
|
|
18
19
|
switch (scheme.toLowerCase()) {
|
|
19
20
|
case "addon":
|
|
@@ -52,6 +53,9 @@ export function xanoscriptParser(text, scheme) {
|
|
|
52
53
|
case "realtime_channel":
|
|
53
54
|
parser.realtimeChannelDeclaration();
|
|
54
55
|
return parser;
|
|
56
|
+
case "run":
|
|
57
|
+
parser.runDeclaration();
|
|
58
|
+
return parser;
|
|
55
59
|
case "table_trigger":
|
|
56
60
|
parser.tableTriggerDeclaration();
|
|
57
61
|
return parser;
|
package/parser/register.js
CHANGED
|
@@ -16,6 +16,11 @@ import { middlewareDeclaration } from "./middleware_parser.js";
|
|
|
16
16
|
import { queryDeclaration } from "./query_parser.js";
|
|
17
17
|
import { realtimeChannelDeclaration } from "./realtime_channel_parser.js";
|
|
18
18
|
import { realtimeTriggerDeclaration } from "./realtime_trigger_parser.js";
|
|
19
|
+
import {
|
|
20
|
+
runDeclaration,
|
|
21
|
+
runJobClause,
|
|
22
|
+
runServiceClause,
|
|
23
|
+
} from "./run_parser.js";
|
|
19
24
|
import { tableDeclaration } from "./table_parser.js";
|
|
20
25
|
import { tableTriggerDeclaration } from "./table_trigger_parser.js";
|
|
21
26
|
import { taskDeclaration } from "./task_parser.js";
|
|
@@ -66,6 +71,9 @@ export const register = ($) => {
|
|
|
66
71
|
"realtimeChannelDeclaration",
|
|
67
72
|
realtimeChannelDeclaration($)
|
|
68
73
|
);
|
|
74
|
+
$.runDeclaration = $.RULE("runDeclaration", runDeclaration($));
|
|
75
|
+
$.runJobClause = $.RULE("runJobClause", runJobClause($));
|
|
76
|
+
$.runServiceClause = $.RULE("runServiceClause", runServiceClause($));
|
|
69
77
|
$.tableTriggerDeclaration = $.RULE(
|
|
70
78
|
"tableTriggerDeclaration",
|
|
71
79
|
tableTriggerDeclaration($)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { StringLiteral } from "../lexer/literal.js";
|
|
2
|
+
import { JobToken, RunToken, ServiceToken } from "../lexer/run.js";
|
|
3
|
+
import { DotToken, Identifier, NewlineToken } from "../lexer/tokens.js";
|
|
4
|
+
|
|
5
|
+
export function runDeclaration($) {
|
|
6
|
+
return () => {
|
|
7
|
+
$.sectionStack.push("runDeclaration");
|
|
8
|
+
// Allow leading comments and newlines before the run declaration
|
|
9
|
+
$.SUBRULE($.optionalCommentBlockFn);
|
|
10
|
+
|
|
11
|
+
$.CONSUME(RunToken); // "run"
|
|
12
|
+
$.CONSUME(DotToken); // "."
|
|
13
|
+
|
|
14
|
+
$.OR([
|
|
15
|
+
{ ALT: () => $.SUBRULE($.runJobClause) },
|
|
16
|
+
{ ALT: () => $.SUBRULE($.runServiceClause) },
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
$.MANY2(() => $.CONSUME2(NewlineToken)); // optional trailing newlines
|
|
20
|
+
$.sectionStack.pop();
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function runJobClause($) {
|
|
25
|
+
return () => {
|
|
26
|
+
const parent = $.CONSUME(JobToken); // "job"
|
|
27
|
+
|
|
28
|
+
$.OR([
|
|
29
|
+
{ ALT: () => $.CONSUME(StringLiteral) },
|
|
30
|
+
{ ALT: () => $.CONSUME(Identifier) },
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
$.SUBRULE($.schemaParseAttributeFn, {
|
|
34
|
+
ARGS: [
|
|
35
|
+
parent,
|
|
36
|
+
{
|
|
37
|
+
main: {
|
|
38
|
+
name: "[string]",
|
|
39
|
+
input: { "[string]?": "[constant]" },
|
|
40
|
+
},
|
|
41
|
+
"env?": ["[string]"],
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function runServiceClause($) {
|
|
49
|
+
return () => {
|
|
50
|
+
const parent = $.CONSUME(ServiceToken); // "service"
|
|
51
|
+
|
|
52
|
+
$.OR([
|
|
53
|
+
{ ALT: () => $.CONSUME(StringLiteral) },
|
|
54
|
+
{ ALT: () => $.CONSUME(Identifier) },
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
$.SUBRULE($.schemaParseAttributeFn, {
|
|
58
|
+
ARGS: [
|
|
59
|
+
parent,
|
|
60
|
+
{
|
|
61
|
+
"pre?": {
|
|
62
|
+
name: "[string]",
|
|
63
|
+
input: { "[string]?": "[constant]" },
|
|
64
|
+
},
|
|
65
|
+
"env?": ["[string]"],
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { describe, it } from "mocha";
|
|
3
|
+
import { xanoscriptParser } from "./parser.js";
|
|
4
|
+
|
|
5
|
+
describe("run", () => {
|
|
6
|
+
it("should parse a basic run.job", () => {
|
|
7
|
+
const parser = xanoscriptParser(`run.job "Average of values" {
|
|
8
|
+
main = {name: "avg_value", input: {}}
|
|
9
|
+
}`);
|
|
10
|
+
expect(parser.errors).to.be.empty;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("should parse a basic run.job with inputs", () => {
|
|
14
|
+
const parser = xanoscriptParser(`run.job "Average of values" {
|
|
15
|
+
main = {name: "avg_value", input: {left: 1, right: 2}}
|
|
16
|
+
}`);
|
|
17
|
+
expect(parser.errors).to.be.empty;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("run.job requires a main attribute", () => {
|
|
21
|
+
let parser = xanoscriptParser(`run.job "Average of values" {
|
|
22
|
+
pre = {name: "avg_value", input: {left: 1, right: 2}}
|
|
23
|
+
}`);
|
|
24
|
+
expect(parser.errors).to.not.be.empty;
|
|
25
|
+
|
|
26
|
+
parser = xanoscriptParser(`run.job "Average of values" {}`);
|
|
27
|
+
expect(parser.errors).to.not.be.empty;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should accept env variable", () => {
|
|
31
|
+
const parser = xanoscriptParser(`run.job "Gemini -> Image Understanding" {
|
|
32
|
+
main = {
|
|
33
|
+
name : "Gemini -> Image Understanding"
|
|
34
|
+
input: {
|
|
35
|
+
model : "gemini-1.5-flash"
|
|
36
|
+
prompt: "Describe what is happening in this image."
|
|
37
|
+
image : "(attach image file)"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
env = ["gemini_api_key"]
|
|
42
|
+
}`);
|
|
43
|
+
expect(parser.errors).to.be.empty;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("should parse a basic run.service", () => {
|
|
47
|
+
const parser = xanoscriptParser(`run.service "email proxy" {
|
|
48
|
+
pre = {name: "email_proxy_fn", input: {}}
|
|
49
|
+
}`);
|
|
50
|
+
expect(parser.errors).to.be.empty;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("run.service has a minimal form", () => {
|
|
54
|
+
const parser = xanoscriptParser(`run.service "email proxy"`);
|
|
55
|
+
expect(parser.errors).to.be.empty;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("environment variables can only be an array of strings", () => {
|
|
59
|
+
let parser = xanoscriptParser(`run.service "email proxy" {
|
|
60
|
+
env = ["email_proxy_api_key", 123, true]
|
|
61
|
+
}`);
|
|
62
|
+
expect(parser.errors).to.not.be.empty;
|
|
63
|
+
|
|
64
|
+
parser = xanoscriptParser(`run.job "Average of values" {
|
|
65
|
+
main = {name: "avg_value", input: {}}
|
|
66
|
+
env = ["email_proxy_api_key", 123, true]
|
|
67
|
+
}`);
|
|
68
|
+
expect(parser.errors).to.not.be.empty;
|
|
69
|
+
|
|
70
|
+
parser = xanoscriptParser(`run.service "email proxy" {
|
|
71
|
+
env = []
|
|
72
|
+
}`);
|
|
73
|
+
expect(parser.errors).to.be.empty;
|
|
74
|
+
|
|
75
|
+
parser = xanoscriptParser(`run.service "email proxy" {
|
|
76
|
+
env = ["a_value", "another value"]
|
|
77
|
+
}`);
|
|
78
|
+
expect(parser.errors).to.be.empty;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("run.service does not require a pre", () => {
|
|
82
|
+
const parser = xanoscriptParser(`run.service "email proxy"`);
|
|
83
|
+
expect(parser.errors).to.be.empty;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("run.service does not accept a main", () => {
|
|
87
|
+
const parser = xanoscriptParser(`run.service "email proxy" {
|
|
88
|
+
main = { name : "some_fn", input: {} }
|
|
89
|
+
}`);
|
|
90
|
+
expect(parser.errors).to.not.be.empty;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("run.service accepts env variables", () => {
|
|
94
|
+
const parser = xanoscriptParser(`run.service "email proxy" {
|
|
95
|
+
pre = {name: "email_proxy_init", input: {}}
|
|
96
|
+
env = ["email_proxy_api_key"]
|
|
97
|
+
}`);
|
|
98
|
+
expect(parser.errors).to.be.empty;
|
|
99
|
+
});
|
|
100
|
+
});
|
package/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
TextDocuments,
|
|
6
6
|
} from "vscode-languageserver/node.js";
|
|
7
7
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
8
|
+
import { debugLog } from "./debug.js";
|
|
8
9
|
import { onCompletion } from "./onCompletion/onCompletion.js";
|
|
9
10
|
import { onDidChangeContent } from "./onDidChangeContent/onDidChangeContent.js";
|
|
10
11
|
import { onHover } from "./onHover/onHover.js";
|
|
@@ -50,7 +51,7 @@ documents.onDidChangeContent((params) =>
|
|
|
50
51
|
onDidChangeContent(params, connection)
|
|
51
52
|
);
|
|
52
53
|
connection.onDidOpenTextDocument((params) => {
|
|
53
|
-
|
|
54
|
+
debugLog("Document opened:", params.textDocument.uri);
|
|
54
55
|
// Existing handler logic
|
|
55
56
|
});
|
|
56
57
|
|
package/utils.js
CHANGED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm test:*)",
|
|
5
|
-
"Bash(npm run lint)",
|
|
6
|
-
"Bash(npm run test:*)",
|
|
7
|
-
"Bash(npx eslint:*)",
|
|
8
|
-
"Bash(node:*)",
|
|
9
|
-
"Bash(npm install)",
|
|
10
|
-
"Bash(grep:*)",
|
|
11
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server log --all --pretty=format:\"%h %s\")",
|
|
12
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server log --all -S \"password\\\\|secret\\\\|api_key\\\\|token\" --oneline)",
|
|
13
|
-
"Bash(xargs file:*)",
|
|
14
|
-
"Bash(find:*)",
|
|
15
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server log -p --all -S \"webhook\\\\|Bearer\\\\|discord\\\\|slack\\\\|xano_insiders\" -- \"*.xs\" \"*.js\")",
|
|
16
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server ls-files:*)",
|
|
17
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server status)",
|
|
18
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server diff --stat)",
|
|
19
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server log --oneline -5)",
|
|
20
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server add README.md LICENSE package.json lexer/tests/query/valid_sources/basic_query.xs parser/functions/stream/streamFromRequestFn.js parser/functions/stream/streamFromRequestFn.spec.js parser/tests/function/valid_sources/discord_poll_send_to_slack.xs parser/tests/query/valid_sources/all_basics.xs)",
|
|
21
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server commit -m \"$\\(cat <<''EOF''\nPrepare package for public npm release as @xano/xanoscript-language-server\n\n- Rename package to @xano/xanoscript-language-server\n- Add MIT LICENSE file\n- Add npm metadata \\(description, repository, author, license, bugs, homepage\\)\n- Remove private flag to allow publishing\n- Sanitize test files: replace internal URLs, IDs, and env var names with placeholders\n- Simplify README maintainer section\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n\\)\")",
|
|
22
|
-
"Bash(git -C /Users/justinalbrecht/git/xs-language-server log -1 --oneline)",
|
|
23
|
-
"Bash(git add:*)",
|
|
24
|
-
"Bash(git commit:*)",
|
|
25
|
-
"Bash(git push)",
|
|
26
|
-
"Bash(npm whoami:*)",
|
|
27
|
-
"Bash(npm view:*)",
|
|
28
|
-
"Bash(npm version:*)",
|
|
29
|
-
"Bash(npm publish:*)"
|
|
30
|
-
],
|
|
31
|
-
"deny": [],
|
|
32
|
-
"ask": []
|
|
33
|
-
}
|
|
34
|
-
}
|