@tishlang/tish 2.2.0 → 2.2.4
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/bin/tish +0 -0
- package/crates/tish/Cargo.toml +7 -3
- package/crates/tish/src/main.rs +26 -8
- package/crates/tish_ast/src/ast.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +0 -4
- package/crates/tish_build_utils/src/lib.rs +0 -41
- package/crates/tish_bytecode/Cargo.toml +0 -1
- package/crates/tish_compile/src/codegen.rs +21 -0
- package/crates/tish_cranelift/Cargo.toml +0 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +0 -1
- package/crates/tish_eval/Cargo.toml +1 -6
- package/crates/tish_fmt/src/lib.rs +156 -75
- package/crates/tish_lsp/src/import_goto.rs +25 -3
- package/crates/tish_lsp/src/main.rs +250 -5
- package/crates/tish_parser/src/parser.rs +20 -8
- package/crates/tish_resolve/src/lib.rs +775 -55
- package/crates/tish_runtime/Cargo.toml +0 -4
- package/crates/tishlang_cargo_bindgen/Cargo.toml +7 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -69,6 +69,16 @@ fn pos(line: u32, col: u32) -> Position {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/// End position of a full-document range. Splits on '\n' (not `str::lines`, which drops a trailing
|
|
73
|
+
/// newline) so the range reaches *past* the document's final newline; the last segment is counted in
|
|
74
|
+
/// UTF-16 code units, as the LSP position encoding requires.
|
|
75
|
+
fn full_doc_end(text: &str) -> (u32, u32) {
|
|
76
|
+
let line = text.matches('\n').count() as u32;
|
|
77
|
+
let last_seg = text.rsplit('\n').next().unwrap_or("");
|
|
78
|
+
let col = last_seg.encode_utf16().count() as u32;
|
|
79
|
+
(line, col)
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
fn diag_range(line: u32, col: u32, text: &str) -> Range {
|
|
73
83
|
let line_str = text.lines().nth(line as usize).unwrap_or("");
|
|
74
84
|
let end_char = line_str.len().max(col as usize + 1) as u32;
|
|
@@ -400,6 +410,24 @@ impl LanguageServer for Backend {
|
|
|
400
410
|
if let Some(def) =
|
|
401
411
|
tishlang_resolve::definition_span(&program, &text, position.line, position.character)
|
|
402
412
|
{
|
|
413
|
+
// If the use resolves to an import specifier, jump THROUGH the import into the source
|
|
414
|
+
// module (a relative .tish file) instead of to the local import line. Falls back to the
|
|
415
|
+
// specifier span when the source module can't be located.
|
|
416
|
+
if is_import_specifier_span(&program, &def) {
|
|
417
|
+
if let Ok(ref file_path) = uri.to_file_path() {
|
|
418
|
+
let word = word_at_position(&text, position);
|
|
419
|
+
let roots = self.roots.read().unwrap().clone();
|
|
420
|
+
if let Some(loc) = import_goto::definition_for_import(
|
|
421
|
+
&program,
|
|
422
|
+
file_path,
|
|
423
|
+
word.as_str(),
|
|
424
|
+
&roots,
|
|
425
|
+
self.cargo_src_cache.as_ref(),
|
|
426
|
+
) {
|
|
427
|
+
return Ok(Some(GotoDefinitionResponse::Scalar(loc)));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
403
431
|
let range = span_to_range(&def, &text);
|
|
404
432
|
return Ok(Some(GotoDefinitionResponse::Scalar(Location {
|
|
405
433
|
uri: uri.clone(),
|
|
@@ -608,8 +636,12 @@ impl LanguageServer for Backend {
|
|
|
608
636
|
};
|
|
609
637
|
let spans =
|
|
610
638
|
tishlang_resolve::reference_spans_for_def(&program, &text, nu.name.as_ref(), def);
|
|
639
|
+
// Honor the client's includeDeclaration flag: reference_spans_for_def returns the definition
|
|
640
|
+
// span plus the use spans, so drop the definition when only uses were requested.
|
|
641
|
+
let include_decl = params.context.include_declaration;
|
|
611
642
|
let locs: Vec<Location> = spans
|
|
612
643
|
.into_iter()
|
|
644
|
+
.filter(|sp| include_decl || *sp != def)
|
|
613
645
|
.map(|sp| Location {
|
|
614
646
|
uri: uri.clone(),
|
|
615
647
|
range: span_to_range(&sp, &text),
|
|
@@ -703,12 +735,14 @@ impl LanguageServer for Backend {
|
|
|
703
735
|
};
|
|
704
736
|
match tishlang_fmt::format_source(&text) {
|
|
705
737
|
Ok(formatted) => {
|
|
706
|
-
|
|
707
|
-
|
|
738
|
+
// Replace the WHOLE document. Using a range that stops before the document's final
|
|
739
|
+
// newline appends the formatter's own trailing newline on top of it, adding a blank
|
|
740
|
+
// line on every format (see full_doc_end).
|
|
741
|
+
let (end_line, end_char) = full_doc_end(&text);
|
|
708
742
|
Ok(Some(vec![tower_lsp::lsp_types::TextEdit {
|
|
709
743
|
range: Range {
|
|
710
744
|
start: pos(0, 0),
|
|
711
|
-
end: pos(
|
|
745
|
+
end: pos(end_line, end_char),
|
|
712
746
|
},
|
|
713
747
|
new_text: formatted,
|
|
714
748
|
}]))
|
|
@@ -798,7 +832,61 @@ fn collect_workspace_syms(
|
|
|
798
832
|
));
|
|
799
833
|
}
|
|
800
834
|
}
|
|
801
|
-
tishlang_ast::Statement::
|
|
835
|
+
tishlang_ast::Statement::TypeAlias {
|
|
836
|
+
name, name_span, ..
|
|
837
|
+
} => {
|
|
838
|
+
if name.to_lowercase().contains(query) {
|
|
839
|
+
out.push(symbol_information(
|
|
840
|
+
name.to_string(),
|
|
841
|
+
SymbolKind::INTERFACE,
|
|
842
|
+
None,
|
|
843
|
+
Location {
|
|
844
|
+
uri: uri.clone(),
|
|
845
|
+
range: span_to_range(name_span, text),
|
|
846
|
+
},
|
|
847
|
+
None,
|
|
848
|
+
));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
tishlang_ast::Statement::DeclareFun {
|
|
852
|
+
name, name_span, ..
|
|
853
|
+
} => {
|
|
854
|
+
if name.to_lowercase().contains(query) {
|
|
855
|
+
out.push(symbol_information(
|
|
856
|
+
name.to_string(),
|
|
857
|
+
SymbolKind::FUNCTION,
|
|
858
|
+
None,
|
|
859
|
+
Location {
|
|
860
|
+
uri: uri.clone(),
|
|
861
|
+
range: span_to_range(name_span, text),
|
|
862
|
+
},
|
|
863
|
+
None,
|
|
864
|
+
));
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
tishlang_ast::Statement::DeclareVar {
|
|
868
|
+
name, name_span, ..
|
|
869
|
+
} => {
|
|
870
|
+
if name.to_lowercase().contains(query) {
|
|
871
|
+
out.push(symbol_information(
|
|
872
|
+
name.to_string(),
|
|
873
|
+
SymbolKind::VARIABLE,
|
|
874
|
+
None,
|
|
875
|
+
Location {
|
|
876
|
+
uri: uri.clone(),
|
|
877
|
+
range: span_to_range(name_span, text),
|
|
878
|
+
},
|
|
879
|
+
None,
|
|
880
|
+
));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
tishlang_ast::Statement::Export { declaration, .. } => {
|
|
884
|
+
if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
|
|
885
|
+
collect_workspace_syms(inner, text, uri, query, out);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
tishlang_ast::Statement::Block { statements, .. }
|
|
889
|
+
| tishlang_ast::Statement::Multi { statements, .. } => {
|
|
802
890
|
for x in statements {
|
|
803
891
|
collect_workspace_syms(x, text, uri, query, out);
|
|
804
892
|
}
|
|
@@ -842,6 +930,28 @@ pub(crate) fn find_export(
|
|
|
842
930
|
None
|
|
843
931
|
}
|
|
844
932
|
|
|
933
|
+
/// Locate the `export default …` statement in a module (the target of a default import).
|
|
934
|
+
pub(crate) fn find_default_export(
|
|
935
|
+
program: &tishlang_ast::Program,
|
|
936
|
+
uri: &Url,
|
|
937
|
+
text: &str,
|
|
938
|
+
) -> Option<Location> {
|
|
939
|
+
for s in &program.statements {
|
|
940
|
+
if let tishlang_ast::Statement::Export { declaration, span } = s {
|
|
941
|
+
if matches!(
|
|
942
|
+
declaration.as_ref(),
|
|
943
|
+
tishlang_ast::ExportDeclaration::Default(_)
|
|
944
|
+
) {
|
|
945
|
+
return Some(Location {
|
|
946
|
+
uri: uri.clone(),
|
|
947
|
+
range: span_to_range(span, text),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
None
|
|
953
|
+
}
|
|
954
|
+
|
|
845
955
|
fn find_decl_in_stmt(
|
|
846
956
|
s: &tishlang_ast::Statement,
|
|
847
957
|
word: &str,
|
|
@@ -893,6 +1003,31 @@ fn span_to_range(span: &tishlang_ast::Span, text: &str) -> Range {
|
|
|
893
1003
|
}
|
|
894
1004
|
}
|
|
895
1005
|
|
|
1006
|
+
/// Whether `span` is the local-name span of an import specifier — i.e. `definition_span` resolved a
|
|
1007
|
+
/// use to an `import { … }` line. Go-to-definition should follow such a result through to the source
|
|
1008
|
+
/// module rather than jumping to the import line itself.
|
|
1009
|
+
fn is_import_specifier_span(program: &tishlang_ast::Program, span: &tishlang_ast::Span) -> bool {
|
|
1010
|
+
use tishlang_ast::{ImportSpecifier, Statement};
|
|
1011
|
+
program.statements.iter().any(|s| {
|
|
1012
|
+
if let Statement::Import { specifiers, .. } = s {
|
|
1013
|
+
specifiers.iter().any(|sp| {
|
|
1014
|
+
let local = match sp {
|
|
1015
|
+
ImportSpecifier::Named {
|
|
1016
|
+
name_span,
|
|
1017
|
+
alias_span,
|
|
1018
|
+
..
|
|
1019
|
+
} => alias_span.as_ref().unwrap_or(name_span),
|
|
1020
|
+
ImportSpecifier::Namespace { name_span, .. }
|
|
1021
|
+
| ImportSpecifier::Default { name_span, .. } => name_span,
|
|
1022
|
+
};
|
|
1023
|
+
local == span
|
|
1024
|
+
})
|
|
1025
|
+
} else {
|
|
1026
|
+
false
|
|
1027
|
+
}
|
|
1028
|
+
})
|
|
1029
|
+
}
|
|
1030
|
+
|
|
896
1031
|
fn word_at_position(text: &str, position: Position) -> String {
|
|
897
1032
|
let line = text.lines().nth(position.line as usize).unwrap_or("");
|
|
898
1033
|
let chars: Vec<(usize, char)> = line.char_indices().collect();
|
|
@@ -1296,7 +1431,64 @@ fn doc_symbol_stmt(
|
|
|
1296
1431
|
None,
|
|
1297
1432
|
));
|
|
1298
1433
|
}
|
|
1299
|
-
tishlang_ast::Statement::
|
|
1434
|
+
tishlang_ast::Statement::TypeAlias {
|
|
1435
|
+
name,
|
|
1436
|
+
name_span,
|
|
1437
|
+
span,
|
|
1438
|
+
..
|
|
1439
|
+
} => {
|
|
1440
|
+
out.push(document_symbol(
|
|
1441
|
+
name.to_string(),
|
|
1442
|
+
None,
|
|
1443
|
+
SymbolKind::INTERFACE,
|
|
1444
|
+
None,
|
|
1445
|
+
span_to_range(span, text),
|
|
1446
|
+
span_to_range(name_span, text),
|
|
1447
|
+
None,
|
|
1448
|
+
));
|
|
1449
|
+
}
|
|
1450
|
+
tishlang_ast::Statement::DeclareFun {
|
|
1451
|
+
name,
|
|
1452
|
+
name_span,
|
|
1453
|
+
span,
|
|
1454
|
+
..
|
|
1455
|
+
} => {
|
|
1456
|
+
out.push(document_symbol(
|
|
1457
|
+
name.to_string(),
|
|
1458
|
+
None,
|
|
1459
|
+
SymbolKind::FUNCTION,
|
|
1460
|
+
None,
|
|
1461
|
+
span_to_range(span, text),
|
|
1462
|
+
span_to_range(name_span, text),
|
|
1463
|
+
None,
|
|
1464
|
+
));
|
|
1465
|
+
}
|
|
1466
|
+
tishlang_ast::Statement::DeclareVar {
|
|
1467
|
+
name,
|
|
1468
|
+
name_span,
|
|
1469
|
+
span,
|
|
1470
|
+
..
|
|
1471
|
+
} => {
|
|
1472
|
+
out.push(document_symbol(
|
|
1473
|
+
name.to_string(),
|
|
1474
|
+
None,
|
|
1475
|
+
SymbolKind::VARIABLE,
|
|
1476
|
+
None,
|
|
1477
|
+
span_to_range(span, text),
|
|
1478
|
+
span_to_range(name_span, text),
|
|
1479
|
+
None,
|
|
1480
|
+
));
|
|
1481
|
+
}
|
|
1482
|
+
// `export fn` / `export let` / `export type` wrap the declaration — descend into it so
|
|
1483
|
+
// exported symbols appear in the outline.
|
|
1484
|
+
tishlang_ast::Statement::Export { declaration, .. } => {
|
|
1485
|
+
if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
|
|
1486
|
+
doc_symbol_stmt(inner, text, out);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
// Block and the transparent comma-declarator group (`let a = 1, b = 2`).
|
|
1490
|
+
tishlang_ast::Statement::Block { statements, .. }
|
|
1491
|
+
| tishlang_ast::Statement::Multi { statements, .. } => {
|
|
1300
1492
|
for x in statements {
|
|
1301
1493
|
doc_symbol_stmt(x, text, out);
|
|
1302
1494
|
}
|
|
@@ -1381,6 +1573,50 @@ mod hover_tests {
|
|
|
1381
1573
|
type_hint_at_def(p, span).expect("expected a type hint")
|
|
1382
1574
|
}
|
|
1383
1575
|
|
|
1576
|
+
#[test]
|
|
1577
|
+
fn document_symbols_include_exported_type_and_comma_decls() {
|
|
1578
|
+
let src = "export fn foo() {}\ntype Status = number\nlet a = 1, b = 2\ndeclare fn ext(): void\nlet plain = 3\n";
|
|
1579
|
+
let program = tishlang_parser::parse(src).unwrap();
|
|
1580
|
+
let mut syms = Vec::new();
|
|
1581
|
+
for s in &program.statements {
|
|
1582
|
+
doc_symbol_stmt(s, src, &mut syms);
|
|
1583
|
+
}
|
|
1584
|
+
let names: Vec<&str> = syms.iter().map(|s| s.name.as_str()).collect();
|
|
1585
|
+
for expected in ["foo", "Status", "a", "b", "ext", "plain"] {
|
|
1586
|
+
assert!(names.contains(&expected), "outline missing `{expected}`: {names:?}");
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
#[test]
|
|
1591
|
+
fn is_import_specifier_span_detects_imports() {
|
|
1592
|
+
let src = "import { foo } from \"./m\"\nfoo()\nlet x = 1\nx\n";
|
|
1593
|
+
let program = tishlang_parser::parse(src).unwrap();
|
|
1594
|
+
// `foo()` (line 1) resolves to the import specifier → go-to-def should follow it cross-file.
|
|
1595
|
+
let foo_def = tishlang_resolve::definition_span(&program, src, 1, 0).expect("foo resolves");
|
|
1596
|
+
assert!(
|
|
1597
|
+
is_import_specifier_span(&program, &foo_def),
|
|
1598
|
+
"foo resolves to an import specifier"
|
|
1599
|
+
);
|
|
1600
|
+
// `x` (line 3) resolves to the local `let` → not an import, jump to the local def as usual.
|
|
1601
|
+
let x_def = tishlang_resolve::definition_span(&program, src, 3, 0).expect("x resolves");
|
|
1602
|
+
assert!(
|
|
1603
|
+
!is_import_specifier_span(&program, &x_def),
|
|
1604
|
+
"x is a local binding, not an import"
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
#[test]
|
|
1609
|
+
fn find_default_export_locates_export_default() {
|
|
1610
|
+
let src = "export fn foo() {}\nexport default 42\n";
|
|
1611
|
+
let program = tishlang_parser::parse(src).unwrap();
|
|
1612
|
+
let uri = Url::parse("file:///m.tish").unwrap();
|
|
1613
|
+
let loc = find_default_export(&program, &uri, src).expect("default export found");
|
|
1614
|
+
assert_eq!(loc.range.start.line, 1, "export default is on line 1");
|
|
1615
|
+
let none_src = "export fn bar() {}\n";
|
|
1616
|
+
let p2 = tishlang_parser::parse(none_src).unwrap();
|
|
1617
|
+
assert!(find_default_export(&p2, &uri, none_src).is_none());
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1384
1620
|
#[test]
|
|
1385
1621
|
fn annotated_var() {
|
|
1386
1622
|
let p = parse("let count: number = 0\n");
|
|
@@ -1426,6 +1662,15 @@ mod hover_tests {
|
|
|
1426
1662
|
let arr_of_union = T::Array(Box::new(uni));
|
|
1427
1663
|
assert_eq!(render_type(&arr_of_union), "(number | null)[]");
|
|
1428
1664
|
}
|
|
1665
|
+
|
|
1666
|
+
#[test]
|
|
1667
|
+
fn full_doc_end_reaches_past_trailing_newline_in_utf16() {
|
|
1668
|
+
assert_eq!(full_doc_end("a\nb\n"), (2, 0)); // past the final newline (was the blank-line bug)
|
|
1669
|
+
assert_eq!(full_doc_end("a\nb"), (1, 1)); // no trailing newline
|
|
1670
|
+
assert_eq!(full_doc_end("x\n"), (1, 0));
|
|
1671
|
+
assert_eq!(full_doc_end(""), (0, 0));
|
|
1672
|
+
assert_eq!(full_doc_end("café"), (0, 4)); // UTF-16 units (é = 1), not bytes (5)
|
|
1673
|
+
}
|
|
1429
1674
|
}
|
|
1430
1675
|
|
|
1431
1676
|
#[cfg(test)]
|
|
@@ -2391,6 +2391,10 @@ impl<'a> Parser<'a> {
|
|
|
2391
2391
|
fn parse_jsx_element(&mut self, start: (usize, usize)) -> Result<Expr, String> {
|
|
2392
2392
|
let tag_tok = self.expect(TokenKind::Ident)?;
|
|
2393
2393
|
let tag = tag_tok.literal.clone().ok_or("Expected tag name")?;
|
|
2394
|
+
let tag_span = Span {
|
|
2395
|
+
start: tag_tok.span.start,
|
|
2396
|
+
end: tag_tok.span.end,
|
|
2397
|
+
};
|
|
2394
2398
|
|
|
2395
2399
|
let mut props = Vec::new();
|
|
2396
2400
|
loop {
|
|
@@ -2402,6 +2406,8 @@ impl<'a> Parser<'a> {
|
|
|
2402
2406
|
let end = self.previous_span_end();
|
|
2403
2407
|
return Ok(Expr::JsxElement {
|
|
2404
2408
|
tag,
|
|
2409
|
+
tag_span,
|
|
2410
|
+
close_tag_span: None,
|
|
2405
2411
|
props,
|
|
2406
2412
|
children: vec![],
|
|
2407
2413
|
span: Span { start, end },
|
|
@@ -2451,10 +2457,12 @@ impl<'a> Parser<'a> {
|
|
|
2451
2457
|
}
|
|
2452
2458
|
self.advance(); // consume >
|
|
2453
2459
|
|
|
2454
|
-
let children = self.parse_jsx_children(&tag)?;
|
|
2460
|
+
let (children, close_tag_span) = self.parse_jsx_children(&tag)?;
|
|
2455
2461
|
let end = self.previous_span_end();
|
|
2456
2462
|
Ok(Expr::JsxElement {
|
|
2457
2463
|
tag,
|
|
2464
|
+
tag_span,
|
|
2465
|
+
close_tag_span,
|
|
2458
2466
|
props,
|
|
2459
2467
|
children,
|
|
2460
2468
|
span: Span { start, end },
|
|
@@ -2517,7 +2525,10 @@ impl<'a> Parser<'a> {
|
|
|
2517
2525
|
}
|
|
2518
2526
|
|
|
2519
2527
|
/// Parse JSX children until </Tag> or </>
|
|
2520
|
-
fn parse_jsx_children(
|
|
2528
|
+
fn parse_jsx_children(
|
|
2529
|
+
&mut self,
|
|
2530
|
+
close_tag: &str,
|
|
2531
|
+
) -> Result<(Vec<JsxChild>, Option<Span>), String> {
|
|
2521
2532
|
let mut children = Vec::new();
|
|
2522
2533
|
loop {
|
|
2523
2534
|
match self.peek_kind() {
|
|
@@ -2529,11 +2540,12 @@ impl<'a> Parser<'a> {
|
|
|
2529
2540
|
// </ closing tag
|
|
2530
2541
|
self.advance(); // <
|
|
2531
2542
|
self.advance(); // /
|
|
2532
|
-
let
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
.
|
|
2536
|
-
.
|
|
2543
|
+
let name_tok = self.expect(TokenKind::Ident)?;
|
|
2544
|
+
let name = name_tok.literal.clone().ok_or("Expected tag name")?;
|
|
2545
|
+
let close_tag_span = Span {
|
|
2546
|
+
start: name_tok.span.start,
|
|
2547
|
+
end: name_tok.span.end,
|
|
2548
|
+
};
|
|
2537
2549
|
if name.as_ref() != close_tag {
|
|
2538
2550
|
return Err(format!(
|
|
2539
2551
|
"Mismatched JSX tag: expected </{}> got </{}>",
|
|
@@ -2541,7 +2553,7 @@ impl<'a> Parser<'a> {
|
|
|
2541
2553
|
));
|
|
2542
2554
|
}
|
|
2543
2555
|
self.expect(TokenKind::Gt)?; // >
|
|
2544
|
-
return Ok(children);
|
|
2556
|
+
return Ok((children, Some(close_tag_span)));
|
|
2545
2557
|
}
|
|
2546
2558
|
if t.kind == TokenKind::Gt {
|
|
2547
2559
|
return Err("Unexpected <> in JSX children".to_string());
|