@tishlang/tish-format 1.0.13 → 2.0.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.
Files changed (108) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +10 -2
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/Cargo.toml +1 -1
  66. package/crates/tish_fmt/src/lib.rs +61 -5
  67. package/crates/tish_lexer/src/lib.rs +397 -9
  68. package/crates/tish_lexer/src/token.rs +7 -0
  69. package/crates/tish_lint/src/lib.rs +2 -10
  70. package/crates/tish_lsp/src/import_goto.rs +2 -0
  71. package/crates/tish_lsp/src/main.rs +439 -26
  72. package/crates/tish_native/src/build.rs +55 -1
  73. package/crates/tish_opt/src/lib.rs +126 -23
  74. package/crates/tish_parser/src/lib.rs +55 -1
  75. package/crates/tish_parser/src/parser.rs +456 -34
  76. package/crates/tish_pg/src/lib.rs +3 -3
  77. package/crates/tish_resolve/src/lib.rs +99 -59
  78. package/crates/tish_runtime/Cargo.toml +4 -0
  79. package/crates/tish_runtime/src/http.rs +66 -17
  80. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  81. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  82. package/crates/tish_runtime/src/lib.rs +299 -44
  83. package/crates/tish_runtime/src/promise.rs +328 -18
  84. package/crates/tish_runtime/src/timers.rs +13 -7
  85. package/crates/tish_runtime/src/tty.rs +226 -0
  86. package/crates/tish_runtime/src/ws.rs +35 -18
  87. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  88. package/crates/tish_ui/src/jsx.rs +10 -0
  89. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  90. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  91. package/crates/tish_vm/Cargo.toml +14 -1
  92. package/crates/tish_vm/src/jit.rs +1050 -0
  93. package/crates/tish_vm/src/lib.rs +2 -0
  94. package/crates/tish_vm/src/vm.rs +1546 -202
  95. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  96. package/crates/tish_wasm/src/lib.rs +6 -2
  97. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  98. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  99. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  100. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  101. package/justfile +8 -0
  102. package/package.json +2 -2
  103. package/platform/darwin-arm64/tish-fmt +0 -0
  104. package/platform/darwin-x64/tish-fmt +0 -0
  105. package/platform/linux-arm64/tish-fmt +0 -0
  106. package/platform/linux-x64/tish-fmt +0 -0
  107. package/platform/win32-x64/tish-fmt.exe +0 -0
  108. package/README.md +0 -138
@@ -109,17 +109,9 @@ fn lint_stmt(s: &Statement, out: &mut Vec<LintDiagnostic>) {
109
109
  }
110
110
  }
111
111
  Statement::ExprStmt { expr, .. } => lint_expr(expr, out),
112
- Statement::VarDecl { init, .. } => {
113
- if let Some(e) = init {
114
- lint_expr(e, out);
115
- }
116
- }
112
+ Statement::VarDecl { init: Some(e), .. } => lint_expr(e, out),
117
113
  Statement::VarDeclDestructure { init, .. } => lint_expr(init, out),
118
- Statement::Return { value, .. } => {
119
- if let Some(e) = value {
120
- lint_expr(e, out);
121
- }
122
- }
114
+ Statement::Return { value: Some(e), .. } => lint_expr(e, out),
123
115
  Statement::Throw { value, .. } => lint_expr(value, out),
124
116
  _ => {}
125
117
  }
@@ -304,6 +304,7 @@ pub struct NativeMemberDefinition {
304
304
 
305
305
  /// Static member chain `root.a.b` where `root` is an import: resolve the leaf to a Rust `pub fn`,
306
306
  /// else to `lsp-pragmas.d.tish` in the native package (e.g. `tish-macos`).
307
+ #[allow(clippy::too_many_arguments)] // LSP request context (program/file/text/roots/cache/position/word)
307
308
  pub fn native_member_definition(
308
309
  program: &Program,
309
310
  file_path: &Path,
@@ -411,6 +412,7 @@ pub fn native_member_definition(
411
412
 
412
413
  /// Static member chain `root.a.b` where `root` is an import: resolve the leaf name to a Rust `pub fn`
413
414
  /// (native / `cargo:`) or a single-level export in a relative `.tish` module.
415
+ #[allow(clippy::too_many_arguments)] // LSP request context (program/file/text/roots/cache/position/word)
414
416
  pub fn definition_for_native_receiver_member(
415
417
  program: &Program,
416
418
  file_path: &Path,
@@ -121,7 +121,7 @@ fn document_symbol(
121
121
  }
122
122
  }
123
123
 
124
- fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
124
+ async fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
125
125
  let mut diags = Vec::new();
126
126
  match tishlang_parser::parse(text) {
127
127
  Ok(program) => {
@@ -172,6 +172,17 @@ fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
172
172
  ..Default::default()
173
173
  });
174
174
  }
175
+ // Gradual type checker (Phase 2): surface provable annotation violations as warnings.
176
+ for d in tishlang_compile::check_program(&program) {
177
+ diags.push(Diagnostic {
178
+ range: span_to_range(&d.span, text),
179
+ severity: Some(DiagnosticSeverity::WARNING),
180
+ code: Some(NumberOrString::String("tish-type".into())),
181
+ message: d.message,
182
+ source: Some("tish".into()),
183
+ ..Default::default()
184
+ });
185
+ }
175
186
  }
176
187
  Err(e) => {
177
188
  let (l, c) = parse_error_pos(&e);
@@ -183,7 +194,9 @@ fn publish_parse_and_lint(client: &Client, uri: Url, text: &str) {
183
194
  });
184
195
  }
185
196
  }
186
- let _ = client.publish_diagnostics(uri, diags, None);
197
+ // MUST be awaited — `publish_diagnostics` is async; a bare `let _ = …` drops the future
198
+ // unsent, which silently disables ALL LSP diagnostics (parse errors, lints, unused bindings).
199
+ client.publish_diagnostics(uri, diags, None).await;
187
200
  }
188
201
 
189
202
  #[tower_lsp::async_trait]
@@ -268,7 +281,7 @@ impl LanguageServer for Backend {
268
281
  let uri = p.text_document.uri;
269
282
  let text = p.text_document.text;
270
283
  self.docs.write().unwrap().insert(uri.clone(), text.clone());
271
- publish_parse_and_lint(&self.client, uri, &text);
284
+ publish_parse_and_lint(&self.client, uri, &text).await;
272
285
  }
273
286
 
274
287
  async fn did_change(&self, p: DidChangeTextDocumentParams) {
@@ -278,15 +291,15 @@ impl LanguageServer for Backend {
278
291
  .write()
279
292
  .unwrap()
280
293
  .insert(uri.clone(), chg.text.clone());
281
- publish_parse_and_lint(&self.client, uri, &chg.text);
294
+ publish_parse_and_lint(&self.client, uri, &chg.text).await;
282
295
  }
283
296
  }
284
297
 
285
298
  async fn did_close(&self, p: DidCloseTextDocumentParams) {
286
299
  self.docs.write().unwrap().remove(&p.text_document.uri);
287
- let _ = self
288
- .client
289
- .publish_diagnostics(p.text_document.uri, vec![], None);
300
+ self.client
301
+ .publish_diagnostics(p.text_document.uri, vec![], None)
302
+ .await;
290
303
  }
291
304
 
292
305
  async fn completion(&self, params: CompletionParams) -> Result<Option<CompletionResponse>> {
@@ -399,7 +412,17 @@ impl LanguageServer for Backend {
399
412
  return Ok(None);
400
413
  }
401
414
 
402
- if let Some(ref file_path) = uri.to_file_path().ok() {
415
+ // Type reference (`: SomeType`, `extends SomeType`, `as SomeType`) → jump to its
416
+ // `type`/`interface` declaration. Value bindings are resolved above, so this only
417
+ // fires for genuine type names.
418
+ if let Some(sp) = type_decl_span(&program, word.as_str()) {
419
+ return Ok(Some(GotoDefinitionResponse::Scalar(Location {
420
+ uri: uri.clone(),
421
+ range: span_to_range(&sp, &text),
422
+ })));
423
+ }
424
+
425
+ if let Ok(ref file_path) = uri.to_file_path() {
403
426
  let roots = self.roots.read().unwrap().clone();
404
427
  if let Some(loc) = import_goto::definition_for_import(
405
428
  &program,
@@ -456,10 +479,29 @@ impl LanguageServer for Backend {
456
479
  let Some(use_site) =
457
480
  tishlang_resolve::name_at_cursor(&program, &text, pos.line, pos.character)
458
481
  else {
482
+ // Not a value name at the cursor — it may be a type reference (`: SomeType`,
483
+ // `extends SomeType`). Type annotations carry no spans, so match by word.
484
+ let word = word_at_position(&text, pos);
485
+ if let Some(ty) = type_alias_body(&program, &word) {
486
+ let value = format!("**`{}`**{}", word, code_hint(&format!("type {} = {}", word, ty)));
487
+ return Ok(Some(Hover {
488
+ range: None,
489
+ contents: HoverContents::Markup(MarkupContent {
490
+ kind: MarkupKind::Markdown,
491
+ value,
492
+ }),
493
+ }));
494
+ }
459
495
  return Ok(None);
460
496
  };
461
497
  let def = tishlang_resolve::definition_span(&program, &text, pos.line, pos.character);
462
498
  let mut md = format!("**`{}`**", use_site.name);
499
+ // Type-aware hover: show the declared (or simply-inferred) type / fn signature.
500
+ if let Some(ref dspan) = def {
501
+ if let Some(hint) = type_hint_at_def(&program, dspan) {
502
+ md.push_str(&hint);
503
+ }
504
+ }
463
505
  match def {
464
506
  Some(def) if def.start == use_site.span.start && def.end == use_site.span.end => {
465
507
  md.push_str("\n\n_(binding site)_");
@@ -789,13 +831,10 @@ pub(crate) fn find_export(
789
831
  range: span_to_range(name_span, text),
790
832
  });
791
833
  }
792
- tishlang_ast::Statement::Export { declaration, .. } => match declaration.as_ref() {
793
- tishlang_ast::ExportDeclaration::Named(inner) => {
794
- if let Some(loc) = find_decl_in_stmt(inner, name, uri, text) {
795
- return Some(loc);
796
- }
834
+ tishlang_ast::Statement::Export { declaration, .. } => if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
835
+ if let Some(loc) = find_decl_in_stmt(inner, name, uri, text) {
836
+ return Some(loc);
797
837
  }
798
- _ => {}
799
838
  },
800
839
  _ => {}
801
840
  }
@@ -856,27 +895,263 @@ fn span_to_range(span: &tishlang_ast::Span, text: &str) -> Range {
856
895
 
857
896
  fn word_at_position(text: &str, position: Position) -> String {
858
897
  let line = text.lines().nth(position.line as usize).unwrap_or("");
859
- let col = position.character as usize;
860
- let bytes: Vec<(usize, char)> = line.char_indices().collect();
861
- let mut start = col.min(bytes.len().saturating_sub(1));
862
- while start > 0 && !is_ident_char(bytes.get(start).map(|(_, c)| *c).unwrap_or(' ')) {
863
- start = start.saturating_sub(1);
898
+ let chars: Vec<(usize, char)> = line.char_indices().collect();
899
+ let col = (position.character as usize).min(chars.len());
900
+ // Pick the identifier the cursor is on. If the cursor sits just past a word's end
901
+ // (on whitespace/punct or EOL), fall back to the identifier immediately to its left.
902
+ let mut start = col;
903
+ if start >= chars.len() || !is_ident_char(chars[start].1) {
904
+ if start == 0 || !is_ident_char(chars[start - 1].1) {
905
+ return String::new();
906
+ }
907
+ start -= 1;
864
908
  }
865
- let mut i = start;
866
- while i < bytes.len() && is_ident_char(bytes[i].1) {
867
- i += 1;
909
+ // Scan left to the word start, then right to the word end (the original missed the prefix
910
+ // when the cursor landed in the middle of a word).
911
+ while start > 0 && is_ident_char(chars[start - 1].1) {
912
+ start -= 1;
868
913
  }
869
- if start < bytes.len() {
870
- line[bytes[start].0..bytes.get(i).map(|(p, _)| *p).unwrap_or(line.len())].to_string()
871
- } else {
872
- String::new()
914
+ let mut end = start;
915
+ while end < chars.len() && is_ident_char(chars[end].1) {
916
+ end += 1;
873
917
  }
918
+ let s = chars[start].0;
919
+ let e = chars.get(end).map(|(p, _)| *p).unwrap_or(line.len());
920
+ line[s..e].to_string()
874
921
  }
875
922
 
876
923
  fn is_ident_char(c: char) -> bool {
877
924
  c.is_alphanumeric() || c == '_'
878
925
  }
879
926
 
927
+ // ── Type-aware hover ─────────────────────────────────────────────────────────
928
+
929
+ /// Render a `TypeAnnotation` to a readable, TypeScript-ish string for hover.
930
+ fn render_type(t: &tishlang_ast::TypeAnnotation) -> String {
931
+ use tishlang_ast::{TypeAnnotation as T, TypeLiteral as L};
932
+ match t {
933
+ T::Simple(s) => s.to_string(),
934
+ T::Array(inner) => {
935
+ // Parenthesize composite element types so `(A | B)[]` reads unambiguously.
936
+ if matches!(
937
+ inner.as_ref(),
938
+ T::Union(_) | T::Intersection(_) | T::Function { .. }
939
+ ) {
940
+ format!("({})[]", render_type(inner))
941
+ } else {
942
+ format!("{}[]", render_type(inner))
943
+ }
944
+ }
945
+ T::Object(fields) => format!(
946
+ "{{ {} }}",
947
+ fields
948
+ .iter()
949
+ .map(|(k, v)| format!("{}: {}", k, render_type(v)))
950
+ .collect::<Vec<_>>()
951
+ .join(", ")
952
+ ),
953
+ T::Function { params, returns } => format!(
954
+ "({}) => {}",
955
+ params.iter().map(render_type).collect::<Vec<_>>().join(", "),
956
+ render_type(returns)
957
+ ),
958
+ T::Union(ts) => ts.iter().map(render_type).collect::<Vec<_>>().join(" | "),
959
+ T::Tuple(ts) => format!(
960
+ "[{}]",
961
+ ts.iter().map(render_type).collect::<Vec<_>>().join(", ")
962
+ ),
963
+ T::Intersection(ts) => ts.iter().map(render_type).collect::<Vec<_>>().join(" & "),
964
+ T::Literal(L::Str(s)) => format!("\"{}\"", s),
965
+ T::Literal(L::Num(n)) => {
966
+ if n.fract() == 0.0 && n.is_finite() {
967
+ format!("{}", *n as i64)
968
+ } else {
969
+ n.to_string()
970
+ }
971
+ }
972
+ T::Literal(L::Bool(b)) => b.to_string(),
973
+ }
974
+ }
975
+
976
+ /// Best-effort type of a simple initializer (literals only). Anything non-trivial returns `None`,
977
+ /// so hover omits the type rather than guessing wrong.
978
+ fn shallow_expr_type(e: &tishlang_ast::Expr) -> Option<tishlang_ast::TypeAnnotation> {
979
+ use tishlang_ast::{Expr, Literal, TypeAnnotation as T};
980
+ if let Expr::Literal { value, .. } = e {
981
+ let name = match value {
982
+ Literal::Number(_) => "number",
983
+ Literal::String(_) => "string",
984
+ Literal::Bool(_) => "boolean",
985
+ Literal::Null => "null",
986
+ };
987
+ Some(T::Simple(Arc::from(name)))
988
+ } else {
989
+ None
990
+ }
991
+ }
992
+
993
+ /// Render a function parameter as `name: T` (or just `name` when unannotated).
994
+ fn render_param(p: &tishlang_ast::FunParam) -> String {
995
+ use tishlang_ast::FunParam;
996
+ match p {
997
+ FunParam::Simple(tp) => match &tp.type_ann {
998
+ Some(t) => format!("{}: {}", tp.name, render_type(t)),
999
+ None => tp.name.to_string(),
1000
+ },
1001
+ FunParam::Destructure { type_ann, .. } => match type_ann {
1002
+ Some(t) => format!("{{…}}: {}", render_type(t)),
1003
+ None => "{…}".to_string(),
1004
+ },
1005
+ }
1006
+ }
1007
+
1008
+ /// `fn name(params): R` signature line for a function declaration.
1009
+ fn fn_signature(
1010
+ name: &str,
1011
+ params: &[tishlang_ast::FunParam],
1012
+ rest: &Option<tishlang_ast::TypedParam>,
1013
+ ret: &Option<tishlang_ast::TypeAnnotation>,
1014
+ ) -> String {
1015
+ let mut ps: Vec<String> = params.iter().map(render_param).collect();
1016
+ if let Some(r) = rest {
1017
+ let t = r
1018
+ .type_ann
1019
+ .as_ref()
1020
+ .map(|t| format!(": {}", render_type(t)))
1021
+ .unwrap_or_default();
1022
+ ps.push(format!("...{}{}", r.name, t));
1023
+ }
1024
+ let ret_s = ret
1025
+ .as_ref()
1026
+ .map(render_type)
1027
+ .unwrap_or_else(|| "void".to_string());
1028
+ format!("fn {}({}): {}", name, ps.join(", "), ret_s)
1029
+ }
1030
+
1031
+ /// Definition spans are name spans; match on the start position.
1032
+ fn same_start(a: &tishlang_ast::Span, b: &tishlang_ast::Span) -> bool {
1033
+ a.start == b.start
1034
+ }
1035
+
1036
+ /// Wrap a one-line type hint in a tish code fence for hover.
1037
+ fn code_hint(line: &str) -> String {
1038
+ format!("\n\n```tish\n{}\n```", line)
1039
+ }
1040
+
1041
+ /// Find the declaration whose name is at `def` and produce a hover type line (markdown), if any.
1042
+ fn type_hint_at_def(program: &tishlang_ast::Program, def: &tishlang_ast::Span) -> Option<String> {
1043
+ program.statements.iter().find_map(|s| hint_in_stmt(s, def))
1044
+ }
1045
+
1046
+ /// `name_span` of a `type`/`interface` declaration named `name` (both parse to `TypeAlias`).
1047
+ /// Used so cmd+click on a `: SomeType` reference jumps to its declaration. Type annotations
1048
+ /// carry no spans, so this is a name match — sound because value bindings resolve first.
1049
+ fn type_decl_span(program: &tishlang_ast::Program, name: &str) -> Option<tishlang_ast::Span> {
1050
+ program.statements.iter().find_map(|s| match s {
1051
+ tishlang_ast::Statement::TypeAlias {
1052
+ name: n, name_span, ..
1053
+ } if n.as_ref() == name => Some(*name_span),
1054
+ _ => None,
1055
+ })
1056
+ }
1057
+
1058
+ /// Rendered body of a `type`/`interface` declaration named `name`, for hover.
1059
+ fn type_alias_body(program: &tishlang_ast::Program, name: &str) -> Option<String> {
1060
+ program.statements.iter().find_map(|s| match s {
1061
+ tishlang_ast::Statement::TypeAlias { name: n, ty, .. } if n.as_ref() == name => {
1062
+ Some(render_type(ty))
1063
+ }
1064
+ _ => None,
1065
+ })
1066
+ }
1067
+
1068
+ fn hint_in_stmt(s: &tishlang_ast::Statement, def: &tishlang_ast::Span) -> Option<String> {
1069
+ use tishlang_ast::{FunParam, Statement as St};
1070
+ match s {
1071
+ St::VarDecl {
1072
+ name,
1073
+ name_span,
1074
+ mutable,
1075
+ type_ann,
1076
+ init,
1077
+ ..
1078
+ } => {
1079
+ if same_start(name_span, def) {
1080
+ let ty = type_ann
1081
+ .clone()
1082
+ .or_else(|| init.as_ref().and_then(shallow_expr_type))?;
1083
+ let kw = if *mutable { "let" } else { "const" };
1084
+ return Some(code_hint(&format!(
1085
+ "{} {}: {}",
1086
+ kw,
1087
+ name,
1088
+ render_type(&ty)
1089
+ )));
1090
+ }
1091
+ None
1092
+ }
1093
+ St::FunDecl {
1094
+ name,
1095
+ name_span,
1096
+ params,
1097
+ rest_param,
1098
+ return_type,
1099
+ body,
1100
+ ..
1101
+ } => {
1102
+ if same_start(name_span, def) {
1103
+ return Some(code_hint(&fn_signature(
1104
+ name,
1105
+ params,
1106
+ rest_param,
1107
+ return_type,
1108
+ )));
1109
+ }
1110
+ for p in params {
1111
+ if let FunParam::Simple(tp) = p {
1112
+ if same_start(&tp.name_span, def) {
1113
+ let ty = tp
1114
+ .type_ann
1115
+ .as_ref()
1116
+ .map(render_type)
1117
+ .unwrap_or_else(|| "any".to_string());
1118
+ return Some(code_hint(&format!("(parameter) {}: {}", tp.name, ty)));
1119
+ }
1120
+ }
1121
+ }
1122
+ if let Some(r) = rest_param {
1123
+ if same_start(&r.name_span, def) {
1124
+ let ty = r
1125
+ .type_ann
1126
+ .as_ref()
1127
+ .map(render_type)
1128
+ .unwrap_or_else(|| "any[]".to_string());
1129
+ return Some(code_hint(&format!("(parameter) ...{}: {}", r.name, ty)));
1130
+ }
1131
+ }
1132
+ hint_in_stmt(body, def)
1133
+ }
1134
+ St::Block { statements, .. } | St::Multi { statements, .. } => {
1135
+ statements.iter().find_map(|s| hint_in_stmt(s, def))
1136
+ }
1137
+ St::If {
1138
+ then_branch,
1139
+ else_branch,
1140
+ ..
1141
+ } => hint_in_stmt(then_branch, def)
1142
+ .or_else(|| else_branch.as_ref().and_then(|e| hint_in_stmt(e, def))),
1143
+ St::For { init, body, .. } => init
1144
+ .as_ref()
1145
+ .and_then(|i| hint_in_stmt(i, def))
1146
+ .or_else(|| hint_in_stmt(body, def)),
1147
+ St::While { body, .. } | St::DoWhile { body, .. } | St::ForOf { body, .. } => {
1148
+ hint_in_stmt(body, def)
1149
+ }
1150
+ St::Try { body, .. } => hint_in_stmt(body, def),
1151
+ _ => None,
1152
+ }
1153
+ }
1154
+
880
1155
  fn value_completion_kind(program: &tishlang_ast::Program, name: &str) -> CompletionItemKind {
881
1156
  for s in &program.statements {
882
1157
  if let Some(k) = value_completion_kind_stmt(s, name) {
@@ -1044,3 +1319,141 @@ fn collect_child_syms(
1044
1319
  _ => doc_symbol_stmt(s, text, out),
1045
1320
  }
1046
1321
  }
1322
+
1323
+ #[cfg(test)]
1324
+ mod hover_tests {
1325
+ use super::*;
1326
+ use tishlang_ast::{FunParam, Span, Statement};
1327
+
1328
+ fn parse(src: &str) -> tishlang_ast::Program {
1329
+ tishlang_parser::parse(src).expect("parse")
1330
+ }
1331
+
1332
+ /// name_span of the first VarDecl/FunDecl named `name`, searched recursively.
1333
+ fn decl_span(s: &Statement, name: &str) -> Option<Span> {
1334
+ match s {
1335
+ Statement::VarDecl { name: n, name_span, .. } if n.as_ref() == name => Some(*name_span),
1336
+ Statement::FunDecl { name: n, name_span, body, .. } => {
1337
+ if n.as_ref() == name {
1338
+ Some(*name_span)
1339
+ } else {
1340
+ decl_span(body, name)
1341
+ }
1342
+ }
1343
+ Statement::Block { statements, .. } | Statement::Multi { statements, .. } => {
1344
+ statements.iter().find_map(|x| decl_span(x, name))
1345
+ }
1346
+ Statement::If { then_branch, else_branch, .. } => decl_span(then_branch, name)
1347
+ .or_else(|| else_branch.as_ref().and_then(|e| decl_span(e, name))),
1348
+ Statement::For { body, .. }
1349
+ | Statement::While { body, .. }
1350
+ | Statement::DoWhile { body, .. }
1351
+ | Statement::ForOf { body, .. } => decl_span(body, name),
1352
+ _ => None,
1353
+ }
1354
+ }
1355
+
1356
+ fn span_of(p: &tishlang_ast::Program, name: &str) -> Span {
1357
+ p.statements
1358
+ .iter()
1359
+ .find_map(|s| decl_span(s, name))
1360
+ .unwrap_or_else(|| panic!("decl `{name}` not found"))
1361
+ }
1362
+
1363
+ fn param_span(p: &tishlang_ast::Program, fname: &str, pname: &str) -> Span {
1364
+ for s in &p.statements {
1365
+ if let Statement::FunDecl { name, params, .. } = s {
1366
+ if name.as_ref() == fname {
1367
+ for fp in params {
1368
+ if let FunParam::Simple(tp) = fp {
1369
+ if tp.name.as_ref() == pname {
1370
+ return tp.name_span;
1371
+ }
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+ }
1377
+ panic!("param `{fname}.{pname}` not found")
1378
+ }
1379
+
1380
+ fn hint(p: &tishlang_ast::Program, span: &Span) -> String {
1381
+ type_hint_at_def(p, span).expect("expected a type hint")
1382
+ }
1383
+
1384
+ #[test]
1385
+ fn annotated_var() {
1386
+ let p = parse("let count: number = 0\n");
1387
+ assert!(hint(&p, &span_of(&p, "count")).contains("let count: number"));
1388
+ }
1389
+
1390
+ #[test]
1391
+ fn inferred_var_and_const() {
1392
+ let p = parse("let x = 42\nconst label = \"hi\"\nlet ok = true\n");
1393
+ assert!(hint(&p, &span_of(&p, "x")).contains("let x: number"));
1394
+ assert!(hint(&p, &span_of(&p, "label")).contains("const label: string"));
1395
+ assert!(hint(&p, &span_of(&p, "ok")).contains("let ok: boolean"));
1396
+ }
1397
+
1398
+ #[test]
1399
+ fn function_signature() {
1400
+ let p = parse("fn add(a: number, b: number): number { return a + b }\n");
1401
+ assert!(hint(&p, &span_of(&p, "add")).contains("fn add(a: number, b: number): number"));
1402
+ }
1403
+
1404
+ #[test]
1405
+ fn parameter_hover() {
1406
+ let p = parse("fn f(p: string) { return p }\n");
1407
+ assert!(hint(&p, &param_span(&p, "f", "p")).contains("(parameter) p: string"));
1408
+ }
1409
+
1410
+ #[test]
1411
+ fn nested_decl_resolves() {
1412
+ let p = parse("fn g() {\n let inner: boolean = true\n return inner\n}\n");
1413
+ assert!(hint(&p, &span_of(&p, "inner")).contains("let inner: boolean"));
1414
+ }
1415
+
1416
+ #[test]
1417
+ fn composite_types_render() {
1418
+ use tishlang_ast::{TypeAnnotation as T, TypeLiteral as L};
1419
+ let arr = T::Array(Box::new(T::Simple("number".into())));
1420
+ assert_eq!(render_type(&arr), "number[]");
1421
+ let tup = T::Tuple(vec![T::Simple("number".into()), T::Simple("string".into())]);
1422
+ assert_eq!(render_type(&tup), "[number, string]");
1423
+ let uni = T::Union(vec![T::Simple("number".into()), T::Simple("null".into())]);
1424
+ assert_eq!(render_type(&uni), "number | null");
1425
+ assert_eq!(render_type(&T::Literal(L::Str("on".into()))), "\"on\"");
1426
+ let arr_of_union = T::Array(Box::new(uni));
1427
+ assert_eq!(render_type(&arr_of_union), "(number | null)[]");
1428
+ }
1429
+ }
1430
+
1431
+ #[cfg(test)]
1432
+ mod type_ref_tests {
1433
+ use super::*;
1434
+
1435
+ const SRC: &str =
1436
+ "interface Point { x: number, y: number }\ntype Status = \"on\" | \"off\"\nlet p: Point = { x: 1, y: 2 }\n";
1437
+
1438
+ #[test]
1439
+ fn type_decl_lookup_and_body() {
1440
+ let p = tishlang_parser::parse(SRC).expect("parse");
1441
+ assert!(type_decl_span(&p, "Point").is_some());
1442
+ assert!(type_decl_span(&p, "Status").is_some());
1443
+ assert_eq!(type_alias_body(&p, "Point").as_deref(), Some("{ x: number, y: number }"));
1444
+ assert_eq!(type_alias_body(&p, "Status").as_deref(), Some("\"on\" | \"off\""));
1445
+ assert!(type_decl_span(&p, "Nope").is_none());
1446
+ }
1447
+
1448
+ #[test]
1449
+ fn word_at_position_finds_whole_word() {
1450
+ // Cursor in the MIDDLE of `Point` (line 2, the `o`) must yield the whole word.
1451
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 8 }), "Point");
1452
+ // At the word start.
1453
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 7 }), "Point");
1454
+ // Just past the end (on the space) falls back to the word on the left.
1455
+ assert_eq!(word_at_position(SRC, Position { line: 2, character: 12 }), "Point");
1456
+ // On punctuation between words → empty.
1457
+ assert_eq!(word_at_position("a = b\n", Position { line: 0, character: 2 }), "");
1458
+ }
1459
+ }