rapydscript-ng 0.8.0 → 0.8.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/CHANGELOG.md +9 -0
- package/README.md +8 -0
- package/bin/build.ts +117 -0
- package/build_wheels.py +133 -0
- package/editor-plugins/README.md +80 -0
- package/editor-plugins/nvim.lua +321 -0
- package/package.json +1 -1
- package/publish.py +27 -17
- package/release/compiler.js +8125 -8093
- package/release/signatures.json +27 -27
- package/release/stdlib_modules.json +1 -0
- package/src/ast.pyj +379 -279
- package/src/baselib-builtins.pyj +47 -26
- package/src/baselib-containers.pyj +105 -92
- package/src/baselib-errors.pyj +8 -1
- package/src/baselib-internal.pyj +35 -20
- package/src/baselib-itertools.pyj +13 -7
- package/src/baselib-str.pyj +55 -80
- package/src/compiler.pyj +4 -4
- package/src/errors.pyj +3 -4
- package/src/lib/aes.pyj +46 -32
- package/src/lib/elementmaker.pyj +11 -9
- package/src/lib/encodings.pyj +15 -5
- package/src/lib/gettext.pyj +9 -4
- package/src/lib/math.pyj +106 -45
- package/src/lib/operator.pyj +10 -10
- package/src/lib/pythonize.pyj +2 -1
- package/src/lib/random.pyj +28 -21
- package/src/lib/re.pyj +490 -17
- package/src/lib/traceback.pyj +7 -2
- package/src/lib/uuid.pyj +2 -2
- package/src/output/classes.pyj +28 -27
- package/src/output/codegen.pyj +80 -83
- package/src/output/comments.pyj +8 -8
- package/src/output/exceptions.pyj +20 -21
- package/src/output/functions.pyj +37 -26
- package/src/output/literals.pyj +14 -10
- package/src/output/loops.pyj +63 -59
- package/src/output/modules.pyj +52 -23
- package/src/output/operators.pyj +40 -29
- package/src/output/statements.pyj +20 -14
- package/src/output/stream.pyj +33 -34
- package/src/output/utils.pyj +12 -8
- package/src/parse.pyj +234 -233
- package/src/string_interpolation.pyj +5 -3
- package/src/tokenizer.pyj +176 -148
- package/src/utils.pyj +31 -14
- package/test/fmt.pyj +94 -4
- package/test/generic.pyj +9 -0
- package/test/lsp.pyj +35 -0
- package/test/str.pyj +8 -0
- package/tools/cli.mjs +25 -4
- package/tools/compile.mjs +7 -3
- package/tools/compiler.mjs +34 -2
- package/tools/fmt.mjs +262 -22
- package/tools/ini.mjs +112 -1
- package/tools/lsp.mjs +56 -6
- package/tools/repl.mjs +5 -2
- package/tools/self.mjs +15 -0
- package/tools/test.mjs +100 -0
- package/tools/web_repl_export.mjs +4 -0
- package/tree-sitter/package.json +1 -1
- package/tree-sitter/tree-sitter.json +1 -1
- package/editor-plugins/nvim/rapydscript/README.md +0 -184
- package/editor-plugins/nvim/rapydscript/bin/rapydscript.so +0 -0
- package/editor-plugins/nvim/rapydscript/ftdetect/rapydscript.lua +0 -10
- package/editor-plugins/nvim/rapydscript/lua/rapydscript/health.lua +0 -88
- package/editor-plugins/nvim/rapydscript/lua/rapydscript/init.lua +0 -279
- /package/tree-sitter/queries/{rapydscript/highlights.scm → highlights.scm} +0 -0
- /package/tree-sitter/queries/{rapydscript/indents.scm → indents.scm} +0 -0
- /package/tree-sitter/queries/{rapydscript/injections.scm → injections.scm} +0 -0
- /package/tree-sitter/queries/{rapydscript/locals.scm → locals.scm} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
version 0.8.1
|
|
2
|
+
=======================
|
|
3
|
+
|
|
4
|
+
* Fix concatenation of mixed string and f-string literal
|
|
5
|
+
* fmt and lsp now by default read format related settings from pyproject.toml, overridden via command line
|
|
6
|
+
* fmt command reports unfixable long lines in normal mode as well
|
|
7
|
+
* Fix newlines inside tuples not being allowed
|
|
8
|
+
* Miscellaneous improvements for the code auto formatter
|
|
9
|
+
|
|
1
10
|
version 0.8.0
|
|
2
11
|
=======================
|
|
3
12
|
|
package/README.md
CHANGED
|
@@ -104,6 +104,14 @@ Installation
|
|
|
104
104
|
|
|
105
105
|
[Try RapydScript-ng live via an in-browser REPL!](https://sw.kovidgoyal.net/rapydscript/repl/)
|
|
106
106
|
|
|
107
|
+
If you just want the standalone rapydscript binary, you can install it from
|
|
108
|
+
PyPI, with::
|
|
109
|
+
|
|
110
|
+
pip install rapydscript-ng
|
|
111
|
+
|
|
112
|
+
If you want the full package, needed to use the ``web-repl-export`` command, or
|
|
113
|
+
integrate with a node project, you can instead install using npm.
|
|
114
|
+
|
|
107
115
|
First make sure you have installed the latest version of [node.js](https://nodejs.org/) (You may need to restart your computer after this step).
|
|
108
116
|
|
|
109
117
|
From NPM for use as a command line app:
|
package/bin/build.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// vim:ft=typescript:ts=4:et
|
|
3
|
+
/*
|
|
4
|
+
* Generates bin/rapydscript.mjs — the bun compilation entry point — by scanning
|
|
5
|
+
* the filesystem for embeddable assets and prepending their import declarations
|
|
6
|
+
* and the __rapydscript_embedded__ global setup to the contents of bin/rapydscript.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun bin/build.ts # generate bin/rapydscript.mjs only
|
|
10
|
+
* bun bin/build.ts --compile --outfile foo # generate + run: bun build bin/rapydscript.mjs --compile --outfile foo
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
|
|
16
|
+
const repo_root = path.dirname(path.dirname(Bun.main));
|
|
17
|
+
const dev_dir = path.join(repo_root, 'dev');
|
|
18
|
+
const src_lib_dir = path.join(repo_root, 'src', 'lib');
|
|
19
|
+
const bin_dir = path.join(repo_root, 'bin');
|
|
20
|
+
const out_path = path.join(bin_dir, 'rapydscript.mjs');
|
|
21
|
+
|
|
22
|
+
async function exists(p: string): Promise<boolean> {
|
|
23
|
+
try { await fs.promises.access(p); return true; } catch { return false; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- Scan assets ---
|
|
27
|
+
|
|
28
|
+
const DEV_TEXT_ASSETS = ['compiler.js', 'baselib-plain-pretty.js', 'baselib-plain-ugly.js'];
|
|
29
|
+
const DEV_JSON_ASSETS = ['stdlib_modules.json'];
|
|
30
|
+
|
|
31
|
+
const found_dev_text: string[] = [];
|
|
32
|
+
for (const a of DEV_TEXT_ASSETS) {
|
|
33
|
+
if (await exists(path.join(dev_dir, a))) found_dev_text.push(a);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const found_dev_json: string[] = [];
|
|
37
|
+
for (const a of DEV_JSON_ASSETS) {
|
|
38
|
+
if (await exists(path.join(dev_dir, a))) found_dev_json.push(a);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const stdlib_files: string[] = (await fs.promises.readdir(src_lib_dir))
|
|
42
|
+
.filter(f => f.endsWith('.pyj'))
|
|
43
|
+
.sort();
|
|
44
|
+
|
|
45
|
+
// --- Build the generated asset section ---
|
|
46
|
+
|
|
47
|
+
function var_name(prefix: string, filename: string): string {
|
|
48
|
+
return prefix + filename.replace(/[^a-zA-Z0-9]/g, '_');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const lines: string[] = [];
|
|
52
|
+
|
|
53
|
+
lines.push('// Generated by bin/build.ts — do not edit manually');
|
|
54
|
+
lines.push('// vim:ft=javascript:ts=4:et');
|
|
55
|
+
lines.push('');
|
|
56
|
+
lines.push('// === Embedded assets (generated by bin/build.ts) ===');
|
|
57
|
+
|
|
58
|
+
for (const a of found_dev_text) {
|
|
59
|
+
lines.push(`import ${var_name('_dev_', a)} from '../dev/${a}' with { type: 'text' };`);
|
|
60
|
+
}
|
|
61
|
+
for (const a of found_dev_json) {
|
|
62
|
+
lines.push(`import ${var_name('_dev_', a)} from '../dev/${a}' with { type: 'json' };`);
|
|
63
|
+
}
|
|
64
|
+
for (const f of stdlib_files) {
|
|
65
|
+
lines.push(`import ${var_name('_stdlib_', f)} from '../src/lib/${f}' with { type: 'text' };`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push('globalThis.__rapydscript_embedded__ = {');
|
|
70
|
+
|
|
71
|
+
for (const a of found_dev_text) {
|
|
72
|
+
lines.push(` '${a}': ${var_name('_dev_', a)},`);
|
|
73
|
+
}
|
|
74
|
+
for (const a of found_dev_json) {
|
|
75
|
+
// strip extension for the key so consumers use e.g. embedded.stdlib_modules
|
|
76
|
+
const key = a.replace(/\.json$/, '').replace(/-/g, '_');
|
|
77
|
+
lines.push(` ${key}: ${var_name('_dev_', a)},`);
|
|
78
|
+
}
|
|
79
|
+
lines.push(' stdlib: {');
|
|
80
|
+
for (const f of stdlib_files) {
|
|
81
|
+
lines.push(` '${f}': ${var_name('_stdlib_', f)},`);
|
|
82
|
+
}
|
|
83
|
+
lines.push(' },');
|
|
84
|
+
lines.push('};');
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push('// === Entry point (from bin/rapydscript) ===');
|
|
87
|
+
lines.push('');
|
|
88
|
+
|
|
89
|
+
// --- Append bin/rapydscript content (strip the shebang line) ---
|
|
90
|
+
|
|
91
|
+
const entry_src = await fs.promises.readFile(path.join(bin_dir, 'rapydscript'), 'utf-8');
|
|
92
|
+
const entry_lines = entry_src.split('\n');
|
|
93
|
+
// Drop the first line if it's the shebang, and the vim modeline comment right after
|
|
94
|
+
let start = 0;
|
|
95
|
+
if (entry_lines[0].startsWith('#!')) start = 1;
|
|
96
|
+
if (start < entry_lines.length && entry_lines[start].trim().startsWith('// vim:')) start++;
|
|
97
|
+
|
|
98
|
+
lines.push(...entry_lines.slice(start));
|
|
99
|
+
|
|
100
|
+
// Ensure the file ends with a newline
|
|
101
|
+
const content = lines.join('\n');
|
|
102
|
+
const final_content = content.endsWith('\n') ? content : content + '\n';
|
|
103
|
+
|
|
104
|
+
await fs.promises.writeFile(out_path, final_content, 'utf-8');
|
|
105
|
+
console.log(`Written ${path.relative(repo_root, out_path)}`);
|
|
106
|
+
console.log(` ${found_dev_text.length} dev text assets, ${found_dev_json.length} dev json assets, ${stdlib_files.length} stdlib files`);
|
|
107
|
+
|
|
108
|
+
// --- Optionally run bun build with passed-through args ---
|
|
109
|
+
|
|
110
|
+
const extra_args = process.argv.slice(2);
|
|
111
|
+
if (extra_args.length > 0) {
|
|
112
|
+
const cmd_args = ['build', out_path, ...extra_args];
|
|
113
|
+
console.log(`Running: bun ${cmd_args.join(' ')}`);
|
|
114
|
+
const proc = Bun.spawn(['bun', ...cmd_args], { stdout: 'inherit', stderr: 'inherit', stdin: 'inherit' });
|
|
115
|
+
const code = await proc.exited;
|
|
116
|
+
process.exit(code);
|
|
117
|
+
}
|
package/build_wheels.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# vim:fileencoding=utf-8
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
import subprocess
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
BASE = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
|
|
14
|
+
# Maps (bun_target, wheel_platform_tag, binary_name)
|
|
15
|
+
PLATFORMS = [
|
|
16
|
+
("bun-linux-x64", "manylinux_2_17_x86_64", "rapydscript"),
|
|
17
|
+
("bun-linux-arm64", "manylinux_2_17_aarch64", "rapydscript"),
|
|
18
|
+
("bun-linux-x64-musl", "musllinux_1_2_x86_64", "rapydscript"),
|
|
19
|
+
("bun-linux-arm64-musl", "musllinux_1_2_aarch64", "rapydscript"),
|
|
20
|
+
("bun-darwin-x64", "macosx_10_9_x86_64", "rapydscript"),
|
|
21
|
+
("bun-darwin-arm64", "macosx_11_0_arm64", "rapydscript"),
|
|
22
|
+
("bun-windows-x64", "win_amd64", "rapydscript.exe"),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def normalize_name(name):
|
|
27
|
+
return re.sub(r"[-_.]+", "_", name).lower()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_metadata():
|
|
31
|
+
with open(os.path.join(BASE, "package.json"), "rb") as f:
|
|
32
|
+
return json.load(f)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_binary(bun_target, outfile):
|
|
36
|
+
cmd = [
|
|
37
|
+
"bun",
|
|
38
|
+
"bin/build.ts",
|
|
39
|
+
"--compile",
|
|
40
|
+
f"--outfile={outfile}",
|
|
41
|
+
f"--target={bun_target}",
|
|
42
|
+
]
|
|
43
|
+
print(f" Building {bun_target} ...")
|
|
44
|
+
subprocess.check_call(cmd, cwd=BASE)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_wheel_metadata(dist_info_dir, meta, wheel_platform):
|
|
48
|
+
maintainers = meta.get("maintainers", [])
|
|
49
|
+
author = maintainers[0].get("name", "") if maintainers else ""
|
|
50
|
+
author_email = maintainers[0].get("email", "") if maintainers else ""
|
|
51
|
+
repo_url = meta.get("repository", {}).get("url", "")
|
|
52
|
+
|
|
53
|
+
with open(os.path.join(dist_info_dir, "WHEEL"), "w") as f:
|
|
54
|
+
f.write("Wheel-Version: 1.0\n")
|
|
55
|
+
f.write("Generator: wheel.py\n")
|
|
56
|
+
f.write("Root-Is-Purelib: false\n")
|
|
57
|
+
f.write(f"Tag: py3-none-{wheel_platform}\n")
|
|
58
|
+
|
|
59
|
+
with open(os.path.join(dist_info_dir, "METADATA"), "w") as f:
|
|
60
|
+
f.write("Metadata-Version: 2.1\n")
|
|
61
|
+
f.write(f"Name: {meta['name']}\n")
|
|
62
|
+
f.write(f"Version: {meta['version']}\n")
|
|
63
|
+
f.write(f"Summary: {meta.get('description', '')}\n")
|
|
64
|
+
if meta.get("homepage"):
|
|
65
|
+
f.write(f"Home-page: {meta['homepage']}\n")
|
|
66
|
+
if author:
|
|
67
|
+
f.write(f"Author: {author}\n")
|
|
68
|
+
if author_email:
|
|
69
|
+
f.write(f"Author-email: {author_email}\n")
|
|
70
|
+
if meta.get("license"):
|
|
71
|
+
f.write(f"License: {meta['license']}\n")
|
|
72
|
+
if meta.get("keywords"):
|
|
73
|
+
f.write(f"Keywords: {', '.join(meta['keywords'])}\n")
|
|
74
|
+
if repo_url:
|
|
75
|
+
f.write(f"Project-URL: Source Code, {repo_url}\n")
|
|
76
|
+
f.write("Description-Content-Type: text/markdown")
|
|
77
|
+
print(file=f)
|
|
78
|
+
with open("README.md") as s:
|
|
79
|
+
f.write(s.read())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def build_wheel(meta, bun_target, wheel_platform, binary_name, dest_dir, tmpdir):
|
|
83
|
+
norm_name = normalize_name(meta["name"])
|
|
84
|
+
version = meta["version"]
|
|
85
|
+
|
|
86
|
+
suffix = ".exe" if binary_name.endswith(".exe") else ""
|
|
87
|
+
binary_outfile = os.path.join(tmpdir, f"rapydscript-{bun_target}{suffix}")
|
|
88
|
+
build_binary(bun_target, binary_outfile)
|
|
89
|
+
|
|
90
|
+
# Assemble unpacked wheel layout
|
|
91
|
+
wheel_dir = os.path.join(tmpdir, f"{norm_name}-{version}-{wheel_platform}")
|
|
92
|
+
dist_info_dir = os.path.join(wheel_dir, f"{norm_name}-{version}.dist-info")
|
|
93
|
+
scripts_dir = os.path.join(wheel_dir, f"{norm_name}-{version}.data", "scripts")
|
|
94
|
+
os.makedirs(dist_info_dir)
|
|
95
|
+
os.makedirs(scripts_dir)
|
|
96
|
+
|
|
97
|
+
dest_binary = os.path.join(scripts_dir, binary_name)
|
|
98
|
+
shutil.copy2(binary_outfile, dest_binary)
|
|
99
|
+
os.chmod(dest_binary, 0o755)
|
|
100
|
+
|
|
101
|
+
write_wheel_metadata(dist_info_dir, meta, wheel_platform)
|
|
102
|
+
|
|
103
|
+
print(" Packing wheel ...")
|
|
104
|
+
subprocess.check_call(
|
|
105
|
+
[sys.executable, "-m", "wheel", "pack", wheel_dir, "--dest-dir", dest_dir],
|
|
106
|
+
cwd=tmpdir,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return f"{norm_name}-{version}-py3-none-{wheel_platform}.whl"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main():
|
|
113
|
+
meta = load_metadata()
|
|
114
|
+
dest_dir = os.path.join(BASE, "dist")
|
|
115
|
+
os.makedirs(dest_dir, exist_ok=True)
|
|
116
|
+
for x in os.listdir(dest_dir):
|
|
117
|
+
os.remove(os.path.join(dest_dir, x))
|
|
118
|
+
|
|
119
|
+
wheels = []
|
|
120
|
+
with tempfile.TemporaryDirectory(prefix="rapydscript-wheel-") as tmpdir:
|
|
121
|
+
for bun_target, wheel_platform, binary_name in PLATFORMS:
|
|
122
|
+
print(f"\n[{bun_target}]")
|
|
123
|
+
whl = build_wheel(
|
|
124
|
+
meta, bun_target, wheel_platform, binary_name, dest_dir, tmpdir
|
|
125
|
+
)
|
|
126
|
+
wheels.append(whl)
|
|
127
|
+
print(f" -> dist/{whl}")
|
|
128
|
+
|
|
129
|
+
print(f"\nBuilt {len(wheels)} wheels in dist/")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Plugins to integrate with editors
|
|
2
|
+
|
|
3
|
+
Wires up the RapydScript LSP server (`rapydscript lsp`) as a
|
|
4
|
+
LSP client for various editors (currently only Neovim).
|
|
5
|
+
It also enables treesitter based syntax highlighting.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
Provided by the LSP server — no extra plugins required:
|
|
10
|
+
|
|
11
|
+
- **Completions** — in-scope symbols, builtins, keywords, and member completion (`mod.<TAB>`)
|
|
12
|
+
- **Diagnostics** — lint checks and unresolved import warnings, updated as you type
|
|
13
|
+
- **Hover** (`K`) — kind, origin, and docstring of the symbol under the cursor
|
|
14
|
+
- **Go to definition** (`gd`) — including jumping into imported modules
|
|
15
|
+
- **Find references** (`gr`) — resolved across files through the import graph
|
|
16
|
+
- **Rename** (`<leader>rn`) — renames everywhere across the workspace
|
|
17
|
+
- **Code actions** (`<leader>ca`) — remove unused import/local, add `# noqa`, format document
|
|
18
|
+
- **Document formatting** (`<leader>f`) — same as `rapydscript fmt`
|
|
19
|
+
- **Document symbols** — outline view via any symbols picker
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- `node` (from nodejs) on your `$PATH`
|
|
24
|
+
|
|
25
|
+
- **Syntax highlighting only**: a C compiler on your `$PATH` is required to compile the
|
|
26
|
+
tree-sitter parser the first time the plugin loads. On Linux/macOS any `cc`-compatible
|
|
27
|
+
compiler works (gcc, clang, etc.). On Windows, `cl` (MSVC) or `clang-cl` must be
|
|
28
|
+
available. LSP features work regardless of whether compilation
|
|
29
|
+
succeeds.
|
|
30
|
+
|
|
31
|
+
## Neovim Installation
|
|
32
|
+
|
|
33
|
+
Needs Neovim >= 0.12. Just add the following to your ``~/.config/nvim/init.lua``, changing the
|
|
34
|
+
options to suit yourself:
|
|
35
|
+
|
|
36
|
+
```lua
|
|
37
|
+
vim.pack.add({ { src = "https://github.com/kovidgoyal/rapydscript-ng", name = "rapydscript" } }, {
|
|
38
|
+
load = function(plug_data)
|
|
39
|
+
vim.cmd.packadd(plug_data.spec.name)
|
|
40
|
+
for _, pack in ipairs(vim.pack.get({ plug_data.spec.name })) do
|
|
41
|
+
vim.opt.rtp:append(pack.path .. "/editor-plugins/nvim/rapydscript")
|
|
42
|
+
dofile(pack.path .. '/editor-plugins/nvim.lua').setup({
|
|
43
|
+
-- options you should customize
|
|
44
|
+
line_length = 80,
|
|
45
|
+
preferred_quote = "single",
|
|
46
|
+
})
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or if you want to load it manually for older versions of Neovim:
|
|
53
|
+
|
|
54
|
+
```lua
|
|
55
|
+
dofile("/path/to/rapydscript/editor-plugins/nvim.lua").setup()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration reference
|
|
59
|
+
|
|
60
|
+
| Key | Type | Default | Description |
|
|
61
|
+
|-----|------|---------|-------------|
|
|
62
|
+
| `cmd` | `string[]` | `{"rapydscript","lsp"}` | Command used to start the server |
|
|
63
|
+
| `import_path_patterns` | `string[]` | `{".", "src", "src/pyj"}` | Glob patterns relative to the project root; matching directories that contain `.pyj` files are passed automatically as `--import-path` |
|
|
64
|
+
| `line_length` | `number\|nil` | `nil` | Max line length for the formatter, passed as `--line-length` |
|
|
65
|
+
| `preferred_quote` | `string\|nil` | `nil` | `"single"` or `"double"`, passed as `--preferred-quote` |
|
|
66
|
+
| `join_lines` | `boolean\|nil` | `nil` | When `true`, multi-line statements that fit within `line_length` are joined onto one line, passed as `--join-lines`. Disabled by default. |
|
|
67
|
+
| `filetypes` | `string[]` | `{"rapydscript"}` | Filetypes that trigger server attachment |
|
|
68
|
+
| `root_markers` | `string[]` | `{".git","package.json","rapydscript.json"}` | Files/dirs used to detect the project root |
|
|
69
|
+
|
|
70
|
+
### Automatic import path detection
|
|
71
|
+
|
|
72
|
+
When a project root is found (via `root_markers`), the plugin expands each pattern in
|
|
73
|
+
`import_path_patterns` as a glob relative to that root. Any matching directory that
|
|
74
|
+
contains at least one `.pyj` file is added to the server's import search path
|
|
75
|
+
automatically — no manual configuration needed for standard project layouts.
|
|
76
|
+
|
|
77
|
+
If no project root is detected the setting has no effect.
|
|
78
|
+
|
|
79
|
+
To add custom search locations alongside the defaults, use
|
|
80
|
+
``import_path_patterns``.
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
#!/usr/bin/env lua
|
|
2
|
+
--
|
|
3
|
+
-- nvim.lua
|
|
4
|
+
-- Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
|
|
5
|
+
--
|
|
6
|
+
-- Distributed under terms of the MIT license.
|
|
7
|
+
--
|
|
8
|
+
|
|
9
|
+
local M = {}
|
|
10
|
+
|
|
11
|
+
local _plugin_root = vim.fn.fnamemodify(
|
|
12
|
+
debug.getinfo(1, "S").source:sub(2), ":p:h"
|
|
13
|
+
)
|
|
14
|
+
local _repo_root = vim.fn.fnamemodify(_plugin_root, ":h")
|
|
15
|
+
local ts_root = _repo_root .. '/tree-sitter'
|
|
16
|
+
|
|
17
|
+
-- _ts_so holds the .so/.dll path once the background build succeeds, nil otherwise.
|
|
18
|
+
local _ts_so = nil
|
|
19
|
+
|
|
20
|
+
-- Tracks the async build lifecycle.
|
|
21
|
+
local _build_state = {
|
|
22
|
+
started = false, -- set when _build_async() is called
|
|
23
|
+
done = false, -- set when the job exits (success or failure)
|
|
24
|
+
waiters = {}, -- callbacks queued while the job is running
|
|
25
|
+
error = nil, -- stderr from a failed build, or a plain error string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
-- Mark the build finished, optionally record the .so path, and fire any waiters.
|
|
29
|
+
local function _build_finish(success, so, err)
|
|
30
|
+
if success then _ts_so = so end
|
|
31
|
+
_build_state.done = true
|
|
32
|
+
_build_state.error = err or nil
|
|
33
|
+
local ws = _build_state.waiters
|
|
34
|
+
_build_state.waiters = {}
|
|
35
|
+
for _, cb in ipairs(ws) do
|
|
36
|
+
vim.schedule(cb)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
-- Run cb immediately if the build is already done; otherwise enqueue it.
|
|
41
|
+
local function _when_built(cb)
|
|
42
|
+
if _build_state.done then
|
|
43
|
+
cb()
|
|
44
|
+
else
|
|
45
|
+
table.insert(_build_state.waiters, cb)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
-- Register the compiled shared library with neovim's tree-sitter runtime.
|
|
50
|
+
-- Returns true on success.
|
|
51
|
+
local function _register_queries()
|
|
52
|
+
local queries_dir = ts_root .. '/queries/'
|
|
53
|
+
vim.treesitter.query.set(
|
|
54
|
+
'rapydscript', 'highlights', table.concat(vim.fn.readfile(queries_dir .. 'highlights.scm'), "\n"))
|
|
55
|
+
vim.treesitter.query.set(
|
|
56
|
+
'rapydscript', 'indents', table.concat(vim.fn.readfile(queries_dir .. 'indents.scm'), "\n"))
|
|
57
|
+
vim.treesitter.query.set(
|
|
58
|
+
'rapydscript', 'injections', table.concat(vim.fn.readfile(queries_dir .. 'injections.scm'), "\n"))
|
|
59
|
+
vim.treesitter.query.set(
|
|
60
|
+
'rapydscript', 'locals', table.concat(vim.fn.readfile(queries_dir .. 'locals.scm'), "\n"))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
local function _load_parser(so)
|
|
65
|
+
vim.treesitter.language.register("rapydscript", "rapydscript")
|
|
66
|
+
local ok, err = pcall(vim.treesitter.language.add, "rapydscript", { path = so })
|
|
67
|
+
if not ok then
|
|
68
|
+
vim.notify("rapydscript: failed to load tree-sitter parser: " .. tostring(err), vim.log.levels.ERROR)
|
|
69
|
+
return false
|
|
70
|
+
end
|
|
71
|
+
_register_queries()
|
|
72
|
+
return true
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
-- Compile the tree-sitter parser in a background job.
|
|
76
|
+
local function _build_async()
|
|
77
|
+
_build_state.started = true
|
|
78
|
+
|
|
79
|
+
local ts_src = ts_root .. "/src"
|
|
80
|
+
local parser_c = ts_src .. "/parser.c"
|
|
81
|
+
local scanner_c = ts_src .. "/scanner.c"
|
|
82
|
+
if vim.fn.filereadable(parser_c) == 0 then
|
|
83
|
+
local msg = "parser.c not found: " .. parser_c
|
|
84
|
+
vim.notify("rapydscript: " .. msg, vim.log.levels.ERROR)
|
|
85
|
+
_build_finish(false, nil, msg)
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
local bin_dir = _plugin_root .. "/bin"
|
|
90
|
+
vim.fn.mkdir(bin_dir, "p")
|
|
91
|
+
|
|
92
|
+
local is_win = vim.fn.has("win32") == 1
|
|
93
|
+
local lib_ext = is_win and ".dll" or ".so"
|
|
94
|
+
local so = bin_dir .. "/rapydscript" .. lib_ext
|
|
95
|
+
|
|
96
|
+
local so_mtime = vim.fn.getftime(so)
|
|
97
|
+
local needs = so_mtime < 0
|
|
98
|
+
or vim.fn.getftime(parser_c) > so_mtime
|
|
99
|
+
or vim.fn.getftime(scanner_c) > so_mtime
|
|
100
|
+
|
|
101
|
+
if not needs then
|
|
102
|
+
-- Already up-to-date; load the existing .so and mark done without spawning a job.
|
|
103
|
+
_build_finish(_load_parser(so), so)
|
|
104
|
+
return
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
local cmd
|
|
108
|
+
if is_win then
|
|
109
|
+
local compiler
|
|
110
|
+
if vim.fn.exepath("cl") ~= "" then
|
|
111
|
+
compiler = "cl"
|
|
112
|
+
elseif vim.fn.exepath("clang-cl") ~= "" then
|
|
113
|
+
compiler = "clang-cl"
|
|
114
|
+
else
|
|
115
|
+
local msg = "tree-sitter syntax highlighting requires cl or clang-cl on PATH"
|
|
116
|
+
vim.notify("rapydscript: " .. msg, vim.log.levels.ERROR)
|
|
117
|
+
_build_finish(false, nil, msg)
|
|
118
|
+
return
|
|
119
|
+
end
|
|
120
|
+
-- /LD = build DLL, /nologo = suppress banner, /O2 = optimize,
|
|
121
|
+
-- /I = include path, /Fe: = output DLL path
|
|
122
|
+
cmd = string.format(
|
|
123
|
+
'"%s" /LD /nologo /O2 /I"%s" /Fe:"%s" "%s" "%s"',
|
|
124
|
+
compiler, ts_src, so, parser_c, scanner_c
|
|
125
|
+
)
|
|
126
|
+
else
|
|
127
|
+
local shared = vim.loop.os_uname().sysname == "Darwin" and "-dynamiclib" or "-shared"
|
|
128
|
+
cmd = { "cc", shared, "-fPIC", "-Os", "-I", ts_src, "-o", so, parser_c, scanner_c }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
vim.notify("rapydscript: compiling tree-sitter parser…", vim.log.levels.INFO)
|
|
132
|
+
|
|
133
|
+
local stderr_lines = {}
|
|
134
|
+
vim.fn.jobstart(cmd, {
|
|
135
|
+
on_stdout = function(_, _data) end,
|
|
136
|
+
on_stderr = function(_, data)
|
|
137
|
+
if data then vim.list_extend(stderr_lines, data) end
|
|
138
|
+
end,
|
|
139
|
+
on_exit = function(_, code)
|
|
140
|
+
vim.schedule(function()
|
|
141
|
+
if code ~= 0 then
|
|
142
|
+
local stderr = table.concat(stderr_lines, "\n")
|
|
143
|
+
vim.notify(
|
|
144
|
+
"rapydscript: tree-sitter compile failed:\n" .. stderr,
|
|
145
|
+
vim.log.levels.ERROR
|
|
146
|
+
)
|
|
147
|
+
_build_finish(false, nil, stderr)
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
_build_finish(_load_parser(so), so, nil)
|
|
151
|
+
end)
|
|
152
|
+
end,
|
|
153
|
+
})
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
local function _start_treesitter(bufnr)
|
|
157
|
+
_when_built(function()
|
|
158
|
+
if not _ts_so then return end
|
|
159
|
+
local ts_ok, ts_err = pcall(vim.treesitter.start, bufnr, "rapydscript")
|
|
160
|
+
if not ts_ok then
|
|
161
|
+
vim.notify("rapydscript: tree-sitter start failed: " .. tostring(ts_err), vim.log.levels.ERROR)
|
|
162
|
+
end
|
|
163
|
+
end)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
-- Populates M._binary_info as a side-effect.
|
|
167
|
+
local function _default_lsp_cmd()
|
|
168
|
+
local repo_bin = _repo_root .. "/bin/rapydscript"
|
|
169
|
+
M._binary_info.repo = { path = repo_bin }
|
|
170
|
+
if vim.fn.has("win32") == 1 then
|
|
171
|
+
-- On Windows the shebang is not honoured; invoke via node explicitly.
|
|
172
|
+
return { "node", repo_bin, "lsp" }
|
|
173
|
+
end
|
|
174
|
+
return { repo_bin, "lsp" }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
-- Expose tree-sitter compilation result for checkhealth.
|
|
178
|
+
-- Blocks up to 30 s if the background build is still running.
|
|
179
|
+
M._tree_sitter_path = function()
|
|
180
|
+
if _build_state.started and not _build_state.done then
|
|
181
|
+
vim.wait(30000, function() return _build_state.done end, 50)
|
|
182
|
+
end
|
|
183
|
+
return _ts_so
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
-- Expose the build error (if any) for checkhealth.
|
|
187
|
+
-- Must be called after _tree_sitter_path() so the build is already settled.
|
|
188
|
+
M._tree_sitter_error = function()
|
|
189
|
+
return _build_state.error
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
-- Populated by _default_lsp_cmd() below; read by lua/rapydscript/health.lua.
|
|
193
|
+
M._binary_info = { system = nil, repo = nil }
|
|
194
|
+
|
|
195
|
+
-- Expand glob patterns relative to root, keep only dirs that contain .pyj files,
|
|
196
|
+
-- and return them colon-separated. Returns nil when nothing matches.
|
|
197
|
+
local function _resolve_import_paths(root, patterns)
|
|
198
|
+
local seen = {}
|
|
199
|
+
local dirs = {}
|
|
200
|
+
for _, pat in ipairs(patterns) do
|
|
201
|
+
local matches = vim.fn.glob(root .. "/" .. pat, false, true)
|
|
202
|
+
for _, path in ipairs(matches) do
|
|
203
|
+
local norm = vim.fn.fnamemodify(path, ":p"):gsub("/$", "")
|
|
204
|
+
if not seen[norm] and vim.fn.isdirectory(norm) == 1 then
|
|
205
|
+
if #vim.fn.glob(norm .. "/*.pyj", false, true) > 0 then
|
|
206
|
+
seen[norm] = true
|
|
207
|
+
dirs[#dirs + 1] = norm
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
return #dirs > 0 and table.concat(dirs, ":") or nil
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
M.defaults = {
|
|
216
|
+
cmd = _default_lsp_cmd(),
|
|
217
|
+
-- Glob patterns relative to the project root; matching dirs that contain
|
|
218
|
+
-- .pyj files are passed automatically as --import-path to the LSP server.
|
|
219
|
+
import_path_patterns = { ".", "src", "src/pyj" },
|
|
220
|
+
line_length = nil,
|
|
221
|
+
preferred_quote = nil,
|
|
222
|
+
filetypes = { "rapydscript" },
|
|
223
|
+
root_markers = { ".git", "package.json", "rapydscript.json" },
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
-- Current runtime settings, kept in sync whenever update_settings() is called.
|
|
227
|
+
M._settings = {}
|
|
228
|
+
|
|
229
|
+
local function build_cmd(opts, import_path)
|
|
230
|
+
local cmd = vim.deepcopy(opts.cmd)
|
|
231
|
+
if import_path then
|
|
232
|
+
vim.list_extend(cmd, { "--import-path", import_path })
|
|
233
|
+
end
|
|
234
|
+
if opts.line_length then
|
|
235
|
+
vim.list_extend(cmd, { "--line-length", tostring(opts.line_length) })
|
|
236
|
+
end
|
|
237
|
+
if opts.preferred_quote then
|
|
238
|
+
vim.list_extend(cmd, { "--preferred-quote", opts.preferred_quote })
|
|
239
|
+
end
|
|
240
|
+
if opts.join_lines then
|
|
241
|
+
vim.list_extend(cmd, { "--join-lines" })
|
|
242
|
+
end
|
|
243
|
+
return cmd
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
local function make_capabilities()
|
|
247
|
+
local caps = vim.lsp.protocol.make_client_capabilities()
|
|
248
|
+
-- Tell the server we support dynamic registration for workspace/didChangeConfiguration
|
|
249
|
+
-- so it will register that notification and accept live setting updates.
|
|
250
|
+
caps.workspace = caps.workspace or {}
|
|
251
|
+
caps.workspace.didChangeConfiguration = { dynamicRegistration = true }
|
|
252
|
+
return caps
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
-- Send workspace/didChangeConfiguration to every active rapydscript client.
|
|
256
|
+
-- `new_settings` is a table with any subset of:
|
|
257
|
+
-- line_length (number)
|
|
258
|
+
-- preferred_quote (string) -- "single" | "double"
|
|
259
|
+
function M.update_settings(new_settings)
|
|
260
|
+
new_settings = new_settings or {}
|
|
261
|
+
|
|
262
|
+
-- Merge into the module-level settings table.
|
|
263
|
+
if new_settings.line_length ~= nil then
|
|
264
|
+
M._settings.lineLength = new_settings.line_length
|
|
265
|
+
end
|
|
266
|
+
if new_settings.preferred_quote ~= nil then
|
|
267
|
+
M._settings.preferredQuote = new_settings.preferred_quote
|
|
268
|
+
end
|
|
269
|
+
if new_settings.join_lines ~= nil then
|
|
270
|
+
M._settings.joinLines = new_settings.join_lines
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
local clients = vim.lsp.get_clients({ name = "rapydscript" })
|
|
274
|
+
if #clients == 0 then
|
|
275
|
+
vim.notify("rapydscript: no active LSP client found", vim.log.levels.WARN)
|
|
276
|
+
return
|
|
277
|
+
end
|
|
278
|
+
for _, client in ipairs(clients) do
|
|
279
|
+
client:notify("workspace/didChangeConfiguration", {
|
|
280
|
+
settings = { rapydscript = M._settings },
|
|
281
|
+
})
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
function M.setup(opts)
|
|
286
|
+
vim.filetype.add({ extension = { pyj = 'rapydscript' } })
|
|
287
|
+
opts = vim.tbl_deep_extend("force", M.defaults, opts or {})
|
|
288
|
+
M._active_opts = opts
|
|
289
|
+
|
|
290
|
+
-- Seed the runtime settings table from the initial opts so that partial
|
|
291
|
+
-- update_settings() calls later always send the full current state.
|
|
292
|
+
M._settings = {
|
|
293
|
+
lineLength = opts.line_length,
|
|
294
|
+
preferredQuote = opts.preferred_quote,
|
|
295
|
+
joinLines = opts.join_lines,
|
|
296
|
+
}
|
|
297
|
+
_build_async()
|
|
298
|
+
|
|
299
|
+
vim.api.nvim_create_autocmd("FileType", {
|
|
300
|
+
pattern = opts.filetypes,
|
|
301
|
+
callback = function(ev)
|
|
302
|
+
vim.bo[ev.buf].syntax = "off" -- use treesitter for syntax highlighting
|
|
303
|
+
_start_treesitter(ev.buf)
|
|
304
|
+
local root = vim.fs.root(ev.buf, opts.root_markers)
|
|
305
|
+
local import_path = root
|
|
306
|
+
and _resolve_import_paths(root, opts.import_path_patterns)
|
|
307
|
+
or nil
|
|
308
|
+
vim.lsp.start({
|
|
309
|
+
name = "rapydscript",
|
|
310
|
+
cmd = build_cmd(opts, import_path),
|
|
311
|
+
root_dir = root or vim.fn.getcwd(),
|
|
312
|
+
capabilities = make_capabilities(),
|
|
313
|
+
settings = {
|
|
314
|
+
rapydscript = M._settings,
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
end,
|
|
318
|
+
})
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
return M
|