tree-sitter-xonsh 0.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/README.md +50 -0
- package/binding.gyp +35 -0
- package/bindings/node/binding.cc +19 -0
- package/bindings/node/binding_test.js +9 -0
- package/bindings/node/index.d.ts +27 -0
- package/bindings/node/index.js +11 -0
- package/grammar.js +461 -0
- package/package.json +41 -0
- package/queries/highlights.scm +526 -0
- package/queries/injections.scm +23 -0
- package/queries/locals.scm +165 -0
- package/src/grammar.json +8187 -0
- package/src/node-types.json +5042 -0
- package/src/parser.c +229118 -0
- package/src/parser.o +0 -0
- package/src/scanner.c +1266 -0
- package/src/scanner.o +0 -0
- package/src/tree_sitter/alloc.h +54 -0
- package/src/tree_sitter/array.h +291 -0
- package/src/tree_sitter/parser.h +286 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Xonsh grammar for TreeSitter
|
|
2
|
+
|
|
3
|
+
A [tree-sitter](https://tree-sitter.github.io/) grammar for [xonsh](https://xon.sh/), the Python-powered shell.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Xonsh extends Python 3 with shell-like syntax for subprocess execution. This grammar extends `tree-sitter-python` with xonsh-specific constructs.
|
|
8
|
+
|
|
9
|
+
> [!IMPORTANT]
|
|
10
|
+
> - This should be treated as experimental beta-stage software. The output tree layout would change.
|
|
11
|
+
> - Some limitations are forced by the fact that tree-sitter is context-free while some xonsh constructs are resolvable only at runtime.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### Building from source
|
|
16
|
+
```bash
|
|
17
|
+
git clone https://github.com/FoamScience/tree-sitter-xonsh
|
|
18
|
+
cd tree-sitter-xonsh
|
|
19
|
+
npm install
|
|
20
|
+
tree-sitter generate
|
|
21
|
+
tree-sitter parse <your_file>.xsh
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Known Limitations
|
|
25
|
+
|
|
26
|
+
1. **Unknown commands parsed as Python** instead of a bare subprocess command.
|
|
27
|
+
- Workaround: Use explicit subprocess syntax: `$[mycommand]` instead of just `mycommand`
|
|
28
|
+
- This is an effect of scanner-based approaches, for context-bound xonsh subprocesses.
|
|
29
|
+
|
|
30
|
+
## Architecture
|
|
31
|
+
|
|
32
|
+
This grammar extends [tree-sitter-python](https://github.com/tree-sitter/tree-sitter-python). Key components:
|
|
33
|
+
|
|
34
|
+
- **grammar.js**: Defines xonsh-specific rules and overrides Python rules where needed
|
|
35
|
+
- **scanner.c**: External scanner for:
|
|
36
|
+
- Bare subprocess detection (heuristic-based)
|
|
37
|
+
- `@` symbol usage disambiguation (decorator vs `@(...)` vs `@.attr` vs `@modifier`)
|
|
38
|
+
- Subprocess modifier handling (`@json`, `@unthread`, etc.)
|
|
39
|
+
- `&&`/`||` vs `&` disambiguation
|
|
40
|
+
- Brace expansion vs literal detection
|
|
41
|
+
- Python's indent/dedent handling (inherited)
|
|
42
|
+
- String delimiter handling (inherited)
|
|
43
|
+
- **queries/highlights.scm** provides syntax highlighting queries for Neovim.
|
|
44
|
+
- The TreeSitter CLI can read those, but will render the highlighting differently.
|
|
45
|
+
|
|
46
|
+
> Currently the scanner may look-ahead a whole line, which can affect performance.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
package/binding.gyp
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [
|
|
3
|
+
{
|
|
4
|
+
"target_name": "tree_sitter_xonsh_binding",
|
|
5
|
+
"dependencies": [
|
|
6
|
+
"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
|
|
7
|
+
],
|
|
8
|
+
"include_dirs": [
|
|
9
|
+
"src",
|
|
10
|
+
],
|
|
11
|
+
"sources": [
|
|
12
|
+
"bindings/node/binding.cc",
|
|
13
|
+
"src/parser.c",
|
|
14
|
+
],
|
|
15
|
+
"variables": {
|
|
16
|
+
"has_scanner": "<!(node -p \"fs.existsSync('src/scanner.c')\")"
|
|
17
|
+
},
|
|
18
|
+
"conditions": [
|
|
19
|
+
["has_scanner=='true'", {
|
|
20
|
+
"sources+": ["src/scanner.c"],
|
|
21
|
+
}],
|
|
22
|
+
["OS!='win'", {
|
|
23
|
+
"cflags_c": [
|
|
24
|
+
"-std=c11",
|
|
25
|
+
],
|
|
26
|
+
}, { # OS == "win"
|
|
27
|
+
"cflags_c": [
|
|
28
|
+
"/std:c11",
|
|
29
|
+
"/utf-8",
|
|
30
|
+
],
|
|
31
|
+
}],
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#include <napi.h>
|
|
2
|
+
|
|
3
|
+
typedef struct TSLanguage TSLanguage;
|
|
4
|
+
|
|
5
|
+
extern "C" TSLanguage *tree_sitter_xonsh();
|
|
6
|
+
|
|
7
|
+
// "tree-sitter", "language" hashed with BLAKE2
|
|
8
|
+
const napi_type_tag LANGUAGE_TYPE_TAG = {
|
|
9
|
+
0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
13
|
+
auto language = Napi::External<TSLanguage>::New(env, tree_sitter_xonsh());
|
|
14
|
+
language.TypeTag(&LANGUAGE_TYPE_TAG);
|
|
15
|
+
exports["language"] = language;
|
|
16
|
+
return exports;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
NODE_API_MODULE(tree_sitter_xonsh_binding, Init)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
type BaseNode = {
|
|
2
|
+
type: string;
|
|
3
|
+
named: boolean;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type ChildNode = {
|
|
7
|
+
multiple: boolean;
|
|
8
|
+
required: boolean;
|
|
9
|
+
types: BaseNode[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type NodeInfo =
|
|
13
|
+
| (BaseNode & {
|
|
14
|
+
subtypes: BaseNode[];
|
|
15
|
+
})
|
|
16
|
+
| (BaseNode & {
|
|
17
|
+
fields: { [name: string]: ChildNode };
|
|
18
|
+
children: ChildNode[];
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
type Language = {
|
|
22
|
+
language: unknown;
|
|
23
|
+
nodeTypeInfo: NodeInfo[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
declare const language: Language;
|
|
27
|
+
export = language;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const root = require("path").join(__dirname, "..", "..");
|
|
2
|
+
|
|
3
|
+
module.exports =
|
|
4
|
+
typeof process.versions.bun === "string"
|
|
5
|
+
// Support `bun build --compile` by being statically analyzable enough to find the .node file at build-time
|
|
6
|
+
? require(`../../prebuilds/${process.platform}-${process.arch}/tree-sitter-xonsh.node`)
|
|
7
|
+
: require("node-gyp-build")(root);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
module.exports.nodeTypeInfo = require("../../src/node-types.json");
|
|
11
|
+
} catch (_) {}
|
package/grammar.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Xonsh grammar for tree-sitter
|
|
3
|
+
* @author Mohammed Elwardi Fadeli
|
|
4
|
+
* @license MIT
|
|
5
|
+
*
|
|
6
|
+
* Xonsh extends Python with shell-like syntax for subprocess execution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/// <reference types="tree-sitter-cli/dsl" />
|
|
10
|
+
// @ts-check
|
|
11
|
+
|
|
12
|
+
const Python = require('tree-sitter-python/grammar');
|
|
13
|
+
|
|
14
|
+
module.exports = grammar(Python, {
|
|
15
|
+
name: 'xonsh',
|
|
16
|
+
|
|
17
|
+
// Override externals to add xonsh-specific tokens from scanner
|
|
18
|
+
externals: ($, original) => original.concat([
|
|
19
|
+
$._subprocess_start, // Bare subprocess detection from scanner
|
|
20
|
+
$._logical_and, // && operator (disambiguated from &)
|
|
21
|
+
$._logical_or, // || operator
|
|
22
|
+
$._background_amp, // Single & for background execution
|
|
23
|
+
$._keyword_and, // 'and' keyword in subprocess context
|
|
24
|
+
$._keyword_or, // 'or' keyword in subprocess context
|
|
25
|
+
$._subprocess_macro_start, // Subprocess macro: identifier! (consumed by scanner)
|
|
26
|
+
]),
|
|
27
|
+
|
|
28
|
+
rules: {
|
|
29
|
+
|
|
30
|
+
// Env. Vars
|
|
31
|
+
env_variable: $ => seq('$', $.identifier),
|
|
32
|
+
env_variable_braced: $ => seq(
|
|
33
|
+
'${',
|
|
34
|
+
field('expression', $.expression),
|
|
35
|
+
'}',
|
|
36
|
+
),
|
|
37
|
+
|
|
38
|
+
// Env. Var. Assignment: $VAR = value
|
|
39
|
+
env_assignment: $ => seq(
|
|
40
|
+
field('left', $.env_variable),
|
|
41
|
+
'=',
|
|
42
|
+
field('right', $.expression),
|
|
43
|
+
),
|
|
44
|
+
|
|
45
|
+
// Env. Var. Deletion: del $VAR
|
|
46
|
+
env_deletion: $ => prec(2, seq(
|
|
47
|
+
'del',
|
|
48
|
+
field('target', $.env_variable),
|
|
49
|
+
)),
|
|
50
|
+
|
|
51
|
+
// Scoped Env. Var.: $VAR=value cmd
|
|
52
|
+
env_scoped_command: $ => prec(5, seq(
|
|
53
|
+
field('env', repeat1($.env_prefix)),
|
|
54
|
+
field('command', $.subprocess_body),
|
|
55
|
+
)),
|
|
56
|
+
env_prefix: $ => seq(
|
|
57
|
+
$.env_variable,
|
|
58
|
+
token.immediate('='),
|
|
59
|
+
field('value', choice(
|
|
60
|
+
$.string,
|
|
61
|
+
$.identifier,
|
|
62
|
+
$.integer,
|
|
63
|
+
)),
|
|
64
|
+
),
|
|
65
|
+
|
|
66
|
+
// Subprocess Operators
|
|
67
|
+
captured_subprocess: $ => seq(
|
|
68
|
+
'$(',
|
|
69
|
+
field('modifier', optional($.subprocess_modifier)),
|
|
70
|
+
field('body', optional($.subprocess_body)),
|
|
71
|
+
')',
|
|
72
|
+
),
|
|
73
|
+
|
|
74
|
+
// Subprocess output modifiers: @json, @yaml, @name, etc.
|
|
75
|
+
// Built-ins transform output into Python objects.
|
|
76
|
+
// Users can also define custom decorator aliases (e.g., @noerr, @path).
|
|
77
|
+
// Higher precedence than custom_function_glob to resolve @identifier ambiguity
|
|
78
|
+
subprocess_modifier: $ => prec(2, seq('@', $.identifier)),
|
|
79
|
+
captured_subprocess_object: $ => seq(
|
|
80
|
+
'!(',
|
|
81
|
+
field('modifier', optional($.subprocess_modifier)),
|
|
82
|
+
field('body', optional($.subprocess_body)),
|
|
83
|
+
')',
|
|
84
|
+
),
|
|
85
|
+
uncaptured_subprocess: $ => seq(
|
|
86
|
+
'$[',
|
|
87
|
+
field('modifier', optional($.subprocess_modifier)),
|
|
88
|
+
field('body', optional($.subprocess_body)),
|
|
89
|
+
']',
|
|
90
|
+
),
|
|
91
|
+
uncaptured_subprocess_object: $ => seq(
|
|
92
|
+
'![',
|
|
93
|
+
field('modifier', optional($.subprocess_modifier)),
|
|
94
|
+
field('body', optional($.subprocess_body)),
|
|
95
|
+
']',
|
|
96
|
+
),
|
|
97
|
+
|
|
98
|
+
// Python Evaluation in Subprocess Context
|
|
99
|
+
python_evaluation: $ => seq(
|
|
100
|
+
'@(',
|
|
101
|
+
field('expression', $.expression),
|
|
102
|
+
')',
|
|
103
|
+
),
|
|
104
|
+
tokenized_substitution: $ => seq(
|
|
105
|
+
'@$(',
|
|
106
|
+
field('body', optional($.subprocess_body)),
|
|
107
|
+
')',
|
|
108
|
+
),
|
|
109
|
+
|
|
110
|
+
// Special @ Object Access: @.env, @.lastcmd, etc.
|
|
111
|
+
at_object: $ => seq(
|
|
112
|
+
'@',
|
|
113
|
+
'.',
|
|
114
|
+
field('attribute', $.identifier),
|
|
115
|
+
),
|
|
116
|
+
|
|
117
|
+
// Help Operators: expr? and expr??
|
|
118
|
+
help_expression: $ => seq(
|
|
119
|
+
field('expression', $.expression),
|
|
120
|
+
'?',
|
|
121
|
+
),
|
|
122
|
+
super_help_expression: $ => seq(
|
|
123
|
+
field('expression', $.expression),
|
|
124
|
+
'??',
|
|
125
|
+
),
|
|
126
|
+
|
|
127
|
+
// Glob Patterns
|
|
128
|
+
// Regex glob: `pattern` or r`pattern`
|
|
129
|
+
regex_glob: $ => seq(
|
|
130
|
+
choice('`', 'r`'),
|
|
131
|
+
field('pattern', alias(/[^`]+/, $.regex_glob_content)),
|
|
132
|
+
'`',
|
|
133
|
+
),
|
|
134
|
+
|
|
135
|
+
// Regex path glob - returns Path objects: rp`pattern`
|
|
136
|
+
regex_path_glob: $ => seq(
|
|
137
|
+
'rp`',
|
|
138
|
+
field('pattern', alias(/[^`]+/, $.regex_path_content)),
|
|
139
|
+
'`',
|
|
140
|
+
),
|
|
141
|
+
glob_pattern: $ => seq(
|
|
142
|
+
'g`',
|
|
143
|
+
field('pattern', alias(/[^`]+/, $.glob_pattern_content)),
|
|
144
|
+
'`',
|
|
145
|
+
),
|
|
146
|
+
|
|
147
|
+
// Formatted glob with variable substitution: f`pattern`
|
|
148
|
+
formatted_glob: $ => seq(
|
|
149
|
+
'f`',
|
|
150
|
+
field('pattern', alias(/[^`]+/, $.formatted_glob_content)),
|
|
151
|
+
'`',
|
|
152
|
+
),
|
|
153
|
+
|
|
154
|
+
// Glob path - returns Path objects instead of strings: gp`pattern`
|
|
155
|
+
glob_path: $ => seq(
|
|
156
|
+
'gp`',
|
|
157
|
+
field('pattern', alias(/[^`]+/, $.glob_path_content)),
|
|
158
|
+
'`',
|
|
159
|
+
),
|
|
160
|
+
|
|
161
|
+
// Custom function glob: @func`pattern`
|
|
162
|
+
// Calls a Python function with the pattern string
|
|
163
|
+
// e.g., @foo`bi` calls foo("bi") and expects a list of strings
|
|
164
|
+
// High precedence to win against modified_bare_subprocess
|
|
165
|
+
custom_function_glob: $ => prec(10, seq(
|
|
166
|
+
'@',
|
|
167
|
+
field('function', $.identifier),
|
|
168
|
+
token.immediate('`'),
|
|
169
|
+
field('pattern', alias(/[^`]*/, $.custom_glob_content)),
|
|
170
|
+
'`',
|
|
171
|
+
)),
|
|
172
|
+
|
|
173
|
+
// Path Literals
|
|
174
|
+
// Xonsh path prefixes: p (basic), pf (formatted), pr (raw)
|
|
175
|
+
path_string: $ => seq(
|
|
176
|
+
field('prefix', alias(choice('p', 'pf', 'pr', 'P', 'PF', 'PR'), $.path_prefix)),
|
|
177
|
+
field('string', $.string),
|
|
178
|
+
),
|
|
179
|
+
|
|
180
|
+
// Subprocess body
|
|
181
|
+
subprocess_body: $ => seq(
|
|
182
|
+
$.subprocess_command,
|
|
183
|
+
repeat(choice(
|
|
184
|
+
$.subprocess_pipeline,
|
|
185
|
+
$.subprocess_logical,
|
|
186
|
+
)),
|
|
187
|
+
),
|
|
188
|
+
|
|
189
|
+
// Subprocess command and args
|
|
190
|
+
subprocess_command: $ => repeat1($.subprocess_argument),
|
|
191
|
+
subprocess_argument: $ => choice(
|
|
192
|
+
$.subprocess_word,
|
|
193
|
+
$.string,
|
|
194
|
+
$.env_variable,
|
|
195
|
+
$.env_variable_braced,
|
|
196
|
+
$.python_evaluation,
|
|
197
|
+
$.captured_subprocess,
|
|
198
|
+
$.uncaptured_subprocess,
|
|
199
|
+
$.tokenized_substitution,
|
|
200
|
+
$.regex_glob,
|
|
201
|
+
$.glob_pattern,
|
|
202
|
+
$.formatted_glob,
|
|
203
|
+
$.glob_path,
|
|
204
|
+
$.regex_path_glob,
|
|
205
|
+
$.custom_function_glob,
|
|
206
|
+
$.brace_expansion,
|
|
207
|
+
$.brace_literal,
|
|
208
|
+
$.subprocess_redirect,
|
|
209
|
+
),
|
|
210
|
+
|
|
211
|
+
// Brace expansion in subprocess context
|
|
212
|
+
// Range: {1..5}, {a..z} or List: {a,b,c}
|
|
213
|
+
brace_expansion: $ => choice(
|
|
214
|
+
// Range expansion: {start..end} - match entire pattern with token
|
|
215
|
+
alias(
|
|
216
|
+
token(seq('{', /\w+/, '..', /\w+/, '}')),
|
|
217
|
+
$.brace_range
|
|
218
|
+
),
|
|
219
|
+
// List expansion: {a,b,c} - dots excluded from items
|
|
220
|
+
seq(
|
|
221
|
+
'{',
|
|
222
|
+
alias(/[^{},.]+/, $.brace_item),
|
|
223
|
+
repeat1(seq(',', alias(/[^{},.]+/, $.brace_item))),
|
|
224
|
+
'}',
|
|
225
|
+
),
|
|
226
|
+
),
|
|
227
|
+
|
|
228
|
+
// Brace literal: {content} without expansion syntax
|
|
229
|
+
// Used when braces contain content that doesn't match expansion patterns
|
|
230
|
+
// e.g., {123} in "bash -c! echo {123}" is literal, not expansion
|
|
231
|
+
// token() avoids conflicts with brace_expansion
|
|
232
|
+
brace_literal: _ => token(prec(1, seq('{', /[^{},.\s]+/, '}'))),
|
|
233
|
+
|
|
234
|
+
// Subprocess word - any sequence of non-special characters
|
|
235
|
+
// Use token() with high precedence to prevent ! from being leaked as a separate token by other rules
|
|
236
|
+
// Allows backslash escapes (e.g., \; \$ \space)
|
|
237
|
+
// Allows @ in middle of words (e.g., user@host in URLs) but not at word start
|
|
238
|
+
subprocess_word: _ => token(prec(100, /([^\s$@`'"()\[\]{}|<>&;\\](@[^\s$`'"()\[\]{}|<>&;\\]+)?|\\[^\n])+/)),
|
|
239
|
+
|
|
240
|
+
subprocess_pipeline: $ => seq(
|
|
241
|
+
$.pipe_operator,
|
|
242
|
+
$.subprocess_command,
|
|
243
|
+
),
|
|
244
|
+
|
|
245
|
+
// Xonsh pipe operators:
|
|
246
|
+
// | - pipe stdout
|
|
247
|
+
// e| - pipe stderr (err|)
|
|
248
|
+
// a| - pipe both stdout and stderr (alias: all|)
|
|
249
|
+
// Use token(prec(101, ...)) for letter-prefixed operators
|
|
250
|
+
pipe_operator: _ => choice(
|
|
251
|
+
'|',
|
|
252
|
+
token(prec(101, 'e|')),
|
|
253
|
+
token(prec(101, 'err|')),
|
|
254
|
+
token(prec(101, 'a|')),
|
|
255
|
+
token(prec(101, 'all|')),
|
|
256
|
+
),
|
|
257
|
+
|
|
258
|
+
// Subprocess Logical Operators: && and ||
|
|
259
|
+
subprocess_logical: $ => seq(
|
|
260
|
+
field('operator', $.logical_operator),
|
|
261
|
+
$.subprocess_command,
|
|
262
|
+
),
|
|
263
|
+
|
|
264
|
+
// Xonsh supports both symbolic (&&, ||) and keyword (and, or) operators.
|
|
265
|
+
// The scanner should handle disambiguation for all of these.
|
|
266
|
+
logical_operator: $ => choice(
|
|
267
|
+
$._logical_and,
|
|
268
|
+
$._logical_or,
|
|
269
|
+
$._keyword_and,
|
|
270
|
+
$._keyword_or,
|
|
271
|
+
),
|
|
272
|
+
|
|
273
|
+
// Redirections
|
|
274
|
+
subprocess_redirect: $ => choice(
|
|
275
|
+
// Redirects with targets
|
|
276
|
+
seq(
|
|
277
|
+
field('operator', $.redirect_operator),
|
|
278
|
+
field('target', $.redirect_target),
|
|
279
|
+
),
|
|
280
|
+
// Stream merging (no target needed)
|
|
281
|
+
field('operator', $.stream_merge_operator),
|
|
282
|
+
),
|
|
283
|
+
|
|
284
|
+
// Use token to win against subprocess_word
|
|
285
|
+
redirect_operator: _ => choice(
|
|
286
|
+
// Standard redirects
|
|
287
|
+
'>', '>>', '<',
|
|
288
|
+
// Numbered file descriptors - need precedence over word + >
|
|
289
|
+
token(prec(101, '1>')), token(prec(101, '1>>')),
|
|
290
|
+
token(prec(101, '2>')), token(prec(101, '2>>')),
|
|
291
|
+
// Xonsh-specific aliases - need high precedence
|
|
292
|
+
token(prec(101, 'o>')), token(prec(101, 'o>>')), // stdout (alias for 1>)
|
|
293
|
+
token(prec(101, 'e>')), token(prec(101, 'e>>')), // stderr (alias for 2>)
|
|
294
|
+
token(prec(101, 'err>')), token(prec(101, 'err>>')), // stderr
|
|
295
|
+
token(prec(101, 'out>')), token(prec(101, 'out>>')), // stdout
|
|
296
|
+
token(prec(101, 'all>')), token(prec(101, 'all>>')), // both stdout and stderr
|
|
297
|
+
'&>', // both stdout and stderr (bash compat)
|
|
298
|
+
token(prec(101, 'a>')), // append both
|
|
299
|
+
),
|
|
300
|
+
|
|
301
|
+
// Stream merging operators (don't take a file target)
|
|
302
|
+
// Need high precedence to win against subprocess_word
|
|
303
|
+
stream_merge_operator: _ => choice(
|
|
304
|
+
token(prec(101, '2>&1')), token(prec(101, '1>&2')),
|
|
305
|
+
token(prec(101, 'err>out')), token(prec(101, 'out>err')),
|
|
306
|
+
token(prec(101, 'err>&1')), token(prec(101, 'out>&2')),
|
|
307
|
+
),
|
|
308
|
+
|
|
309
|
+
redirect_target: $ => choice(
|
|
310
|
+
$.subprocess_word,
|
|
311
|
+
$.string,
|
|
312
|
+
$.env_variable,
|
|
313
|
+
$.env_variable_braced,
|
|
314
|
+
$.python_evaluation,
|
|
315
|
+
),
|
|
316
|
+
|
|
317
|
+
// Background Execution
|
|
318
|
+
background_command: $ => prec(1, seq(
|
|
319
|
+
choice(
|
|
320
|
+
$.captured_subprocess,
|
|
321
|
+
$.captured_subprocess_object,
|
|
322
|
+
$.uncaptured_subprocess,
|
|
323
|
+
$.uncaptured_subprocess_object,
|
|
324
|
+
),
|
|
325
|
+
$._background_amp,
|
|
326
|
+
)),
|
|
327
|
+
|
|
328
|
+
// Xontrib Statement: xontrib load name1 name2 ...
|
|
329
|
+
xontrib_statement: $ => seq(
|
|
330
|
+
'xontrib',
|
|
331
|
+
'load',
|
|
332
|
+
repeat1($.xontrib_name),
|
|
333
|
+
),
|
|
334
|
+
|
|
335
|
+
// Xontrib names can start with numbers (e.g., 1password)
|
|
336
|
+
xontrib_name: _ => /[a-zA-Z0-9_][a-zA-Z0-9_]*/,
|
|
337
|
+
|
|
338
|
+
// Macro Call: func!(args)
|
|
339
|
+
macro_call: $ => prec(10, seq(
|
|
340
|
+
field('name', $.identifier),
|
|
341
|
+
token.immediate('!('),
|
|
342
|
+
field('argument', optional($.macro_argument)),
|
|
343
|
+
')',
|
|
344
|
+
)),
|
|
345
|
+
|
|
346
|
+
macro_argument: _ => /[^)]*/,
|
|
347
|
+
|
|
348
|
+
// =========================================================================
|
|
349
|
+
// Subprocess Macro: cmd! args (passes rest of line as raw string)
|
|
350
|
+
// e.g., echo! "Hello!", bash -c! echo {123}
|
|
351
|
+
// Different from func!(args) which has ! immediately followed by (
|
|
352
|
+
// =========================================================================
|
|
353
|
+
|
|
354
|
+
// Subprocess macro: identifier! followed by space and args
|
|
355
|
+
// e.g., echo! "Hello!", bash -c! echo {123}
|
|
356
|
+
// The scanner emits _subprocess_macro_start after consuming "identifier! "
|
|
357
|
+
subprocess_macro: $ => prec.dynamic(101, seq(
|
|
358
|
+
$._subprocess_macro_start, // Scanner consumed "identifier! "
|
|
359
|
+
field('argument', alias(/[^\n]+/, $.subprocess_macro_argument)),
|
|
360
|
+
)),
|
|
361
|
+
|
|
362
|
+
// Block Macro: with! Context() as var:
|
|
363
|
+
// Captures block content as string for macro processing
|
|
364
|
+
block_macro_statement: $ => prec(20, seq(
|
|
365
|
+
token('with!'),
|
|
366
|
+
field('context', $.expression),
|
|
367
|
+
optional(seq('as', field('alias', prec(20, $.identifier)))),
|
|
368
|
+
':',
|
|
369
|
+
field('body', $._suite),
|
|
370
|
+
)),
|
|
371
|
+
|
|
372
|
+
// Bare Subprocess (detected by scanner heuristics)
|
|
373
|
+
// e.g., "ls -la", "cd $HOME", "cat file | grep pattern"
|
|
374
|
+
// Also handles modified subprocess: "@unthread ./tool.sh", "@json curl api/data"
|
|
375
|
+
// The _subprocess_start token is emitted by the scanner when it senses
|
|
376
|
+
// shell-like patterns (flags, pipes, redirects, path commands, @modifier + command)
|
|
377
|
+
bare_subprocess: $ => seq(
|
|
378
|
+
$._subprocess_start,
|
|
379
|
+
field('modifier', optional($.subprocess_modifier)),
|
|
380
|
+
field('body', $.subprocess_body),
|
|
381
|
+
optional($._background_amp), // Optional & for background execution
|
|
382
|
+
),
|
|
383
|
+
|
|
384
|
+
// Xonsh Expression (all supported xonsh-specific constructs)
|
|
385
|
+
xonsh_expression: $ => choice(
|
|
386
|
+
$.env_variable,
|
|
387
|
+
$.env_variable_braced,
|
|
388
|
+
$.captured_subprocess,
|
|
389
|
+
$.captured_subprocess_object,
|
|
390
|
+
$.uncaptured_subprocess,
|
|
391
|
+
$.uncaptured_subprocess_object,
|
|
392
|
+
$.python_evaluation,
|
|
393
|
+
$.tokenized_substitution,
|
|
394
|
+
$.regex_glob,
|
|
395
|
+
$.glob_pattern,
|
|
396
|
+
$.formatted_glob,
|
|
397
|
+
$.glob_path,
|
|
398
|
+
$.regex_path_glob,
|
|
399
|
+
$.custom_function_glob,
|
|
400
|
+
$.path_string,
|
|
401
|
+
$.background_command,
|
|
402
|
+
$.at_object,
|
|
403
|
+
$.macro_call,
|
|
404
|
+
),
|
|
405
|
+
|
|
406
|
+
// Xonsh Statements (standalone xonsh constructs)
|
|
407
|
+
|
|
408
|
+
// Standalone env_prefix: $VAR="value" (no space around =)
|
|
409
|
+
// Distinct from env_assignment ($VAR = value) and env_scoped_command ($VAR=val cmd)
|
|
410
|
+
env_prefix_statement: $ => prec(3, $.env_prefix),
|
|
411
|
+
|
|
412
|
+
xonsh_statement: $ => choice(
|
|
413
|
+
$.env_assignment,
|
|
414
|
+
$.env_deletion,
|
|
415
|
+
$.env_scoped_command,
|
|
416
|
+
$.env_prefix_statement,
|
|
417
|
+
$.help_expression,
|
|
418
|
+
$.super_help_expression,
|
|
419
|
+
$.xontrib_statement,
|
|
420
|
+
),
|
|
421
|
+
|
|
422
|
+
// Add xonsh constructs to primary_expression (for use in Python expressions)
|
|
423
|
+
primary_expression: ($, original) => choice(
|
|
424
|
+
original,
|
|
425
|
+
prec(1, $.xonsh_expression),
|
|
426
|
+
),
|
|
427
|
+
|
|
428
|
+
// Override boolean_operator to also support && and || (xonsh style)
|
|
429
|
+
// This allows ![cmd1] && ![cmd2] to parse correctly at Python level
|
|
430
|
+
boolean_operator: ($, original) => choice(
|
|
431
|
+
original,
|
|
432
|
+
prec.left(11, seq(
|
|
433
|
+
field('left', $.expression),
|
|
434
|
+
field('operator', $._logical_and),
|
|
435
|
+
field('right', $.expression),
|
|
436
|
+
)),
|
|
437
|
+
prec.left(10, seq(
|
|
438
|
+
field('left', $.expression),
|
|
439
|
+
field('operator', $._logical_or),
|
|
440
|
+
field('right', $.expression),
|
|
441
|
+
)),
|
|
442
|
+
),
|
|
443
|
+
|
|
444
|
+
// Override _simple_statement to include xonsh expressions and statements
|
|
445
|
+
_simple_statement: ($, original) => choice(
|
|
446
|
+
original,
|
|
447
|
+
prec.dynamic(10, $.xonsh_expression),
|
|
448
|
+
prec.dynamic(11, $.xonsh_statement),
|
|
449
|
+
// Bare subprocess has high priority - detected by scanner heuristics
|
|
450
|
+
prec.dynamic(100, $.bare_subprocess),
|
|
451
|
+
// Subprocess macro has highest priority (cmd! args)
|
|
452
|
+
prec.dynamic(101, $.subprocess_macro),
|
|
453
|
+
),
|
|
454
|
+
|
|
455
|
+
// Override _compound_statement to include block macro
|
|
456
|
+
_compound_statement: ($, original) => choice(
|
|
457
|
+
original,
|
|
458
|
+
prec.dynamic(15, $.block_macro_statement),
|
|
459
|
+
),
|
|
460
|
+
},
|
|
461
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tree-sitter-xonsh",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Xonsh grammar for tree-sitter",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/FoamScience/tree-sitter-xonsh"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"main": "bindings/node",
|
|
11
|
+
"types": "bindings/node",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"incremental",
|
|
14
|
+
"parsing",
|
|
15
|
+
"tree-sitter",
|
|
16
|
+
"xonsh",
|
|
17
|
+
"python",
|
|
18
|
+
"shell"
|
|
19
|
+
],
|
|
20
|
+
"files": [
|
|
21
|
+
"grammar.js",
|
|
22
|
+
"binding.gyp",
|
|
23
|
+
"prebuilds/**",
|
|
24
|
+
"bindings/node/*",
|
|
25
|
+
"queries/*",
|
|
26
|
+
"src/**",
|
|
27
|
+
"*.wasm"
|
|
28
|
+
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"tree-sitter-cli": "^0.26.3",
|
|
31
|
+
"tree-sitter-python": "^0.23.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"generate": "tree-sitter generate",
|
|
35
|
+
"build": "tree-sitter generate",
|
|
36
|
+
"test": "tree-sitter test",
|
|
37
|
+
"parse": "tree-sitter parse",
|
|
38
|
+
"build-wasm": "tree-sitter build --wasm",
|
|
39
|
+
"playground": "tree-sitter build --wasm && tree-sitter playground"
|
|
40
|
+
}
|
|
41
|
+
}
|