rapydscript-ng 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +8 -0
  3. package/bin/build.ts +117 -0
  4. package/build_wheels.py +133 -0
  5. package/editor-plugins/README.md +80 -0
  6. package/editor-plugins/nvim.lua +321 -0
  7. package/package.json +1 -1
  8. package/publish.py +27 -17
  9. package/release/compiler.js +8125 -8093
  10. package/release/signatures.json +27 -27
  11. package/release/stdlib_modules.json +1 -0
  12. package/src/ast.pyj +379 -279
  13. package/src/baselib-builtins.pyj +47 -26
  14. package/src/baselib-containers.pyj +105 -92
  15. package/src/baselib-errors.pyj +8 -1
  16. package/src/baselib-internal.pyj +35 -20
  17. package/src/baselib-itertools.pyj +13 -7
  18. package/src/baselib-str.pyj +55 -80
  19. package/src/compiler.pyj +4 -4
  20. package/src/errors.pyj +3 -4
  21. package/src/lib/aes.pyj +46 -32
  22. package/src/lib/elementmaker.pyj +11 -9
  23. package/src/lib/encodings.pyj +15 -5
  24. package/src/lib/gettext.pyj +9 -4
  25. package/src/lib/math.pyj +106 -45
  26. package/src/lib/operator.pyj +10 -10
  27. package/src/lib/pythonize.pyj +2 -1
  28. package/src/lib/random.pyj +28 -21
  29. package/src/lib/re.pyj +490 -17
  30. package/src/lib/traceback.pyj +7 -2
  31. package/src/lib/uuid.pyj +2 -2
  32. package/src/output/classes.pyj +28 -27
  33. package/src/output/codegen.pyj +80 -83
  34. package/src/output/comments.pyj +8 -8
  35. package/src/output/exceptions.pyj +20 -21
  36. package/src/output/functions.pyj +37 -26
  37. package/src/output/literals.pyj +14 -10
  38. package/src/output/loops.pyj +63 -59
  39. package/src/output/modules.pyj +52 -23
  40. package/src/output/operators.pyj +40 -29
  41. package/src/output/statements.pyj +20 -14
  42. package/src/output/stream.pyj +33 -34
  43. package/src/output/utils.pyj +12 -8
  44. package/src/parse.pyj +234 -233
  45. package/src/string_interpolation.pyj +5 -3
  46. package/src/tokenizer.pyj +176 -148
  47. package/src/utils.pyj +31 -14
  48. package/test/fmt.pyj +94 -4
  49. package/test/generic.pyj +9 -0
  50. package/test/lsp.pyj +35 -0
  51. package/test/str.pyj +8 -0
  52. package/tools/cli.mjs +25 -4
  53. package/tools/compile.mjs +7 -3
  54. package/tools/compiler.mjs +34 -2
  55. package/tools/fmt.mjs +269 -22
  56. package/tools/ini.mjs +112 -1
  57. package/tools/lsp.mjs +56 -6
  58. package/tools/repl.mjs +5 -2
  59. package/tools/self.mjs +15 -0
  60. package/tools/test.mjs +100 -0
  61. package/tools/web_repl_export.mjs +4 -0
  62. package/tree-sitter/package.json +1 -1
  63. package/tree-sitter/tree-sitter.json +1 -1
  64. package/editor-plugins/nvim/rapydscript/README.md +0 -184
  65. package/editor-plugins/nvim/rapydscript/bin/rapydscript.so +0 -0
  66. package/editor-plugins/nvim/rapydscript/ftdetect/rapydscript.lua +0 -10
  67. package/editor-plugins/nvim/rapydscript/lua/rapydscript/health.lua +0 -88
  68. package/editor-plugins/nvim/rapydscript/lua/rapydscript/init.lua +0 -279
  69. /package/tree-sitter/queries/{rapydscript/highlights.scm → highlights.scm} +0 -0
  70. /package/tree-sitter/queries/{rapydscript/indents.scm → indents.scm} +0 -0
  71. /package/tree-sitter/queries/{rapydscript/injections.scm → injections.scm} +0 -0
  72. /package/tree-sitter/queries/{rapydscript/locals.scm → locals.scm} +0 -0
package/tools/fmt.mjs CHANGED
@@ -27,6 +27,8 @@
27
27
 
28
28
  import fs from 'fs';
29
29
  import path from 'path';
30
+ import { fileURLToPath } from 'url';
31
+ import { read_pyproject_config } from './ini.mjs';
30
32
 
31
33
  // ---------------------------------------------------------------------------
32
34
  // Lexer
@@ -265,7 +267,6 @@ function is_operand(p) {
265
267
  function is_call_paren(p) {
266
268
  if (!p) return false;
267
269
  if (p.type === 'name' || p.type === 'string' || p.type === 'atom') return true;
268
- if (p.value === 'def') return true;
269
270
  return p.value === ')' || p.value === ']' || p.value === '}';
270
271
  }
271
272
 
@@ -297,7 +298,7 @@ function separator(p, t, stack, prev_was_unary, sig_count, first_at) {
297
298
  if (pv === '@' && first_at && sig_count === 1) return false; // decorator
298
299
  if (tv === '(' && is_call_paren(p)) return false;
299
300
  if (tv === '[' && is_index_bracket(p)) return false;
300
- if ((tv === '=' || pv === '=') && top && top.kind === 'call') return false; // kwarg / default
301
+ if ((tv === '=' || pv === '=') && top && top.kind === 'call' && !top.in_def_body) return false; // kwarg / default
301
302
  if (prev_was_unary) return false;
302
303
  return true;
303
304
  }
@@ -350,6 +351,12 @@ function render(toks, honor_breaks, src_base, new_base, opts, initial_stack) {
350
351
  var out = '';
351
352
  var stack = initial_stack ? initial_stack.slice() : [];
352
353
  var prev_sig = null, prev_was_unary = false, sig_count = 0, first_at = false;
354
+ // after_def: true after seeing 'def' (and optionally a function name), so the
355
+ // next '(' can be identified as the def's parameter list.
356
+ // prev_closed_def_params: true when we just popped a def parameter list ')'.
357
+ // When the very next ':' is seen we mark the enclosing stack frame in_def_body
358
+ // so that '=' tokens inside the body are not suppressed as kwargs.
359
+ var after_def = false, prev_closed_def_params = false;
353
360
  for (var k = 0; k < toks.length; k++) {
354
361
  var t = toks[k];
355
362
  var did_break = false;
@@ -375,10 +382,43 @@ function render(toks, honor_breaks, src_base, new_base, opts, initial_stack) {
375
382
  out += token_text(t, opts);
376
383
  prev_was_unary = is_unary_here(prev_sig, t);
377
384
  if (t.type === 'punc') {
378
- if (t.value === '(') stack.push({ kind: is_call_paren(prev_sig) ? 'call' : 'group' });
379
- else if (t.value === '[') stack.push({ kind: is_index_bracket(prev_sig) ? 'index' : 'list' });
380
- else if (t.value === '{') stack.push({ kind: 'dict' });
381
- else if (t.value === ')' || t.value === ']' || t.value === '}') { if (stack.length) stack.pop(); }
385
+ if (t.value === '(') {
386
+ stack.push({ kind: is_call_paren(prev_sig) ? 'call' : 'group', is_def_params: after_def });
387
+ after_def = false; prev_closed_def_params = false;
388
+ } else if (t.value === '[') {
389
+ stack.push({ kind: is_index_bracket(prev_sig) ? 'index' : 'list' });
390
+ after_def = false; prev_closed_def_params = false;
391
+ } else if (t.value === '{') {
392
+ stack.push({ kind: 'dict' });
393
+ after_def = false; prev_closed_def_params = false;
394
+ } else if (t.value === ')' || t.value === ']' || t.value === '}') {
395
+ var popped = stack.length ? stack.pop() : null;
396
+ prev_closed_def_params = !!(popped && popped.is_def_params);
397
+ after_def = false;
398
+ } else if (t.value === ':') {
399
+ if (prev_closed_def_params && stack.length > 0) {
400
+ // Only mark in_def_body for multi-line def bodies. For inline
401
+ // defs (def():stmt; inside a call), the body is on the same
402
+ // line and subsequent same-depth tokens are still kwargs.
403
+ var next_is_newline = false;
404
+ for (var nk = k + 1; nk < toks.length; nk++) {
405
+ if (toks[nk].type === 'eof') break;
406
+ if (toks[nk].type !== 'comment' && toks[nk].type !== 'shebang') {
407
+ next_is_newline = toks[nk].nlb > 0; break;
408
+ }
409
+ }
410
+ if (next_is_newline) stack[stack.length - 1].in_def_body = true;
411
+ }
412
+ prev_closed_def_params = false; after_def = false;
413
+ } else {
414
+ after_def = false; prev_closed_def_params = false;
415
+ }
416
+ } else if (t.type === 'keyword' && t.value === 'def') {
417
+ after_def = true; prev_closed_def_params = false;
418
+ } else if (after_def && t.type === 'name') {
419
+ prev_closed_def_params = false; // named def: keep after_def for next '('
420
+ } else {
421
+ after_def = false; prev_closed_def_params = false;
382
422
  }
383
423
  if (sig_count === 0 && t.value === '@' && t.type === 'op') first_at = true;
384
424
  prev_sig = t; sig_count++;
@@ -578,6 +618,141 @@ function is_visual_def_start(elements, idx) {
578
618
  return true;
579
619
  }
580
620
 
621
+ // ---------------------------------------------------------------------------
622
+ // Import organization: __python__ group first, then stdlib group, then other group
623
+ // ---------------------------------------------------------------------------
624
+
625
+ // Stdlib module list is generated by self.mjs into dev/stdlib_modules.json by
626
+ // reading src/lib/. Fall back to reading src/lib/ directly when the generated
627
+ // file is absent (e.g. running straight from the release/ tree).
628
+ var STDLIB_MODULES = (function () {
629
+ var em = globalThis.__rapydscript_embedded__;
630
+ if (em) {
631
+ if (em.stdlib_modules) return new Set(em.stdlib_modules);
632
+ if (em.stdlib) return new Set(Object.keys(em.stdlib)
633
+ .filter(function (n) { return n.slice(-4) === '.pyj'; })
634
+ .map(function (n) { return n.slice(0, -4); }));
635
+ }
636
+ try {
637
+ return new Set(JSON.parse(fs.readFileSync(
638
+ new URL('../dev/stdlib_modules.json', import.meta.url), 'utf-8')));
639
+ } catch (e) { /* generated file absent; fall through */ }
640
+ try {
641
+ return new Set(fs.readdirSync(fileURLToPath(new URL('../src/lib/', import.meta.url)))
642
+ .filter(function (n) { return n.slice(-4) === '.pyj'; })
643
+ .map(function (n) { return n.slice(0, -4); }));
644
+ } catch (e) { return new Set(); }
645
+ }());
646
+
647
+ function is_import_stmt(el) {
648
+ if (el.kind !== 'stmt') return false;
649
+ var sig = el.tokens.filter(function (t) { return t.type !== 'comment'; });
650
+ if (!sig.length) return false;
651
+ return sig[0].type === 'keyword' && (sig[0].value === 'import' || sig[0].value === 'from');
652
+ }
653
+
654
+ function is_python_builtin_import(el) {
655
+ var sig = el.tokens.filter(function (t) { return t.type !== 'comment'; });
656
+ if (sig.length < 3) return false;
657
+ return sig[0].type === 'keyword' && sig[0].value === 'from' &&
658
+ sig[1].type === 'name' && sig[1].value === '__python__' &&
659
+ sig[2].type === 'keyword' && sig[2].value === 'import';
660
+ }
661
+
662
+ function import_top_module(el) {
663
+ var sig = el.tokens.filter(function (t) { return t.type !== 'comment'; });
664
+ var i = 1; // skip leading 'import' or 'from'
665
+ while (i < sig.length && sig[i].type === 'punc' && sig[i].value === '.') i++; // skip relative dots
666
+ if (i < sig.length && sig[i].type === 'name') return sig[i].value;
667
+ return '';
668
+ }
669
+
670
+ // Returns a string sort key: '0\0<module>\0' for 'import X', '1\0<module>\0<names>' for 'from X import Y'.
671
+ // This puts bare imports before from-imports (isort convention), then sorts alphabetically.
672
+ function import_sort_key(el) {
673
+ var sig = el.tokens.filter(function (t) { return t.type !== 'comment'; });
674
+ if (!sig.length) return '2\0\0';
675
+ var is_from = sig[0].value === 'from';
676
+ var i = 1, dots = '', mod_parts = [];
677
+ while (i < sig.length && sig[i].type === 'punc' && sig[i].value === '.') { dots += '.'; i++; }
678
+ while (i < sig.length && !(sig[i].type === 'keyword' && sig[i].value === 'import')) {
679
+ if (sig[i].type === 'name') mod_parts.push(sig[i].value);
680
+ i++;
681
+ }
682
+ var mod = (dots + mod_parts.join('.')).toLowerCase();
683
+ if (!is_from) return '0\0' + mod + '\0';
684
+ i++; // skip 'import' keyword
685
+ var names = [];
686
+ while (i < sig.length) { if (sig[i].type === 'name') names.push(sig[i].value.toLowerCase()); i++; }
687
+ return '1\0' + mod + '\0' + names.join(',');
688
+ }
689
+
690
+ function organize_import_elements(elements) {
691
+ // Identify the contiguous top-level import block: from the first level-0 import
692
+ // statement to the last, stopping at the first level-0 non-import statement.
693
+ var block_start = -1, block_end = -1;
694
+ for (var i = 0; i < elements.length; i++) {
695
+ var el = elements[i];
696
+ if (el.kind !== 'stmt' || el.level !== 0) continue;
697
+ if (is_import_stmt(el)) {
698
+ if (block_start < 0) block_start = i;
699
+ block_end = i;
700
+ } else {
701
+ if (block_start >= 0) break;
702
+ }
703
+ }
704
+ if (block_start < 0) return elements;
705
+
706
+ var block = elements.slice(block_start, block_end + 1);
707
+ var imports = [], leading_comments = [], seen_import = false;
708
+ for (var j = 0; j < block.length; j++) {
709
+ var bel = block[j];
710
+ if (bel.kind === 'stmt') { seen_import = true; imports.push(bel); }
711
+ else if (bel.kind === 'comment' && !seen_import) leading_comments.push(bel);
712
+ // Comments after the first import are dropped (import organizers conventionally do this).
713
+ }
714
+ if (!imports.length) return elements;
715
+
716
+ var python = [], stdlib = [], other = [];
717
+ for (var k = 0; k < imports.length; k++) {
718
+ if (is_python_builtin_import(imports[k])) python.push(imports[k]);
719
+ else if (STDLIB_MODULES.has(import_top_module(imports[k]))) stdlib.push(imports[k]);
720
+ else other.push(imports[k]);
721
+ }
722
+ function cmp(a, b) { var ka = import_sort_key(a), kb = import_sort_key(b); return ka < kb ? -1 : ka > kb ? 1 : 0; }
723
+ python.sort(cmp);
724
+ stdlib.sort(cmp);
725
+ other.sort(cmp);
726
+
727
+ var new_block = [], first_blank = block[0].blank_before || 0;
728
+ for (var lc = 0; lc < leading_comments.length; lc++) {
729
+ var cm = Object.assign({}, leading_comments[lc]);
730
+ if (lc === 0) cm.blank_before = first_blank;
731
+ new_block.push(cm);
732
+ first_blank = 0;
733
+ }
734
+ for (var p = 0; p < python.length; p++) {
735
+ var pi = Object.assign({}, python[p]);
736
+ pi.blank_before = (p === 0) ? first_blank : 0;
737
+ new_block.push(pi);
738
+ first_blank = 0;
739
+ }
740
+ var has_python = python.length > 0;
741
+ for (var s = 0; s < stdlib.length; s++) {
742
+ var si = Object.assign({}, stdlib[s]);
743
+ si.blank_before = (s === 0 && has_python) ? 1 : (s === 0 ? first_blank : 0);
744
+ new_block.push(si);
745
+ first_blank = 0;
746
+ }
747
+ var has_stdlib_or_python = stdlib.length > 0 || has_python;
748
+ for (var o = 0; o < other.length; o++) {
749
+ var oi = Object.assign({}, other[o]);
750
+ oi.blank_before = (o === 0 && has_stdlib_or_python) ? 1 : (o === 0 ? first_blank : 0);
751
+ new_block.push(oi);
752
+ }
753
+ return elements.slice(0, block_start).concat(new_block).concat(elements.slice(block_end + 1));
754
+ }
755
+
581
756
  // ---------------------------------------------------------------------------
582
757
  // Element rendering + assembly
583
758
  // ---------------------------------------------------------------------------
@@ -589,9 +764,21 @@ function render_stmt(el, opts) {
589
764
  }
590
765
  var comment = null, code = el.tokens;
591
766
  if (code.length && code[code.length - 1].type === 'comment') { comment = code[code.length - 1]; code = code.slice(0, -1); }
767
+ var comment_str = comment ? ' ' + format_comment(comment.value) : '';
768
+ if (!opts.join_lines) {
769
+ // Preserve source line breaks unless a resulting line exceeds the limit,
770
+ // in which case fall through to the collapse+wrap path below.
771
+ var preserved = base_indent + render(code, true, el.indent, base_indent, opts);
772
+ var preserved_full = preserved + comment_str;
773
+ var plines = preserved_full.split('\n');
774
+ var fits = true;
775
+ for (var pi = 0; pi < plines.length; pi++) {
776
+ if (plines[pi].length > opts.line_length) { fits = false; break; }
777
+ }
778
+ if (fits) return preserved_full;
779
+ }
592
780
  var line = render(code, false, el.indent, base_indent, opts);
593
781
  var full = base_indent + line;
594
- var comment_str = comment ? ' ' + format_comment(comment.value) : '';
595
782
  if ((full.length + comment_str.length) > opts.line_length && el.wrappable) {
596
783
  var wrapped = wrap_line(code, el.level, opts);
597
784
  if (wrapped !== null) return comment_str ? wrapped + comment_str : wrapped;
@@ -599,13 +786,7 @@ function render_stmt(el, opts) {
599
786
  return full + comment_str;
600
787
  }
601
788
 
602
- export function format_string(src, options) {
603
- var opts = normalize_opts(options);
604
- var text = src.replace(/\r\n?/g, '\n');
605
- var toks = lex(text);
606
- var elements = group(toks);
607
- compute_levels(elements);
608
-
789
+ function render_elements(elements, opts) {
609
790
  var result = '';
610
791
  for (var idx = 0; idx < elements.length; idx++) {
611
792
  var el = elements[idx];
@@ -630,6 +811,26 @@ export function format_string(src, options) {
630
811
  return result + '\n';
631
812
  }
632
813
 
814
+ export function format_string(src, options) {
815
+ var opts = normalize_opts(options);
816
+ var text = src.replace(/\r\n?/g, '\n');
817
+ var toks = lex(text);
818
+ var elements = group(toks);
819
+ compute_levels(elements);
820
+ elements = organize_import_elements(elements);
821
+ return render_elements(elements, opts);
822
+ }
823
+
824
+ export function organize_imports(src, options) {
825
+ var opts = normalize_opts(options);
826
+ var text = src.replace(/\r\n?/g, '\n');
827
+ var toks = lex(text);
828
+ var elements = group(toks);
829
+ compute_levels(elements);
830
+ elements = organize_import_elements(elements);
831
+ return render_elements(elements, opts);
832
+ }
833
+
633
834
  function normalize_opts(options) {
634
835
  options = options || {};
635
836
  var ll = options.line_length;
@@ -640,7 +841,7 @@ function normalize_opts(options) {
640
841
  var pq = options.preferred_quote;
641
842
  pref = (pq === 'double' || pq === '"') ? '"' : "'";
642
843
  }
643
- return { line_length: ll, preferred: pref };
844
+ return { line_length: ll, preferred: pref, join_lines: !!options.join_lines };
644
845
  }
645
846
 
646
847
  // ---------------------------------------------------------------------------
@@ -671,9 +872,18 @@ export async function collect_pyj_files(inputs) {
671
872
  var f = list[i];
672
873
  var st;
673
874
  try { st = await fs.promises.lstat(f); }
674
- catch (e) { throw new Error("can't access: " + f); }
875
+ catch (e) {
876
+ if (from_dir && (e.code === 'EACCES' || e.code === 'EPERM')) continue;
877
+ throw new Error("can't access: " + f);
878
+ }
675
879
  if (st.isDirectory()) {
676
- var children = (await fs.promises.readdir(f)).sort().map(function (x) { return path.join(f, x); });
880
+ var children;
881
+ try {
882
+ children = (await fs.promises.readdir(f)).sort().map(function (x) { return path.join(f, x); });
883
+ } catch (e) {
884
+ if (from_dir && (e.code === 'EACCES' || e.code === 'EPERM')) continue;
885
+ throw new Error("can't read directory: " + f);
886
+ }
677
887
  await walk(children, true);
678
888
  } else if (st.isFile()) {
679
889
  if (from_dir) { if (f.endsWith('.pyj')) files.push(f); }
@@ -696,8 +906,36 @@ async function read_stdin() {
696
906
  return chunks.join('');
697
907
  }
698
908
 
909
+ function argv_has_flag(flag_names) {
910
+ var args = process.argv.slice(3); // skip: node, script path, mode
911
+ for (var i = 0; i < args.length; i++) {
912
+ var a = args[i];
913
+ if (a === '--') break;
914
+ for (var j = 0; j < flag_names.length; j++) {
915
+ var f = flag_names[j];
916
+ if (a === f || a.startsWith(f + '=')) return true;
917
+ }
918
+ }
919
+ return false;
920
+ }
921
+
699
922
  export async function cli(argv, base_path, src_path, lib_path) {
700
- var opts = normalize_opts({ line_length: argv.line_length, preferred_quote: argv.preferred_quote });
923
+ var ll_from_cli = argv_has_flag(['--line-length', '--line_length', '-l']);
924
+ var q_from_cli = argv_has_flag(['--preferred-quote', '--preferred_quote', '-q']);
925
+
926
+ var effective_ll = ll_from_cli ? (parseInt(argv.line_length, 10) || 80) : null;
927
+ var effective_quote = q_from_cli ? argv.preferred_quote : null;
928
+
929
+ if (!ll_from_cli || !q_from_cli) {
930
+ var pyconf = await read_pyproject_config(process.cwd());
931
+ if (!ll_from_cli && pyconf.line_length) effective_ll = pyconf.line_length;
932
+ if (!q_from_cli && pyconf.preferred_quote) effective_quote = pyconf.preferred_quote;
933
+ }
934
+
935
+ if (!effective_ll) effective_ll = 80;
936
+ if (!effective_quote) effective_quote = 'single';
937
+
938
+ var opts = normalize_opts({ line_length: effective_ll, preferred_quote: effective_quote, join_lines: argv.join_lines });
701
939
  var inputs = (argv.files || []).slice();
702
940
 
703
941
  if (inputs.length === 0) {
@@ -725,10 +963,19 @@ export async function cli(argv, base_path, src_path, lib_path) {
725
963
  if (argv.check_only) {
726
964
  var errs = check_report(f, code, formatted, opts);
727
965
  if (errs.length) { had_errors = true; errs.forEach(function (m) { console.error(m); }); }
728
- } else if (formatted !== code.replace(/\r\n?/g, '\n')) {
729
- try { await fs.promises.writeFile(f, formatted); }
730
- catch (e) { console.error("ERROR: can't write file: " + f); process.exit(2); }
731
- console.log('reformatted ' + f);
966
+ } else {
967
+ if (formatted !== code.replace(/\r\n?/g, '\n')) {
968
+ try { await fs.promises.writeFile(f, formatted); }
969
+ catch (e) { console.error("ERROR: can't write file: " + f); process.exit(2); }
970
+ console.log('reformatted ' + f);
971
+ }
972
+ var flines = formatted.split('\n');
973
+ for (var li = 0; li < flines.length; li++) {
974
+ if (flines[li].length > opts.line_length) {
975
+ console.error(f + ':' + (li + 1) + ': line exceeds ' + opts.line_length + ' characters (' + flines[li].length + ')');
976
+ had_errors = true;
977
+ }
978
+ }
732
979
  }
733
980
  }
734
981
  process.exit(had_errors ? 1 : 0);
package/tools/ini.mjs CHANGED
@@ -60,5 +60,116 @@ async function read_config(toplevel_dir) {
60
60
  return parse_ini_data(data);
61
61
  }
62
62
 
63
+ // ---------------------------------------------------------------------------
64
+ // pyproject.toml reading
65
+ // ---------------------------------------------------------------------------
63
66
 
64
- export { read_config };
67
+ async function find_pyproject_toml(start_dir) {
68
+ var current = start_dir;
69
+ while (true) {
70
+ try {
71
+ return await fs.promises.readFile(path.join(current, 'pyproject.toml'), 'utf-8');
72
+ } catch (e) {
73
+ if (e.code !== 'ENOENT' && e.code !== 'EACCES' && e.code !== 'EPERM') throw e;
74
+ }
75
+ var parent = path.dirname(current);
76
+ if (parent === current) return null;
77
+ current = parent;
78
+ }
79
+ }
80
+
81
+ function parse_toml_sections(text) {
82
+ // Minimal TOML parser: returns a flat map of dotted-section-name -> {key: value}.
83
+ // Only handles simple string and integer scalar values needed for config settings.
84
+ var sections = {};
85
+ var cur = null;
86
+ var lines = text.split(/\r?\n/);
87
+ for (var i = 0; i < lines.length; i++) {
88
+ var line = lines[i].trim();
89
+ if (!line || line[0] === '#') continue;
90
+ // Section header: [tool.ruff] or [tool.ruff.format] — not [[array]] tables.
91
+ var sec_m = line.match(/^\[([^\[\]]+)\]$/);
92
+ if (sec_m) {
93
+ var sec_name = sec_m[1].trim();
94
+ if (!sections[sec_name]) sections[sec_name] = {};
95
+ cur = sections[sec_name];
96
+ continue;
97
+ }
98
+ if (!cur) continue;
99
+ var eq = line.indexOf('=');
100
+ if (eq < 0) continue;
101
+ var key = line.slice(0, eq).trim();
102
+ var rest = line.slice(eq + 1).trim();
103
+ if (!key) continue;
104
+ var val;
105
+ if (rest[0] === '"' || rest[0] === "'") {
106
+ var q = rest[0];
107
+ if (rest.slice(0, 3) === q + q + q) continue; // triple-quoted: skip
108
+ var j = 1;
109
+ while (j < rest.length && rest[j] !== q) {
110
+ if (rest[j] === '\\') j++;
111
+ j++;
112
+ }
113
+ val = rest.slice(1, j);
114
+ } else {
115
+ var hash = rest.indexOf('#');
116
+ if (hash >= 0) rest = rest.slice(0, hash).trim();
117
+ val = rest;
118
+ }
119
+ cur[key] = val;
120
+ }
121
+ return sections;
122
+ }
123
+
124
+ function pyproject_line_length(sections) {
125
+ var candidates = [
126
+ ['tool.ruff', 'line-length'],
127
+ ['tool.black', 'line-length'],
128
+ ['tool.isort', 'line_length'],
129
+ ['tool.isort', 'line-length'],
130
+ ];
131
+ for (var i = 0; i < candidates.length; i++) {
132
+ var sec = sections[candidates[i][0]];
133
+ if (sec && sec[candidates[i][1]] !== undefined) {
134
+ var ll = parseInt(sec[candidates[i][1]], 10);
135
+ if (!isNaN(ll) && ll > 0) return ll;
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function pyproject_quote(sections) {
142
+ // tool.ruff.format.quote-style (skip 'preserve')
143
+ var rf = sections['tool.ruff.format'];
144
+ if (rf && rf['quote-style']) {
145
+ if (rf['quote-style'] === 'double') return 'double';
146
+ if (rf['quote-style'] === 'single') return 'single';
147
+ }
148
+ // tool.ruff.lint.flake8-quotes.inline-quotes
149
+ var fq = sections['tool.ruff.lint.flake8-quotes'];
150
+ if (fq && fq['inline-quotes']) {
151
+ if (fq['inline-quotes'] === 'double') return 'double';
152
+ if (fq['inline-quotes'] === 'single') return 'single';
153
+ }
154
+ // tool.flake8.inline-quotes
155
+ var flk = sections['tool.flake8'];
156
+ if (flk && flk['inline-quotes']) {
157
+ if (flk['inline-quotes'] === 'double') return 'double';
158
+ if (flk['inline-quotes'] === 'single') return 'single';
159
+ }
160
+ return null;
161
+ }
162
+
163
+ async function read_pyproject_config(start_dir) {
164
+ var text = await find_pyproject_toml(start_dir);
165
+ if (!text) return {};
166
+ var sections = parse_toml_sections(text);
167
+ var result = {};
168
+ var ll = pyproject_line_length(sections);
169
+ if (ll !== null) result.line_length = ll;
170
+ var q = pyproject_quote(sections);
171
+ if (q !== null) result.preferred_quote = q;
172
+ return result;
173
+ }
174
+
175
+ export { read_config, read_pyproject_config };
package/tools/lsp.mjs CHANGED
@@ -22,9 +22,10 @@ import crypto from 'crypto';
22
22
  import { pathToFileURL, fileURLToPath } from 'url';
23
23
  import * as utils from './utils.mjs';
24
24
  import { lint_parsed, BUILTINS, WARN, ERROR, MESSAGES } from './lint.mjs';
25
- import { format_string } from './fmt.mjs';
25
+ import { format_string, organize_imports } from './fmt.mjs';
26
26
  import * as sym from './lsp_symbols.mjs';
27
27
  import { create_connection, DocumentStore, TextDocument, ResponseError, ErrorCodes } from './lsp_protocol.mjs';
28
+ import { read_pyproject_config } from './ini.mjs';
28
29
 
29
30
  const RapydScript = globalThis.create_rapydscript_compiler();
30
31
 
@@ -37,7 +38,7 @@ var CompletionItemKind = {
37
38
  Class: 7, Interface: 8, Module: 9, Property: 10, Keyword: 14, Constant: 21,
38
39
  };
39
40
  var SymbolKindLSP = { File: 1, Module: 2, Class: 5, Method: 6, Property: 7, Field: 8, Function: 12, Variable: 13, Constant: 14 };
40
- var CodeActionKind = { QuickFix: 'quickfix', SourceFixAll: 'source.fixAll', Source: 'source' };
41
+ var CodeActionKind = { QuickFix: 'quickfix', SourceFixAll: 'source.fixAll', Source: 'source', OrganizeImports: 'source.organizeImports' };
41
42
 
42
43
  function kind_to_completion(kind) {
43
44
  switch (kind) {
@@ -72,6 +73,7 @@ export function create_server_context(opts) {
72
73
  libdir: opts.libdir || null, // stdlib dir (src/lib)
73
74
  line_length: opts.line_length || 80,
74
75
  preferred_quote: opts.preferred_quote || 'single',
76
+ join_lines: opts.join_lines || false,
75
77
  docs: new DocumentStore(),
76
78
  analysis_cache: Object.create(null), // key -> analysis
77
79
  workspace_roots: opts.workspace_roots ? opts.workspace_roots.slice() : [],
@@ -96,6 +98,10 @@ export function apply_configuration(ctx, settings) {
96
98
  ctx.preferred_quote = settings.preferredQuote;
97
99
  }
98
100
 
101
+ if (settings.joinLines !== undefined && settings.joinLines !== null) {
102
+ ctx.join_lines = !!settings.joinLines;
103
+ }
104
+
99
105
  if (settings.importPath !== undefined && settings.importPath !== null) {
100
106
  var new_dirs;
101
107
  if (Array.isArray(settings.importPath)) {
@@ -276,7 +282,7 @@ function lint_message_to_diagnostic(doc, m) {
276
282
  // ---------------------------------------------------------------------------
277
283
  export function format_document(ctx, raw_text) {
278
284
  var text = normalize(raw_text);
279
- var formatted = format_string(text, { line_length: ctx.line_length, preferred_quote: ctx.preferred_quote });
285
+ var formatted = format_string(text, { line_length: ctx.line_length, preferred_quote: ctx.preferred_quote, join_lines: ctx.join_lines });
280
286
  if (formatted === text) return [];
281
287
  // Single edit that replaces the whole document.
282
288
  var doc = new TextDocument('inmem', 'rapydscript', 0, text);
@@ -284,6 +290,18 @@ export function format_document(ctx, raw_text) {
284
290
  return [{ range: { start: { line: 0, character: 0 }, end: end }, newText: formatted }];
285
291
  }
286
292
 
293
+ // ---------------------------------------------------------------------------
294
+ // Organize imports
295
+ // ---------------------------------------------------------------------------
296
+ export function organize_imports_document(ctx, raw_text) {
297
+ var text = normalize(raw_text);
298
+ var organized = organize_imports(text, { line_length: ctx.line_length, preferred_quote: ctx.preferred_quote, join_lines: ctx.join_lines });
299
+ if (organized === text) return [];
300
+ var doc = new TextDocument('inmem', 'rapydscript', 0, text);
301
+ var end = doc.position_at(text.length);
302
+ return [{ range: { start: { line: 0, character: 0 }, end: end }, newText: organized }];
303
+ }
304
+
287
305
  // ---------------------------------------------------------------------------
288
306
  // Completion
289
307
  // ---------------------------------------------------------------------------
@@ -722,6 +740,9 @@ export async function code_actions(ctx, uri, raw_text, range, diagnostics) {
722
740
  // Source action: format the document.
723
741
  var fmt_edits = format_document(ctx, a.text);
724
742
  if (fmt_edits.length) actions.push({ title: 'Format document', kind: CodeActionKind.Source, edit: { changes: single_change(uri, fmt_edits) } });
743
+ // Source action: organize imports.
744
+ var org_edits = organize_imports_document(ctx, a.text);
745
+ if (org_edits.length) actions.push({ title: 'Organize imports', kind: CodeActionKind.OrganizeImports, edit: { changes: single_change(uri, org_edits) } });
725
746
  return actions;
726
747
  }
727
748
 
@@ -754,12 +775,41 @@ export async function document_symbols(ctx, uri, raw_text) {
754
775
  // ===========================================================================
755
776
  // Server / CLI
756
777
  // ===========================================================================
778
+ function argv_has_flag(flag_names) {
779
+ var args = process.argv.slice(3); // skip: node, script path, mode
780
+ for (var i = 0; i < args.length; i++) {
781
+ var a = args[i];
782
+ if (a === '--') break;
783
+ for (var j = 0; j < flag_names.length; j++) {
784
+ var f = flag_names[j];
785
+ if (a === f || a.startsWith(f + '=')) return true;
786
+ }
787
+ }
788
+ return false;
789
+ }
790
+
757
791
  export async function cli(argv, base_path, src_path, lib_path) {
792
+ var ll_from_cli = argv_has_flag(['--line-length', '--line_length', '-l']);
793
+ var q_from_cli = argv_has_flag(['--preferred-quote', '--preferred_quote', '-q']);
794
+
795
+ var effective_ll = ll_from_cli ? (parseInt(argv.line_length, 10) || 80) : null;
796
+ var effective_quote = q_from_cli ? argv.preferred_quote : null;
797
+
798
+ if (!ll_from_cli || !q_from_cli) {
799
+ var pyconf = await read_pyproject_config(process.cwd());
800
+ if (!ll_from_cli && pyconf.line_length) effective_ll = pyconf.line_length;
801
+ if (!q_from_cli && pyconf.preferred_quote) effective_quote = pyconf.preferred_quote;
802
+ }
803
+
804
+ if (!effective_ll) effective_ll = 80;
805
+ if (!effective_quote) effective_quote = 'single';
806
+
758
807
  var ctx = create_server_context({
759
808
  import_dirs: utils.get_import_dirs(argv.import_path).map(function (p) { return path.resolve(p); }),
760
809
  libdir: path.join(src_path, 'lib'),
761
- line_length: parseInt(argv.line_length, 10) || 80,
762
- preferred_quote: argv.preferred_quote || 'single',
810
+ line_length: effective_ll,
811
+ preferred_quote: effective_quote,
812
+ join_lines: argv.join_lines || false,
763
813
  });
764
814
 
765
815
  // Diagnostics are pushed to the client, debounced per document.
@@ -797,7 +847,7 @@ export async function cli(argv, base_path, src_path, lib_path) {
797
847
  renameProvider: true,
798
848
  documentFormattingProvider: true,
799
849
  documentSymbolProvider: true,
800
- codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix, CodeActionKind.Source] },
850
+ codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix, CodeActionKind.Source, CodeActionKind.OrganizeImports] },
801
851
  },
802
852
  serverInfo: { name: 'rapydscript-lsp', version: '1.0.0' },
803
853
  };
package/tools/repl.mjs CHANGED
@@ -12,6 +12,7 @@ import vm from 'vm';
12
12
  import util from 'util';
13
13
  import { createRequire } from 'module';
14
14
  import * as utils from './utils.mjs';
15
+ import { EMBEDDED_STDLIB_PREFIX } from './compiler.mjs';
15
16
  import completelib from './completer.mjs';
16
17
 
17
18
  var colored = utils.safe_colored;
@@ -84,7 +85,9 @@ export default async function(options) {
84
85
 
85
86
  // Read baselib synchronously: the repl must initialize synchronously so
86
87
  // that the readline interface and prompt are ready before returning.
87
- var baselib_plain = fs.readFileSync(path.join(options.lib_path, 'baselib-plain-pretty.js'), 'utf-8');
88
+ const _embedded = globalThis.__rapydscript_embedded__;
89
+ var baselib_plain = _embedded?.['baselib-plain-pretty.js'] ??
90
+ fs.readFileSync(path.join(options.lib_path, 'baselib-plain-pretty.js'), 'utf-8');
88
91
 
89
92
  function print_ast(ast, keep_baselib) {
90
93
  var output_options = {omit_baselib:!keep_baselib, write_name:false, private_scope:false, beautify:true, keep_docstrings:true};
@@ -160,7 +163,7 @@ export default async function(options) {
160
163
  toplevel = await RapydScript.parse(source, {
161
164
  'filename':'<repl>',
162
165
  'basedir': process.cwd(),
163
- 'libdir': options.imp_path,
166
+ 'libdir': globalThis.__rapydscript_embedded__ ? EMBEDDED_STDLIB_PREFIX : options.imp_path,
164
167
  'import_dirs': import_dirs,
165
168
  'classes': classes,
166
169
  'scoped_flags': scoped_flags,