@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.
- package/Cargo.toml +2 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +10 -2
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +61 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +2 -2
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
- 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 _ =
|
|
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
|
-
|
|
288
|
-
.
|
|
289
|
-
.
|
|
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
|
-
|
|
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, .. } =>
|
|
793
|
-
|
|
794
|
-
|
|
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
|
|
860
|
-
let
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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, ¶m_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
|
+
}
|