@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.
@@ -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
- let lines = text.lines().count() as u32;
707
- let last_line = text.lines().last().map(|l| l.len() as u32).unwrap_or(0);
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(lines.saturating_sub(1), last_line),
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::Block { statements, .. } => {
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::Block { statements, .. } => {
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(&mut self, close_tag: &str) -> Result<Vec<JsxChild>, String> {
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 name = self
2533
- .expect(TokenKind::Ident)?
2534
- .literal
2535
- .clone()
2536
- .ok_or("Expected tag name")?;
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());