@tishlang/tish 1.7.0 → 1.8.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.
Files changed (95) hide show
  1. package/Cargo.toml +1 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/transform/expr.rs +28 -8
  5. package/crates/js_to_tish/src/transform/stmt.rs +49 -22
  6. package/crates/tish/Cargo.toml +15 -5
  7. package/crates/tish/src/cargo_native_registry.rs +29 -0
  8. package/crates/tish/src/cli_help.rs +16 -10
  9. package/crates/tish/src/main.rs +87 -32
  10. package/crates/tish/src/repl_completion.rs +3 -3
  11. package/crates/tish/tests/cargo_example_compile.rs +1 -1
  12. package/crates/tish/tests/integration_test.rs +19 -7
  13. package/crates/tish/tests/shortcircuit.rs +1 -1
  14. package/crates/tish_ast/src/ast.rs +80 -9
  15. package/crates/tish_build_utils/Cargo.toml +4 -0
  16. package/crates/tish_build_utils/src/lib.rs +105 -2
  17. package/crates/tish_builtins/Cargo.toml +5 -1
  18. package/crates/tish_builtins/src/array.rs +13 -12
  19. package/crates/tish_builtins/src/construct.rs +34 -33
  20. package/crates/tish_builtins/src/globals.rs +12 -11
  21. package/crates/tish_builtins/src/helpers.rs +2 -1
  22. package/crates/tish_builtins/src/object.rs +3 -2
  23. package/crates/tish_builtins/src/string.rs +73 -3
  24. package/crates/tish_bytecode/src/compiler.rs +12 -14
  25. package/crates/tish_bytecode/src/opcode.rs +12 -3
  26. package/crates/tish_compile/Cargo.toml +1 -0
  27. package/crates/tish_compile/src/codegen.rs +745 -199
  28. package/crates/tish_compile/src/infer.rs +6 -0
  29. package/crates/tish_compile/src/lib.rs +4 -3
  30. package/crates/tish_compile/src/resolve.rs +180 -82
  31. package/crates/tish_compile/src/types.rs +175 -11
  32. package/crates/tish_compile_js/Cargo.toml +1 -0
  33. package/crates/tish_compile_js/src/codegen.rs +152 -29
  34. package/crates/tish_compile_js/src/lib.rs +3 -1
  35. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
  36. package/crates/tish_core/Cargo.toml +8 -0
  37. package/crates/tish_core/src/json.rs +102 -53
  38. package/crates/tish_core/src/lib.rs +3 -1
  39. package/crates/tish_core/src/macros.rs +5 -5
  40. package/crates/tish_core/src/value.rs +53 -15
  41. package/crates/tish_core/src/vmref.rs +178 -0
  42. package/crates/tish_eval/Cargo.toml +17 -2
  43. package/crates/tish_eval/src/eval.rs +90 -28
  44. package/crates/tish_eval/src/http.rs +61 -0
  45. package/crates/tish_eval/src/lib.rs +3 -3
  46. package/crates/tish_eval/src/natives.rs +41 -0
  47. package/crates/tish_eval/src/value.rs +7 -3
  48. package/crates/tish_eval/src/value_convert.rs +13 -5
  49. package/crates/tish_fmt/src/lib.rs +120 -30
  50. package/crates/tish_lexer/src/lib.rs +20 -5
  51. package/crates/tish_lexer/src/token.rs +4 -0
  52. package/crates/tish_llvm/src/lib.rs +3 -1
  53. package/crates/tish_lsp/Cargo.toml +4 -1
  54. package/crates/tish_lsp/README.md +1 -1
  55. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  56. package/crates/tish_lsp/src/import_goto.rs +549 -0
  57. package/crates/tish_lsp/src/main.rs +502 -102
  58. package/crates/tish_native/src/build.rs +3 -2
  59. package/crates/tish_native/src/lib.rs +6 -2
  60. package/crates/tish_opt/src/lib.rs +17 -2
  61. package/crates/tish_parser/src/lib.rs +10 -3
  62. package/crates/tish_parser/src/parser.rs +346 -56
  63. package/crates/tish_resolve/Cargo.toml +13 -0
  64. package/crates/tish_resolve/src/lib.rs +3436 -0
  65. package/crates/tish_resolve/src/pos.rs +133 -0
  66. package/crates/tish_runtime/Cargo.toml +68 -3
  67. package/crates/tish_runtime/src/http.rs +1123 -141
  68. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  69. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  70. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  71. package/crates/tish_runtime/src/lib.rs +159 -29
  72. package/crates/tish_runtime/src/promise.rs +199 -36
  73. package/crates/tish_runtime/src/promise_io.rs +2 -1
  74. package/crates/tish_runtime/src/timers.rs +37 -1
  75. package/crates/tish_runtime/src/ws.rs +26 -28
  76. package/crates/tish_ui/src/jsx.rs +279 -8
  77. package/crates/tish_ui/src/lib.rs +5 -2
  78. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  79. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  80. package/crates/tish_vm/Cargo.toml +15 -5
  81. package/crates/tish_vm/src/vm.rs +506 -259
  82. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  83. package/crates/tish_wasm/src/lib.rs +17 -14
  84. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  85. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  86. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  87. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  88. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  89. package/justfile +8 -0
  90. package/package.json +1 -1
  91. package/platform/darwin-arm64/tish +0 -0
  92. package/platform/darwin-x64/tish +0 -0
  93. package/platform/linux-arm64/tish +0 -0
  94. package/platform/linux-x64/tish +0 -0
  95. package/platform/win32-x64/tish.exe +0 -0
@@ -4,7 +4,9 @@ use std::cell::RefCell;
4
4
  use std::rc::Rc;
5
5
  use std::sync::Arc;
6
6
 
7
- use tishlang_core::{ObjectMap, Value as CoreValue};
7
+ use tishlang_core::{ObjectMap, Value as CoreValue, VmRef};
8
+ #[cfg(feature = "regex")]
9
+ use tishlang_core::TishRegExp;
8
10
 
9
11
  use crate::value::{PropMap, Value};
10
12
 
@@ -20,14 +22,14 @@ pub fn eval_to_core(v: &Value) -> Result<CoreValue, String> {
20
22
  for item in arr.borrow().iter() {
21
23
  out.push(eval_to_core(item)?);
22
24
  }
23
- Ok(CoreValue::Array(Rc::new(RefCell::new(out))))
25
+ Ok(CoreValue::Array(VmRef::new(out)))
24
26
  }
25
27
  Value::Object(map) => {
26
28
  let mut out = ObjectMap::default();
27
29
  for (k, v) in map.borrow().iter() {
28
30
  out.insert(Arc::clone(k), eval_to_core(v)?);
29
31
  }
30
- Ok(CoreValue::Object(Rc::new(RefCell::new(out))))
32
+ Ok(CoreValue::Object(VmRef::new(out)))
31
33
  }
32
34
  Value::Opaque(o) => Ok(CoreValue::Opaque(Arc::clone(o))),
33
35
  _ => Err(format!(
@@ -63,13 +65,19 @@ pub fn core_to_eval(v: CoreValue) -> Value {
63
65
  CoreValue::Promise(p) => Value::CorePromise(Arc::clone(&p)),
64
66
  #[cfg(not(feature = "http"))]
65
67
  CoreValue::Promise(_) => Value::Null,
66
- CoreValue::Function(f) => Value::CoreFn(Rc::clone(&f)),
68
+ // `CoreNativeFn` is feature-gated (Rc vs Arc), so use Clone::clone
69
+ // which works for either.
70
+ CoreValue::Function(f) => Value::CoreFn(f.clone()),
67
71
  // tishlang_core gets regex from http or regex features; handle RegExp when it exists
68
72
  #[cfg(any(feature = "http", feature = "regex"))]
69
73
  CoreValue::RegExp(re) => {
70
74
  #[cfg(feature = "regex")]
71
75
  {
72
- Value::RegExp(re)
76
+ // Core uses `VmRef<TishRegExp>` (potentially `Arc<Mutex>`),
77
+ // interpreter uses `Rc<RefCell<TishRegExp>>`. Clone the
78
+ // inner state across so the two storage shapes can coexist.
79
+ let inner: TishRegExp = re.borrow().clone();
80
+ Value::RegExp(Rc::new(RefCell::new(inner)))
73
81
  }
74
82
  #[cfg(not(feature = "regex"))]
75
83
  {
@@ -279,6 +279,51 @@ impl Printer {
279
279
  self.buf.push_str(" from ");
280
280
  self.string_lit(from.as_ref());
281
281
  }
282
+ Statement::TypeAlias { name, ty, .. } => {
283
+ self.indent(level);
284
+ self.buf.push_str("type ");
285
+ self.buf.push_str(name);
286
+ self.buf.push_str(" = ");
287
+ self.type_ann(ty);
288
+ }
289
+ Statement::DeclareVar {
290
+ name,
291
+ type_ann,
292
+ const_,
293
+ ..
294
+ } => {
295
+ self.indent(level);
296
+ self.buf.push_str("declare ");
297
+ self.buf.push_str(if *const_ { "const " } else { "let " });
298
+ self.buf.push_str(name);
299
+ if let Some(t) = type_ann {
300
+ self.buf.push_str(": ");
301
+ self.type_ann(t);
302
+ }
303
+ }
304
+ Statement::DeclareFun {
305
+ async_,
306
+ name,
307
+ params,
308
+ rest_param,
309
+ return_type,
310
+ ..
311
+ } => {
312
+ self.indent(level);
313
+ self.buf.push_str("declare ");
314
+ if *async_ {
315
+ self.buf.push_str("async ");
316
+ }
317
+ self.buf.push_str("fn ");
318
+ self.buf.push_str(name);
319
+ self.buf.push('(');
320
+ self.param_list(params, rest_param);
321
+ self.buf.push(')');
322
+ if let Some(rt) = return_type {
323
+ self.buf.push_str(": ");
324
+ self.type_ann(rt);
325
+ }
326
+ }
282
327
  Statement::Export { declaration, .. } => {
283
328
  self.indent(level);
284
329
  self.buf.push_str("export ");
@@ -361,12 +406,12 @@ impl Printer {
361
406
  fn import_specs(&mut self, specs: &[ImportSpecifier]) {
362
407
  if specs.len() == 1 {
363
408
  match &specs[0] {
364
- ImportSpecifier::Default(n) => self.buf.push_str(n.as_ref()),
365
- ImportSpecifier::Namespace(n) => {
409
+ ImportSpecifier::Default { name, .. } => self.buf.push_str(name.as_ref()),
410
+ ImportSpecifier::Namespace { name, .. } => {
366
411
  self.buf.push_str("* as ");
367
- self.buf.push_str(n.as_ref());
412
+ self.buf.push_str(name.as_ref());
368
413
  }
369
- ImportSpecifier::Named { name, alias } => {
414
+ ImportSpecifier::Named { name, alias, .. } => {
370
415
  self.buf.push_str("{ ");
371
416
  self.buf.push_str(name.as_ref());
372
417
  if let Some(a) = alias {
@@ -384,7 +429,7 @@ impl Printer {
384
429
  self.buf.push_str(", ");
385
430
  }
386
431
  match sp {
387
- ImportSpecifier::Named { name, alias } => {
432
+ ImportSpecifier::Named { name, alias, .. } => {
388
433
  self.buf.push_str(name.as_ref());
389
434
  if let Some(a) = alias {
390
435
  self.buf.push_str(" as ");
@@ -453,9 +498,9 @@ impl Printer {
453
498
  self.buf.push_str(", ");
454
499
  }
455
500
  match e {
456
- Some(DestructElement::Ident(n)) => self.buf.push_str(n.as_ref()),
501
+ Some(DestructElement::Ident(n, _)) => self.buf.push_str(n.as_ref()),
457
502
  Some(DestructElement::Pattern(inner)) => self.destruct_pat(inner),
458
- Some(DestructElement::Rest(n)) => {
503
+ Some(DestructElement::Rest(n, _)) => {
459
504
  self.buf.push_str("...");
460
505
  self.buf.push_str(n.as_ref());
461
506
  }
@@ -472,16 +517,16 @@ impl Printer {
472
517
  }
473
518
  self.buf.push_str(pr.key.as_ref());
474
519
  match &pr.value {
475
- DestructElement::Ident(n) if n.as_ref() != pr.key.as_ref() => {
520
+ DestructElement::Ident(n, _) if n.as_ref() != pr.key.as_ref() => {
476
521
  self.buf.push_str(": ");
477
522
  self.buf.push_str(n.as_ref());
478
523
  }
479
- DestructElement::Ident(_) => {}
524
+ DestructElement::Ident(_, _) => {}
480
525
  DestructElement::Pattern(inner) => {
481
526
  self.buf.push_str(": ");
482
527
  self.destruct_pat(inner);
483
528
  }
484
- DestructElement::Rest(n) => {
529
+ DestructElement::Rest(n, _) => {
485
530
  self.buf.push_str(": ...");
486
531
  self.buf.push_str(n.as_ref());
487
532
  }
@@ -617,7 +662,7 @@ impl Printer {
617
662
  self.buf.push('.');
618
663
  }
619
664
  match prop {
620
- MemberProp::Name(n) => self.buf.push_str(n.as_ref()),
665
+ MemberProp::Name { name, .. } => self.buf.push_str(name.as_ref()),
621
666
  MemberProp::Expr(ex) => {
622
667
  self.buf.push('[');
623
668
  self.expr(ex);
@@ -817,35 +862,72 @@ impl Printer {
817
862
  if children.is_empty() {
818
863
  self.buf.push_str(" />");
819
864
  } else {
820
- self.buf.push('>');
821
- for ch in children {
822
- match ch {
823
- JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
824
- JsxChild::Expr(e) => {
825
- self.buf.push('{');
826
- self.expr(e);
827
- self.buf.push('}');
865
+ let compact = children.len() == 1
866
+ && matches!(
867
+ &children[0],
868
+ JsxChild::Text(t) if !t.as_ref().contains('\n')
869
+ );
870
+ if compact {
871
+ self.buf.push('>');
872
+ if let JsxChild::Text(t) = &children[0] {
873
+ self.buf.push_str(t.as_ref());
874
+ }
875
+ self.buf.push_str("</");
876
+ self.buf.push_str(tag.as_ref());
877
+ self.buf.push('>');
878
+ } else {
879
+ self.buf.push('>');
880
+ self.buf.push('\n');
881
+ for ch in children {
882
+ self.buf.push_str(" ");
883
+ match ch {
884
+ JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
885
+ JsxChild::Expr(e) => {
886
+ self.buf.push('{');
887
+ self.expr(e);
888
+ self.buf.push('}');
889
+ }
828
890
  }
891
+ self.buf.push('\n');
829
892
  }
893
+ self.buf.push_str(" </");
894
+ self.buf.push_str(tag.as_ref());
895
+ self.buf.push('>');
830
896
  }
831
- self.buf.push_str("</");
832
- self.buf.push_str(tag.as_ref());
833
- self.buf.push('>');
834
897
  }
835
898
  }
836
899
  Expr::JsxFragment { children, .. } => {
837
900
  self.buf.push_str("<>");
838
- for ch in children {
839
- match ch {
840
- JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
841
- JsxChild::Expr(e) => {
842
- self.buf.push('{');
843
- self.expr(e);
844
- self.buf.push('}');
901
+ if children.is_empty() {
902
+ self.buf.push_str("</>");
903
+ } else {
904
+ let compact = children.len() == 1
905
+ && matches!(
906
+ &children[0],
907
+ JsxChild::Text(t) if !t.as_ref().contains('\n')
908
+ );
909
+ if compact {
910
+ if let JsxChild::Text(t) = &children[0] {
911
+ self.buf.push_str(t.as_ref());
912
+ }
913
+ self.buf.push_str("</>");
914
+ } else {
915
+ self.buf.push('\n');
916
+ for ch in children {
917
+ self.buf.push_str(" ");
918
+ match ch {
919
+ JsxChild::Text(t) => self.buf.push_str(t.as_ref()),
920
+ JsxChild::Expr(e) => {
921
+ self.buf.push('{');
922
+ self.expr(e);
923
+ self.buf.push('}');
924
+ }
925
+ }
926
+ self.buf.push('\n');
845
927
  }
928
+ self.buf.push_str("</>");
846
929
  }
847
930
  }
848
- self.buf.push_str("</>");
849
931
  }
850
932
  Expr::NativeModuleLoad {
851
933
  spec, export_name, ..
@@ -936,4 +1018,12 @@ mod tests {
936
1018
  let out = format_source(src).unwrap();
937
1019
  let _ = tishlang_parser::parse(&out).unwrap();
938
1020
  }
1021
+
1022
+ #[test]
1023
+ fn jsx_multiline_when_mixed_children() {
1024
+ let src = "let x = <div>a{b}</div>\n";
1025
+ let out = format_source(src).unwrap();
1026
+ assert!(out.contains('\n'), "expected line breaks in formatted JSX: {out:?}");
1027
+ let _ = tishlang_parser::parse(&out).unwrap();
1028
+ }
939
1029
  }
@@ -544,6 +544,13 @@ impl<'a> Lexer<'a> {
544
544
  if self.peek() == Some('/') {
545
545
  self.advance();
546
546
  self.skip_line_comment();
547
+ // `skip_line_comment` consumes the newline via `advance()`, which sets
548
+ // `at_line_start` before we would normally run `skip_whitespace()`. Without
549
+ // stripping the next line's leading spaces here, `read_indent_level` would see
550
+ // physical indentation and emit a spurious `Indent` (breaks e.g. object
551
+ // literals with trailing `//` comments). Newlines handled in `skip_whitespace`
552
+ // eat those spaces before the indent pass; match that behavior.
553
+ self.skip_whitespace();
547
554
  return self.next_token();
548
555
  } else if self.peek() == Some('*') {
549
556
  self.advance();
@@ -636,11 +643,8 @@ impl<'a> Lexer<'a> {
636
643
  return Ok(Some(Token {
637
644
  kind,
638
645
  span: Span { start, end },
639
- literal: if matches!(kind, TokenKind::Ident) {
640
- Some(ident.into())
641
- } else {
642
- None
643
- },
646
+ // Spelling is useful for keywords too (e.g. object keys, type names like `type`).
647
+ literal: Some(ident.into()),
644
648
  }));
645
649
  }
646
650
  '\n' => {
@@ -693,4 +697,15 @@ mod tests {
693
697
  let string_tok = tokens.iter().find(|t| t.kind == TokenKind::String).unwrap();
694
698
  assert_eq!(string_tok.literal.as_deref(), Some("H"));
695
699
  }
700
+
701
+ #[test]
702
+ fn line_comment_does_not_emit_spurious_indent_before_next_line() {
703
+ let with_comment = "fn f() {\n return {\n a: 1, // c\n b: 2\n }\n}\n";
704
+ let tokens: Vec<_> = Lexer::new(with_comment).collect::<Result<Vec<_>, _>>().unwrap();
705
+ assert!(
706
+ !tokens.iter().any(|t| t.kind == TokenKind::Indent),
707
+ "unexpected Indent after line comment: {:?}",
708
+ tokens.iter().map(|t| format!("{:?}", t.kind)).collect::<Vec<_>>()
709
+ );
710
+ }
696
711
  }
@@ -57,6 +57,8 @@ pub enum TokenKind {
57
57
  New,
58
58
  Import,
59
59
  Export,
60
+ Type,
61
+ Declare,
60
62
 
61
63
  // Punctuation
62
64
  LParen,
@@ -153,6 +155,8 @@ impl TokenKind {
153
155
  "new" => TokenKind::New,
154
156
  "import" => TokenKind::Import,
155
157
  "export" => TokenKind::Export,
158
+ "type" => TokenKind::Type,
159
+ "declare" => TokenKind::Declare,
156
160
  _ => TokenKind::Ident,
157
161
  }
158
162
  }
@@ -23,7 +23,9 @@ pub fn compile_to_native(
23
23
  detect_cycles(&modules).map_err(|e| LlvmError {
24
24
  message: e.to_string(),
25
25
  })?;
26
- let program = merge_modules(modules).map_err(|e| LlvmError {
26
+ let program = merge_modules(modules)
27
+ .map(|m| m.program)
28
+ .map_err(|e| LlvmError {
27
29
  message: e.to_string(),
28
30
  })?;
29
31
  let chunk = tishlang_bytecode::compile(&program).map_err(|e| LlvmError {
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang_lsp"
3
- version = "0.1.0"
3
+ version = "0.1.1"
4
4
  edition = "2021"
5
5
  description = "Language Server Protocol implementation for Tish"
6
6
  license-file = { workspace = true }
@@ -12,9 +12,12 @@ path = "src/main.rs"
12
12
 
13
13
  [dependencies]
14
14
  tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
15
+ tishlang_cargo_bindgen = { path = "../tishlang_cargo_bindgen", version = ">=0.1" }
16
+ tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
15
17
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
16
18
  tishlang_fmt = { path = "../tish_fmt", version = ">=0.1" }
17
19
  tishlang_lint = { path = "../tish_lint", version = ">=0.1" }
20
+ tishlang_resolve = { path = "../tish_resolve", version = ">=0.1" }
18
21
  tower-lsp = "0.20"
19
22
  tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std"] }
20
23
  serde_json = "1"
@@ -14,7 +14,7 @@ Binary: `target/release/tish-lsp` (stdio LSP).
14
14
 
15
15
  - Parse diagnostics + lint warnings (via `tish_lint` **library** — use **`tish-lint`** CLI separately in CI)
16
16
  - Document symbols, completion, formatting (via `tish_fmt` **library** — use **`tish-fmt`** CLI separately in CI)
17
- - Go to definition (same file + relative imports)
17
+ - Go to definition (same file, relative `./` / `../`, bare `node_modules` packages like Node, and native `tish:` / `@scope/pkg` / `cargo:` → Rust `pub fn` via `syn` + `cargo metadata` where configured)
18
18
  - Workspace symbol search (`**/*.tish`)
19
19
 
20
20
  ## Client configuration
@@ -0,0 +1,261 @@
1
+ //! Built-in and JSX-intrinsic "go to definition" for the Tish LSP.
2
+ //!
3
+ //! Global / namespace member anchors are loaded from `stdlib/builtins.d.tish` in the `tish`
4
+ //! repository via `// @tish-source <symbol> <rel-path> <1-based-line>` pragmas. The type
5
+ //! surface in that file is the canonical declaration for ambient builtins.
6
+ //!
7
+ //! HTML / SVG intrinsic tag names are still listed here (sorted) for fast lookup; each maps
8
+ //! to the same vnode factory as in the pragma table for `div`.
9
+ //!
10
+ //! Definitions resolve to `file://` URIs only when `TISHLANG_SOURCE_ROOT` is set or the client
11
+ //! passes `tishlangSourceRoot` in LSP `initializationOptions` (VS Code: `tish.tishlangSourceRoot`).
12
+
13
+ use std::collections::HashMap;
14
+ use std::path::Path;
15
+ use std::sync::OnceLock;
16
+
17
+ use regex::Regex;
18
+ use tower_lsp::lsp_types::{Location, Position, Range, Url};
19
+
20
+ /// Stable path relative to the `tish` repository root (where the workspace `Cargo.toml` lives).
21
+ #[derive(Clone, Debug, PartialEq, Eq)]
22
+ pub struct BuiltinDef {
23
+ pub rel_path: String,
24
+ /// 0-based LSP line in the target file.
25
+ pub line: u32,
26
+ /// 0-based UTF-16 code unit offset on that line (ASCII-only targets use byte column).
27
+ pub character: u32,
28
+ }
29
+
30
+ fn is_ident_char(c: char) -> bool {
31
+ c.is_alphanumeric() || c == '_'
32
+ }
33
+
34
+ /// HTML / SVG intrinsic tag names (lowercase) that JSX lowers to `h("tag", …)`; must stay sorted for `binary_search`.
35
+ const HTML_INTRINSIC_TAGS: &[&str] = &[
36
+ "a", "abbr", "address", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo",
37
+ "blockquote", "body", "br", "button", "canvas", "caption", "cite", "code", "col", "colgroup",
38
+ "data", "datalist", "dd", "del", "details", "dfn", "dialog", "div", "dl", "dt", "em", "embed",
39
+ "fieldset", "figcaption", "figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6",
40
+ "head", "header", "hr", "html", "i", "iframe", "img", "input", "ins", "kbd", "label", "legend",
41
+ "li", "link", "main", "map", "mark", "meta", "meter", "nav", "noscript", "object", "ol",
42
+ "optgroup", "option", "output", "p", "param", "picture", "pre", "progress", "q", "rp", "rt",
43
+ "ruby", "s", "samp", "script", "section", "select", "slot", "small", "source", "span", "strong",
44
+ "style", "sub", "summary", "sup", "svg", "table", "tbody", "td", "template", "textarea",
45
+ "tfoot", "th", "thead", "time", "title", "tr", "track", "u", "ul", "var", "video", "wbr",
46
+ ];
47
+
48
+ static SOURCE_MAP: OnceLock<HashMap<String, BuiltinDef>> = OnceLock::new();
49
+
50
+ /// Parse `// @tish-source <symbol> <rel-path> <1-based-line>` lines (same format as `stdlib/builtins.d.tish`).
51
+ pub fn parse_tish_source_pragmas(src: &str) -> HashMap<String, BuiltinDef> {
52
+ let re = Regex::new(r"(?m)^\s*//\s*@tish-source\s+(\S+)\s+(\S+)\s+(\d+)\s*$")
53
+ .expect("builtin pragma regex");
54
+ let mut m = HashMap::new();
55
+ for cap in re.captures_iter(src) {
56
+ let sym = cap[1].to_string();
57
+ let rel = cap[2].to_string();
58
+ let line_1: u32 = cap[3].parse().unwrap_or(1);
59
+ m.insert(
60
+ sym,
61
+ BuiltinDef {
62
+ rel_path: rel,
63
+ line: line_1.saturating_sub(1),
64
+ character: 0,
65
+ },
66
+ );
67
+ }
68
+ m
69
+ }
70
+
71
+ fn source_map() -> &'static HashMap<String, BuiltinDef> {
72
+ SOURCE_MAP.get_or_init(|| {
73
+ let src = include_str!(concat!(
74
+ env!("CARGO_MANIFEST_DIR"),
75
+ "/../../stdlib/builtins.d.tish"
76
+ ));
77
+ parse_tish_source_pragmas(src)
78
+ })
79
+ }
80
+
81
+ /// If `col` lies on the name of a JSX opening (or closing) tag on this line, returns the span of that name in **character indices** (same convention as [`tower_lsp::lsp_types::Position::character`] for ASCII lines).
82
+ fn jsx_tag_name_char_span(line: &str, col: usize) -> Option<(usize, usize)> {
83
+ let chars: Vec<char> = line.chars().collect();
84
+ if chars.is_empty() {
85
+ return None;
86
+ }
87
+ let col = col.min(chars.len().saturating_sub(1));
88
+ let mut j = col;
89
+ while j > 0 {
90
+ j -= 1;
91
+ if chars[j] == '>' {
92
+ return None;
93
+ }
94
+ if chars[j] == '<' {
95
+ let mut k = j + 1;
96
+ while k < chars.len() && chars[k].is_whitespace() {
97
+ k += 1;
98
+ }
99
+ if k < chars.len() && chars[k] == '/' {
100
+ k += 1;
101
+ while k < chars.len() && chars[k].is_whitespace() {
102
+ k += 1;
103
+ }
104
+ }
105
+ let name_start = k;
106
+ while k < chars.len() {
107
+ let c = chars[k];
108
+ if c.is_whitespace() || c == '>' || c == '/' || c == '{' {
109
+ break;
110
+ }
111
+ if !(c.is_alphanumeric() || c == '_' || c == '-') {
112
+ break;
113
+ }
114
+ k += 1;
115
+ }
116
+ let name_end = k;
117
+ if name_start < name_end && col >= name_start && col < name_end {
118
+ return Some((name_start, name_end));
119
+ }
120
+ return None;
121
+ }
122
+ }
123
+ None
124
+ }
125
+
126
+ fn tag_name_at_span(line: &str, start: usize, end: usize) -> String {
127
+ line.chars()
128
+ .enumerate()
129
+ .filter(|(i, _)| *i >= start && *i < end)
130
+ .map(|(_, c)| c)
131
+ .collect()
132
+ }
133
+
134
+ /// `base.member` when the cursor is on `member` (same line, character-index column as `word_at_position`).
135
+ fn split_property_access(line: &str, col: usize) -> Option<(String, String)> {
136
+ let chars: Vec<char> = line.chars().collect();
137
+ if chars.is_empty() {
138
+ return None;
139
+ }
140
+ let col = col.min(chars.len().saturating_sub(1));
141
+ if !is_ident_char(chars[col]) {
142
+ return None;
143
+ }
144
+ let mut end = col;
145
+ while end + 1 < chars.len() && is_ident_char(chars[end + 1]) {
146
+ end += 1;
147
+ }
148
+ let mut start = col;
149
+ while start > 0 && is_ident_char(chars[start - 1]) {
150
+ start -= 1;
151
+ }
152
+ if start == 0 {
153
+ return None;
154
+ }
155
+ if chars[start - 1] != '.' {
156
+ return None;
157
+ }
158
+ let member: String = chars[start..=end].iter().collect();
159
+ let mut k = start - 2;
160
+ while k > 0 && is_ident_char(chars[k - 1]) {
161
+ k -= 1;
162
+ }
163
+ let base: String = chars[k..start - 1].iter().collect();
164
+ if base.is_empty() {
165
+ return None;
166
+ }
167
+ Some((base, member))
168
+ }
169
+
170
+ fn lookup_dotted(base: &str, member: &str) -> Option<BuiltinDef> {
171
+ let key = format!("{base}.{member}");
172
+ source_map().get(&key).cloned()
173
+ }
174
+
175
+ fn lookup_global(word: &str) -> Option<BuiltinDef> {
176
+ source_map().get(word).cloned()
177
+ }
178
+
179
+ /// Built-in or JSX-intrinsic definition for the identifier at `position`, if any.
180
+ pub fn definition_for_builtin(
181
+ text: &str,
182
+ line: u32,
183
+ character: u32,
184
+ word: &str,
185
+ ) -> Option<BuiltinDef> {
186
+ let line_str = text.lines().nth(line as usize)?;
187
+ let col = character as usize;
188
+
189
+ if let Some((ns, ne)) = jsx_tag_name_char_span(line_str, col) {
190
+ let tag = tag_name_at_span(line_str, ns, ne);
191
+ if tag == word {
192
+ if word == "Fragment" {
193
+ return lookup_global("Fragment");
194
+ }
195
+ if HTML_INTRINSIC_TAGS.binary_search(&word).is_ok() {
196
+ // Intrinsic tags share the same `ui_h` entry point as `div`.
197
+ return lookup_global("div");
198
+ }
199
+ }
200
+ }
201
+
202
+ if let Some((base, member)) = split_property_access(line_str, col) {
203
+ if let Some(d) = lookup_dotted(base.as_str(), member.as_str()) {
204
+ return Some(d);
205
+ }
206
+ }
207
+
208
+ lookup_global(word)
209
+ }
210
+
211
+ pub fn to_file_location(root: &Path, def: &BuiltinDef) -> Option<Location> {
212
+ let path = root.join(&def.rel_path);
213
+ if !path.is_file() {
214
+ return None;
215
+ }
216
+ let uri = Url::from_file_path(&path).ok()?;
217
+ let p = Position {
218
+ line: def.line,
219
+ character: def.character,
220
+ };
221
+ Some(Location {
222
+ uri,
223
+ range: Range { start: p, end: p },
224
+ })
225
+ }
226
+
227
+ #[cfg(test)]
228
+ mod tests {
229
+ use super::*;
230
+
231
+ #[test]
232
+ fn pragma_map_loads_console_log() {
233
+ let d = lookup_global("console").expect("console");
234
+ assert!(d.rel_path.contains("eval.rs"));
235
+ let m = lookup_dotted("console", "log").expect("console.log");
236
+ assert!(m.rel_path.contains("natives.rs"));
237
+ }
238
+
239
+ #[test]
240
+ fn jsx_div_maps_to_ui_h() {
241
+ let text = "let x = <div />";
242
+ let line = 0u32;
243
+ let defn = definition_for_builtin(text, line, 9, "div").expect("div builtin");
244
+ assert!(defn.rel_path.contains("runtime"));
245
+ assert_eq!(defn.line, 40);
246
+ }
247
+
248
+ #[test]
249
+ fn set_timeout_global() {
250
+ let defn =
251
+ definition_for_builtin("setTimeout(0, fn() {})\n", 0, 0, "setTimeout").expect("timer");
252
+ assert_eq!(defn.rel_path, "crates/tish_eval/src/timers.rs");
253
+ }
254
+
255
+ #[test]
256
+ fn console_log_qualified() {
257
+ let text = "console.log(1)";
258
+ let defn = definition_for_builtin(text, 0, 10, "log").expect("console.log");
259
+ assert_eq!(defn.rel_path, "crates/tish_eval/src/natives.rs");
260
+ }
261
+ }