java-functional-lsp 0.9.4__tar.gz → 0.9.6__tar.gz

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 (69) hide show
  1. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/PKG-INFO +1 -1
  2. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/pyproject.toml +1 -1
  3. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/__init__.py +1 -1
  4. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/server.py +238 -8
  5. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_server.py +576 -10
  6. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/uv.lock +1 -1
  7. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.claude-plugin/plugin.json +0 -0
  8. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.githooks/pre-commit +0 -0
  9. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.githooks/pre-push +0 -0
  10. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/CODEOWNERS +0 -0
  11. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  12. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  13. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/SECURITY.md +0 -0
  15. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/dependabot.yml +0 -0
  16. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/release-drafter.yml +0 -0
  17. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/workflows/publish.yml +0 -0
  18. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/workflows/release-drafter.yml +0 -0
  19. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/workflows/stale.yml +0 -0
  20. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/workflows/test.yml +0 -0
  21. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/workflows/update-homebrew.yml +0 -0
  22. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.github/workflows/vscode-ext.yml +0 -0
  23. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/.gitignore +0 -0
  24. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/CONTRIBUTING.md +0 -0
  25. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/LICENSE +0 -0
  26. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/README.md +0 -0
  27. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/SKILL.md +0 -0
  28. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/commands/lint-java.md +0 -0
  29. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/editors/intellij/README.md +0 -0
  30. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/editors/intellij/lsp4ij-template.json +0 -0
  31. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/editors/vscode/.vscodeignore +0 -0
  32. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/editors/vscode/README.md +0 -0
  33. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/editors/vscode/package-lock.json +0 -0
  34. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/editors/vscode/package.json +0 -0
  35. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/editors/vscode/src/extension.ts +0 -0
  36. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/editors/vscode/tsconfig.json +0 -0
  37. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/hooks/hooks.json +0 -0
  38. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/hooks/java_linter_reminder.py +0 -0
  39. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/scripts/ensure-lsp.sh +0 -0
  40. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/scripts/generate-formula.py +0 -0
  41. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/__main__.py +0 -0
  42. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  43. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/analyzers/base.py +0 -0
  44. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  45. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  46. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  47. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  48. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  49. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/cli.py +0 -0
  50. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/fixes.py +0 -0
  51. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/merkle.py +0 -0
  52. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/src/java_functional_lsp/proxy.py +0 -0
  53. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/__init__.py +0 -0
  54. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/conftest.py +0 -0
  55. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_base.py +0 -0
  56. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_cli.py +0 -0
  57. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_config.py +0 -0
  58. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_e2e.py +0 -0
  59. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_e2e_jdtls.py +0 -0
  60. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_exception_checker.py +0 -0
  61. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_fixes.py +0 -0
  62. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_functional_checker.py +0 -0
  63. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_merkle.py +0 -0
  64. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_merkle_proxy.py +0 -0
  65. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_mutation_checker.py +0 -0
  66. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_null_checker.py +0 -0
  67. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_proxy.py +0 -0
  68. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_spring_checker.py +0 -0
  69. {java_functional_lsp-0.9.4 → java_functional_lsp-0.9.6}/tests/test_suppress.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.9.4
3
+ Version: 0.9.6
4
4
  Summary: Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions
5
5
  Project-URL: Homepage, https://github.com/aviadshiber/java-functional-lsp
6
6
  Project-URL: Repository, https://github.com/aviadshiber/java-functional-lsp
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "java-functional-lsp"
7
- version = "0.9.4"
7
+ version = "0.9.6"
8
8
  description = "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,3 +1,3 @@
1
1
  """java-functional-lsp: A Java LSP server enforcing functional programming best practices."""
2
2
 
3
- __version__ = "0.9.4"
3
+ __version__ = "0.9.6"
@@ -446,6 +446,20 @@ _JDTLS_CAPABILITIES: list[tuple[str, str, type[Any], dict[str, Any]]] = [
446
446
  ("definition", lsp.TEXT_DOCUMENT_DEFINITION, lsp.DefinitionRegistrationOptions, {}),
447
447
  ("references", lsp.TEXT_DOCUMENT_REFERENCES, lsp.ReferenceRegistrationOptions, {}),
448
448
  ("document-symbol", lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL, lsp.DocumentSymbolRegistrationOptions, {}),
449
+ ("call-hierarchy", lsp.TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY, lsp.CallHierarchyRegistrationOptions, {}),
450
+ (
451
+ "signature-help",
452
+ lsp.TEXT_DOCUMENT_SIGNATURE_HELP,
453
+ lsp.SignatureHelpRegistrationOptions,
454
+ {"trigger_characters": ["(", ","], "retrigger_characters": [")"]},
455
+ ),
456
+ ("implementation", lsp.TEXT_DOCUMENT_IMPLEMENTATION, lsp.ImplementationRegistrationOptions, {}),
457
+ ("type-definition", lsp.TEXT_DOCUMENT_TYPE_DEFINITION, lsp.TypeDefinitionRegistrationOptions, {}),
458
+ ("declaration", lsp.TEXT_DOCUMENT_DECLARATION, lsp.DeclarationRegistrationOptions, {}),
459
+ ("document-highlight", lsp.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT, lsp.DocumentHighlightRegistrationOptions, {}),
460
+ ("rename", lsp.TEXT_DOCUMENT_RENAME, lsp.RenameRegistrationOptions, {"prepare_provider": True}),
461
+ ("type-hierarchy", lsp.TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY, lsp.TypeHierarchyRegistrationOptions, {}),
462
+ ("workspace-symbol", lsp.WORKSPACE_SYMBOL, lsp.WorkspaceSymbolRegistrationOptions, {}),
449
463
  ]
450
464
 
451
465
  # Maps LSP method → handler function for dynamic registration.
@@ -457,14 +471,20 @@ _jdtls_capabilities_registered = False
457
471
 
458
472
  def _build_jdtls_registrations() -> list[lsp.Registration]:
459
473
  """Build LSP Registration objects for jdtls-dependent capabilities."""
460
- return [
461
- lsp.Registration(
462
- id=f"{_JDTLS_REG_PREFIX}{suffix}",
463
- method=method,
464
- register_options=_converter.unstructure(opts_cls(document_selector=_JAVA_SELECTOR, **extra)),
474
+ result = []
475
+ for suffix, method, opts_cls, extra in _JDTLS_CAPABILITIES:
476
+ field_names = {f.name for f in getattr(opts_cls, "__attrs_attrs__", [])}
477
+ kwargs: dict[str, Any] = {**extra}
478
+ if "document_selector" in field_names:
479
+ kwargs["document_selector"] = _JAVA_SELECTOR
480
+ result.append(
481
+ lsp.Registration(
482
+ id=f"{_JDTLS_REG_PREFIX}{suffix}",
483
+ method=method,
484
+ register_options=_converter.unstructure(opts_cls(**kwargs)),
485
+ )
465
486
  )
466
- for suffix, method, opts_cls, extra in _JDTLS_CAPABILITIES
467
- ]
487
+ return result
468
488
 
469
489
 
470
490
  async def _register_jdtls_capabilities() -> None:
@@ -499,7 +519,11 @@ async def _register_jdtls_capabilities() -> None:
499
519
  return
500
520
 
501
521
  _jdtls_capabilities_registered = True
502
- logger.info("Dynamically registered jdtls capabilities (hover, definition, references, completion, symbol)")
522
+ logger.info(
523
+ "jdtls capabilities registered: completion, hover, definition, references, symbol, "
524
+ "call-hierarchy, signature-help, implementation, type-definition, declaration, "
525
+ "document-highlight, rename, type-hierarchy, workspace-symbol"
526
+ )
503
527
  except Exception:
504
528
  logger.warning("Failed to dynamically register jdtls capabilities", exc_info=True)
505
529
 
@@ -739,6 +763,198 @@ async def _on_document_symbol(params: lsp.DocumentSymbolParams) -> list[lsp.Docu
739
763
  return None
740
764
 
741
765
 
766
+ async def _on_prepare_call_hierarchy(params: lsp.CallHierarchyPrepareParams) -> list[lsp.CallHierarchyItem] | None:
767
+ """Forward prepareCallHierarchy request to jdtls."""
768
+ result = await _ensure_module_and_forward("textDocument/prepareCallHierarchy", params, params.text_document.uri)
769
+ if result is None:
770
+ return None
771
+ try:
772
+ return [_converter.structure(item, lsp.CallHierarchyItem) for item in result]
773
+ except Exception:
774
+ return None
775
+
776
+
777
+ async def _on_incoming_calls(
778
+ params: lsp.CallHierarchyIncomingCallsParams,
779
+ ) -> list[lsp.CallHierarchyIncomingCall] | None:
780
+ """Forward callHierarchy/incomingCalls request to jdtls."""
781
+ result = await _ensure_module_and_forward("callHierarchy/incomingCalls", params, params.item.uri)
782
+ if result is None:
783
+ return None
784
+ try:
785
+ return [_converter.structure(c, lsp.CallHierarchyIncomingCall) for c in result]
786
+ except Exception:
787
+ return None
788
+
789
+
790
+ async def _on_outgoing_calls(
791
+ params: lsp.CallHierarchyOutgoingCallsParams,
792
+ ) -> list[lsp.CallHierarchyOutgoingCall] | None:
793
+ """Forward callHierarchy/outgoingCalls request to jdtls."""
794
+ result = await _ensure_module_and_forward("callHierarchy/outgoingCalls", params, params.item.uri)
795
+ if result is None:
796
+ return None
797
+ try:
798
+ return [_converter.structure(c, lsp.CallHierarchyOutgoingCall) for c in result]
799
+ except Exception:
800
+ return None
801
+
802
+
803
+ async def _on_signature_help(params: lsp.SignatureHelpParams) -> lsp.SignatureHelp | None:
804
+ """Forward signatureHelp request to jdtls."""
805
+ result = await _ensure_module_and_forward("textDocument/signatureHelp", params, params.text_document.uri)
806
+ if result is None:
807
+ return None
808
+ try:
809
+ return _converter.structure(result, lsp.SignatureHelp)
810
+ except Exception:
811
+ logger.debug("Failed to structure signatureHelp result", exc_info=True)
812
+ return None
813
+
814
+
815
+ async def _on_implementation(params: lsp.ImplementationParams) -> list[lsp.Location] | None:
816
+ """Forward go-to-implementation request to jdtls."""
817
+ result = await _ensure_module_and_forward("textDocument/implementation", params, params.text_document.uri)
818
+ if result is None:
819
+ return None
820
+ try:
821
+ if isinstance(result, list):
822
+ return [_converter.structure(loc, lsp.Location) for loc in result]
823
+ return [_converter.structure(result, lsp.Location)]
824
+ except Exception:
825
+ logger.debug("Failed to structure implementation result", exc_info=True)
826
+ return None
827
+
828
+
829
+ async def _on_type_definition(params: lsp.TypeDefinitionParams) -> list[lsp.Location] | None:
830
+ """Forward go-to-type-definition request to jdtls."""
831
+ result = await _ensure_module_and_forward("textDocument/typeDefinition", params, params.text_document.uri)
832
+ if result is None:
833
+ return None
834
+ try:
835
+ if isinstance(result, list):
836
+ return [_converter.structure(loc, lsp.Location) for loc in result]
837
+ return [_converter.structure(result, lsp.Location)]
838
+ except Exception:
839
+ logger.debug("Failed to structure typeDefinition result", exc_info=True)
840
+ return None
841
+
842
+
843
+ async def _on_declaration(params: lsp.DeclarationParams) -> list[lsp.Location] | None:
844
+ """Forward go-to-declaration request to jdtls."""
845
+ result = await _ensure_module_and_forward("textDocument/declaration", params, params.text_document.uri)
846
+ if result is None:
847
+ return None
848
+ try:
849
+ if isinstance(result, list):
850
+ return [_converter.structure(loc, lsp.Location) for loc in result]
851
+ return [_converter.structure(result, lsp.Location)]
852
+ except Exception:
853
+ logger.debug("Failed to structure declaration result", exc_info=True)
854
+ return None
855
+
856
+
857
+ async def _on_document_highlight(params: lsp.DocumentHighlightParams) -> list[lsp.DocumentHighlight] | None:
858
+ """Forward documentHighlight request to jdtls."""
859
+ result = await _ensure_module_and_forward("textDocument/documentHighlight", params, params.text_document.uri)
860
+ if result is None:
861
+ return None
862
+ try:
863
+ return [_converter.structure(h, lsp.DocumentHighlight) for h in result]
864
+ except Exception:
865
+ logger.debug("Failed to structure documentHighlight result", exc_info=True)
866
+ return None
867
+
868
+
869
+ async def _on_rename(params: lsp.RenameParams) -> lsp.WorkspaceEdit | None:
870
+ """Forward rename request to jdtls."""
871
+ result = await _ensure_module_and_forward("textDocument/rename", params, params.text_document.uri)
872
+ if result is None:
873
+ return None
874
+ try:
875
+ return _converter.structure(result, lsp.WorkspaceEdit)
876
+ except Exception:
877
+ logger.debug("Failed to structure rename result", exc_info=True)
878
+ return None
879
+
880
+
881
+ async def _on_prepare_rename(
882
+ params: lsp.PrepareRenameParams,
883
+ ) -> lsp.Range | lsp.PrepareRenamePlaceholder | lsp.PrepareRenameDefaultBehavior | None:
884
+ """Forward prepareRename request to jdtls.
885
+
886
+ jdtls may return Range, PrepareRenamePlaceholder ({range, placeholder}),
887
+ or PrepareRenameDefaultBehavior ({defaultBehavior}) — try each in order.
888
+ """
889
+ result = await _ensure_module_and_forward("textDocument/prepareRename", params, params.text_document.uri)
890
+ if result is None:
891
+ return None
892
+ for target_type in (lsp.PrepareRenamePlaceholder, lsp.PrepareRenameDefaultBehavior, lsp.Range):
893
+ try:
894
+ return _converter.structure(result, target_type) # type: ignore[return-value]
895
+ except Exception:
896
+ pass
897
+ logger.debug("Failed to structure prepareRename result as any known type", exc_info=False)
898
+ return None
899
+
900
+
901
+ async def _on_prepare_type_hierarchy(
902
+ params: lsp.TypeHierarchyPrepareParams,
903
+ ) -> list[lsp.TypeHierarchyItem] | None:
904
+ """Forward prepareTypeHierarchy request to jdtls."""
905
+ result = await _ensure_module_and_forward("textDocument/prepareTypeHierarchy", params, params.text_document.uri)
906
+ if result is None:
907
+ return None
908
+ try:
909
+ return [_converter.structure(item, lsp.TypeHierarchyItem) for item in result]
910
+ except Exception:
911
+ logger.debug("Failed to structure prepareTypeHierarchy result", exc_info=True)
912
+ return None
913
+
914
+
915
+ async def _on_type_hierarchy_supertypes(
916
+ params: lsp.TypeHierarchySupertypesParams,
917
+ ) -> list[lsp.TypeHierarchyItem] | None:
918
+ """Forward typeHierarchy/supertypes request to jdtls."""
919
+ result = await _ensure_module_and_forward("typeHierarchy/supertypes", params, params.item.uri)
920
+ if result is None:
921
+ return None
922
+ try:
923
+ return [_converter.structure(item, lsp.TypeHierarchyItem) for item in result]
924
+ except Exception:
925
+ logger.debug("Failed to structure typeHierarchy/supertypes result", exc_info=True)
926
+ return None
927
+
928
+
929
+ async def _on_type_hierarchy_subtypes(
930
+ params: lsp.TypeHierarchySubtypesParams,
931
+ ) -> list[lsp.TypeHierarchyItem] | None:
932
+ """Forward typeHierarchy/subtypes request to jdtls."""
933
+ result = await _ensure_module_and_forward("typeHierarchy/subtypes", params, params.item.uri)
934
+ if result is None:
935
+ return None
936
+ try:
937
+ return [_converter.structure(item, lsp.TypeHierarchyItem) for item in result]
938
+ except Exception:
939
+ logger.debug("Failed to structure typeHierarchy/subtypes result", exc_info=True)
940
+ return None
941
+
942
+
943
+ async def _on_workspace_symbol(params: lsp.WorkspaceSymbolParams) -> list[lsp.WorkspaceSymbol] | None:
944
+ """Forward workspace/symbol request to jdtls (no text_document — use proxy directly)."""
945
+ proxy = server._proxy
946
+ if not proxy.is_available:
947
+ return None
948
+ result = await proxy.send_request("workspace/symbol", _serialize_params(params))
949
+ if result is None:
950
+ return None
951
+ try:
952
+ return [_converter.structure(s, lsp.WorkspaceSymbol) for s in result]
953
+ except Exception:
954
+ logger.debug("Failed to structure workspace/symbol result", exc_info=True)
955
+ return None
956
+
957
+
742
958
  # Populate handler map for dynamic registration.
743
959
  _JDTLS_HANDLERS.update(
744
960
  {
@@ -747,6 +963,20 @@ _JDTLS_HANDLERS.update(
747
963
  lsp.TEXT_DOCUMENT_DEFINITION: _on_definition,
748
964
  lsp.TEXT_DOCUMENT_REFERENCES: _on_references,
749
965
  lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL: _on_document_symbol,
966
+ lsp.TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY: _on_prepare_call_hierarchy,
967
+ lsp.CALL_HIERARCHY_INCOMING_CALLS: _on_incoming_calls,
968
+ lsp.CALL_HIERARCHY_OUTGOING_CALLS: _on_outgoing_calls,
969
+ lsp.TEXT_DOCUMENT_SIGNATURE_HELP: _on_signature_help,
970
+ lsp.TEXT_DOCUMENT_IMPLEMENTATION: _on_implementation,
971
+ lsp.TEXT_DOCUMENT_TYPE_DEFINITION: _on_type_definition,
972
+ lsp.TEXT_DOCUMENT_DECLARATION: _on_declaration,
973
+ lsp.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT: _on_document_highlight,
974
+ lsp.TEXT_DOCUMENT_RENAME: _on_rename,
975
+ lsp.TEXT_DOCUMENT_PREPARE_RENAME: _on_prepare_rename,
976
+ lsp.TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY: _on_prepare_type_hierarchy,
977
+ lsp.TYPE_HIERARCHY_SUPERTYPES: _on_type_hierarchy_supertypes,
978
+ lsp.TYPE_HIERARCHY_SUBTYPES: _on_type_hierarchy_subtypes,
979
+ lsp.WORKSPACE_SYMBOL: _on_workspace_symbol,
750
980
  }
751
981
  )
752
982
 
@@ -319,25 +319,47 @@ class TestServerInternals:
319
319
  from java_functional_lsp.server import _JDTLS_REG_PREFIX, _build_jdtls_registrations
320
320
 
321
321
  regs = _build_jdtls_registrations()
322
- assert len(regs) == 5
322
+ assert len(regs) == 14
323
323
  methods = {r.method for r in regs}
324
324
  assert lsp.TEXT_DOCUMENT_HOVER in methods
325
325
  assert lsp.TEXT_DOCUMENT_DEFINITION in methods
326
326
  assert lsp.TEXT_DOCUMENT_REFERENCES in methods
327
327
  assert lsp.TEXT_DOCUMENT_COMPLETION in methods
328
328
  assert lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL in methods
329
+ assert lsp.TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY in methods
330
+ assert lsp.TEXT_DOCUMENT_SIGNATURE_HELP in methods
331
+ assert lsp.TEXT_DOCUMENT_IMPLEMENTATION in methods
332
+ assert lsp.TEXT_DOCUMENT_TYPE_DEFINITION in methods
333
+ assert lsp.TEXT_DOCUMENT_DECLARATION in methods
334
+ assert lsp.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT in methods
335
+ assert lsp.TEXT_DOCUMENT_RENAME in methods
336
+ assert lsp.TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY in methods
337
+ assert lsp.WORKSPACE_SYMBOL in methods
329
338
  # All IDs are unique and use the shared prefix
330
339
  ids = {r.id for r in regs}
331
- assert len(ids) == 5
340
+ assert len(ids) == 14
332
341
  assert all(rid.startswith(_JDTLS_REG_PREFIX) for rid in ids)
333
- # All have java document selector with correct language
342
+ # Document-scoped capabilities have java document selector; workspace-scoped do not
343
+ workspace_scoped = {lsp.WORKSPACE_SYMBOL}
334
344
  for r in regs:
335
345
  assert r.register_options is not None
336
- selectors = r.register_options["documentSelector"]
337
- assert any(s.get("language") == "java" for s in selectors)
346
+ if r.method in workspace_scoped:
347
+ assert "documentSelector" not in r.register_options
348
+ else:
349
+ selectors = r.register_options["documentSelector"]
350
+ assert any(s.get("language") == "java" for s in selectors)
338
351
  # Completion has triggerCharacters
339
352
  comp = next(r for r in regs if r.method == lsp.TEXT_DOCUMENT_COMPLETION)
353
+ assert comp.register_options is not None
340
354
  assert comp.register_options.get("triggerCharacters") == ["."]
355
+ # SignatureHelp has triggerCharacters
356
+ sig = next(r for r in regs if r.method == lsp.TEXT_DOCUMENT_SIGNATURE_HELP)
357
+ assert sig.register_options is not None
358
+ assert sig.register_options.get("triggerCharacters") == ["(", ","]
359
+ # Rename advertises prepareProvider so the IDE sends textDocument/prepareRename
360
+ rename = next(r for r in regs if r.method == lsp.TEXT_DOCUMENT_RENAME)
361
+ assert rename.register_options is not None
362
+ assert rename.register_options.get("prepareProvider") is True
341
363
 
342
364
  async def test_register_jdtls_capabilities_logs_on_failure(self, caplog: Any) -> None:
343
365
  """_register_jdtls_capabilities logs a warning when the client rejects."""
@@ -386,16 +408,30 @@ class TestServerInternals:
386
408
  await srv_mod._register_jdtls_capabilities()
387
409
  finally:
388
410
  srv_mod._jdtls_capabilities_registered = old_flag
389
- # Handlers were registered for all 5 methods
411
+ # Handlers were registered for all methods (including call hierarchy)
390
412
  assert lsp.TEXT_DOCUMENT_HOVER in registered_methods
391
413
  assert lsp.TEXT_DOCUMENT_COMPLETION in registered_methods
392
414
  assert lsp.TEXT_DOCUMENT_DEFINITION in registered_methods
393
415
  assert lsp.TEXT_DOCUMENT_REFERENCES in registered_methods
394
416
  assert lsp.TEXT_DOCUMENT_DOCUMENT_SYMBOL in registered_methods
417
+ assert lsp.TEXT_DOCUMENT_PREPARE_CALL_HIERARCHY in registered_methods
418
+ assert lsp.CALL_HIERARCHY_INCOMING_CALLS in registered_methods
419
+ assert lsp.CALL_HIERARCHY_OUTGOING_CALLS in registered_methods
420
+ assert lsp.TEXT_DOCUMENT_SIGNATURE_HELP in registered_methods
421
+ assert lsp.TEXT_DOCUMENT_IMPLEMENTATION in registered_methods
422
+ assert lsp.TEXT_DOCUMENT_TYPE_DEFINITION in registered_methods
423
+ assert lsp.TEXT_DOCUMENT_DECLARATION in registered_methods
424
+ assert lsp.TEXT_DOCUMENT_DOCUMENT_HIGHLIGHT in registered_methods
425
+ assert lsp.TEXT_DOCUMENT_RENAME in registered_methods
426
+ assert lsp.TEXT_DOCUMENT_PREPARE_RENAME in registered_methods
427
+ assert lsp.TEXT_DOCUMENT_PREPARE_TYPE_HIERARCHY in registered_methods
428
+ assert lsp.TYPE_HIERARCHY_SUPERTYPES in registered_methods
429
+ assert lsp.TYPE_HIERARCHY_SUBTYPES in registered_methods
430
+ assert lsp.WORKSPACE_SYMBOL in registered_methods
395
431
  # client_register_capability_async was called
396
432
  mock_reg.assert_called_once()
397
433
  # Success log emitted
398
- assert any("Dynamically registered" in r.getMessage() for r in caplog.records)
434
+ assert any("jdtls capabilities registered" in r.getMessage() for r in caplog.records)
399
435
 
400
436
  async def test_register_jdtls_capabilities_idempotent(self) -> None:
401
437
  """Second call is a no-op (idempotency guard)."""
@@ -505,7 +541,7 @@ class TestServerInternals:
505
541
  mock_add = AsyncMock(return_value="file:///mod")
506
542
  mock_send = AsyncMock(side_effect=[None, {"result": "ok"}])
507
543
 
508
- async def mock_wait(uri: str, timeout: float = 30.0) -> bool:
544
+ async def mock_wait(uri: str, timeout: float = 30.0) -> bool: # pyright: ignore[reportUnusedParameter]
509
545
  srv._proxy.modules.mark_ready(uri)
510
546
  return True
511
547
 
@@ -545,6 +581,530 @@ class TestServerInternals:
545
581
  finally:
546
582
  srv._proxy.modules.clear()
547
583
 
584
+ async def test_on_prepare_call_hierarchy_forwards_to_jdtls(self) -> None:
585
+ """_on_prepare_call_hierarchy forwards request and structures result."""
586
+ from unittest.mock import AsyncMock, patch
587
+
588
+ from java_functional_lsp.server import _on_prepare_call_hierarchy
589
+
590
+ item = {
591
+ "name": "parsePrimaryEnrichmentResponse",
592
+ "kind": 6,
593
+ "uri": "file:///mod/Foo.java",
594
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 10}},
595
+ "selectionRange": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 10}},
596
+ }
597
+ params = lsp.CallHierarchyPrepareParams(
598
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
599
+ position=lsp.Position(line=0, character=0),
600
+ )
601
+ mock_forward = AsyncMock(return_value=[item])
602
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
603
+ result = await _on_prepare_call_hierarchy(params)
604
+ assert result is not None
605
+ assert len(result) == 1
606
+ assert result[0].name == "parsePrimaryEnrichmentResponse"
607
+ mock_forward.assert_called_once_with("textDocument/prepareCallHierarchy", params, "file:///mod/Foo.java")
608
+
609
+ async def test_on_prepare_call_hierarchy_returns_none_on_null(self) -> None:
610
+ from unittest.mock import AsyncMock, patch
611
+
612
+ from java_functional_lsp.server import _on_prepare_call_hierarchy
613
+
614
+ params = lsp.CallHierarchyPrepareParams(
615
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
616
+ position=lsp.Position(line=0, character=0),
617
+ )
618
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
619
+ result = await _on_prepare_call_hierarchy(params)
620
+ assert result is None
621
+
622
+ async def test_on_incoming_calls_forwards_using_item_uri(self) -> None:
623
+ """_on_incoming_calls uses params.item.uri as the file_uri for module resolution."""
624
+ from unittest.mock import AsyncMock, patch
625
+
626
+ from java_functional_lsp.server import _on_incoming_calls
627
+
628
+ item = lsp.CallHierarchyItem(
629
+ name="caller",
630
+ kind=lsp.SymbolKind.Method,
631
+ uri="file:///mod/Foo.java",
632
+ range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=5)),
633
+ selection_range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=5)),
634
+ )
635
+ params = lsp.CallHierarchyIncomingCallsParams(item=item)
636
+ incoming = {
637
+ "from": {
638
+ "name": "caller",
639
+ "kind": 6,
640
+ "uri": "file:///mod/Foo.java",
641
+ "range": {"start": {"line": 1, "character": 0}, "end": {"line": 1, "character": 6}},
642
+ "selectionRange": {"start": {"line": 1, "character": 0}, "end": {"line": 1, "character": 6}},
643
+ },
644
+ "fromRanges": [{"start": {"line": 1, "character": 0}, "end": {"line": 1, "character": 6}}],
645
+ }
646
+ mock_forward = AsyncMock(return_value=[incoming])
647
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
648
+ result = await _on_incoming_calls(params)
649
+ assert result is not None
650
+ assert len(result) == 1
651
+ mock_forward.assert_called_once_with("callHierarchy/incomingCalls", params, "file:///mod/Foo.java")
652
+
653
+ async def test_on_incoming_calls_returns_none_on_null(self) -> None:
654
+ from unittest.mock import AsyncMock, patch
655
+
656
+ from java_functional_lsp.server import _on_incoming_calls
657
+
658
+ item = lsp.CallHierarchyItem(
659
+ name="x",
660
+ kind=lsp.SymbolKind.Method,
661
+ uri="file:///mod/Foo.java",
662
+ range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=1)),
663
+ selection_range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=1)),
664
+ )
665
+ params = lsp.CallHierarchyIncomingCallsParams(item=item)
666
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
667
+ result = await _on_incoming_calls(params)
668
+ assert result is None
669
+
670
+ async def test_on_outgoing_calls_forwards_using_item_uri(self) -> None:
671
+ """_on_outgoing_calls uses params.item.uri as the file_uri for module resolution."""
672
+ from unittest.mock import AsyncMock, patch
673
+
674
+ from java_functional_lsp.server import _on_outgoing_calls
675
+
676
+ item = lsp.CallHierarchyItem(
677
+ name="callee",
678
+ kind=lsp.SymbolKind.Method,
679
+ uri="file:///mod/Bar.java",
680
+ range=lsp.Range(start=lsp.Position(line=5, character=0), end=lsp.Position(line=5, character=6)),
681
+ selection_range=lsp.Range(start=lsp.Position(line=5, character=0), end=lsp.Position(line=5, character=6)),
682
+ )
683
+ params = lsp.CallHierarchyOutgoingCallsParams(item=item)
684
+ outgoing = {
685
+ "to": {
686
+ "name": "callee",
687
+ "kind": 6,
688
+ "uri": "file:///mod/Bar.java",
689
+ "range": {"start": {"line": 5, "character": 0}, "end": {"line": 5, "character": 6}},
690
+ "selectionRange": {"start": {"line": 5, "character": 0}, "end": {"line": 5, "character": 6}},
691
+ },
692
+ "fromRanges": [{"start": {"line": 5, "character": 0}, "end": {"line": 5, "character": 6}}],
693
+ }
694
+ mock_forward = AsyncMock(return_value=[outgoing])
695
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
696
+ result = await _on_outgoing_calls(params)
697
+ assert result is not None
698
+ assert len(result) == 1
699
+ mock_forward.assert_called_once_with("callHierarchy/outgoingCalls", params, "file:///mod/Bar.java")
700
+
701
+ async def test_on_signature_help_forwards_to_jdtls(self) -> None:
702
+ """_on_signature_help forwards and structures a SignatureHelp result."""
703
+ from unittest.mock import AsyncMock, patch
704
+
705
+ from java_functional_lsp.server import _on_signature_help
706
+
707
+ params = lsp.SignatureHelpParams(
708
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
709
+ position=lsp.Position(line=5, character=10),
710
+ )
711
+ raw = {"signatures": [{"label": "foo(int x)"}], "activeSignature": 0}
712
+ mock_forward = AsyncMock(return_value=raw)
713
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
714
+ result = await _on_signature_help(params)
715
+ assert result is not None
716
+ mock_forward.assert_called_once_with("textDocument/signatureHelp", params, "file:///mod/Foo.java")
717
+
718
+ async def test_on_signature_help_returns_none_on_null(self) -> None:
719
+ from unittest.mock import AsyncMock, patch
720
+
721
+ from java_functional_lsp.server import _on_signature_help
722
+
723
+ params = lsp.SignatureHelpParams(
724
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
725
+ position=lsp.Position(line=0, character=0),
726
+ )
727
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
728
+ assert await _on_signature_help(params) is None
729
+
730
+ async def test_on_implementation_forwards_using_text_document_uri(self) -> None:
731
+ """_on_implementation uses params.text_document.uri and handles list result."""
732
+ from unittest.mock import AsyncMock, patch
733
+
734
+ from java_functional_lsp.server import _on_implementation
735
+
736
+ params = lsp.ImplementationParams(
737
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
738
+ position=lsp.Position(line=0, character=0),
739
+ )
740
+ loc = {
741
+ "uri": "file:///mod/Impl.java",
742
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 5}},
743
+ }
744
+ mock_forward = AsyncMock(return_value=[loc])
745
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
746
+ result = await _on_implementation(params)
747
+ assert result is not None
748
+ assert len(result) == 1
749
+ mock_forward.assert_called_once_with("textDocument/implementation", params, "file:///mod/Foo.java")
750
+
751
+ async def test_on_implementation_returns_none_on_null(self) -> None:
752
+ from unittest.mock import AsyncMock, patch
753
+
754
+ from java_functional_lsp.server import _on_implementation
755
+
756
+ params = lsp.ImplementationParams(
757
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
758
+ position=lsp.Position(line=0, character=0),
759
+ )
760
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
761
+ assert await _on_implementation(params) is None
762
+
763
+ async def test_on_document_highlight_forwards_to_jdtls(self) -> None:
764
+ """_on_document_highlight forwards and structures list result."""
765
+ from unittest.mock import AsyncMock, patch
766
+
767
+ from java_functional_lsp.server import _on_document_highlight
768
+
769
+ params = lsp.DocumentHighlightParams(
770
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
771
+ position=lsp.Position(line=0, character=0),
772
+ )
773
+ raw = [{"range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 3}}}]
774
+ mock_forward = AsyncMock(return_value=raw)
775
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
776
+ result = await _on_document_highlight(params)
777
+ assert result is not None
778
+ assert len(result) == 1
779
+ mock_forward.assert_called_once_with("textDocument/documentHighlight", params, "file:///mod/Foo.java")
780
+
781
+ async def test_on_rename_forwards_and_returns_workspace_edit(self) -> None:
782
+ """_on_rename forwards and structures a WorkspaceEdit."""
783
+ from unittest.mock import AsyncMock, patch
784
+
785
+ from java_functional_lsp.server import _on_rename
786
+
787
+ params = lsp.RenameParams(
788
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
789
+ position=lsp.Position(line=0, character=0),
790
+ new_name="Bar",
791
+ )
792
+ raw: dict[str, object] = {"changes": {}}
793
+ mock_forward = AsyncMock(return_value=raw)
794
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
795
+ result = await _on_rename(params)
796
+ assert result is not None
797
+ mock_forward.assert_called_once_with("textDocument/rename", params, "file:///mod/Foo.java")
798
+
799
+ async def test_on_rename_returns_none_on_null(self) -> None:
800
+ from unittest.mock import AsyncMock, patch
801
+
802
+ from java_functional_lsp.server import _on_rename
803
+
804
+ params = lsp.RenameParams(
805
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
806
+ position=lsp.Position(line=0, character=0),
807
+ new_name="Bar",
808
+ )
809
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
810
+ assert await _on_rename(params) is None
811
+
812
+ async def test_on_prepare_type_hierarchy_forwards_to_jdtls(self) -> None:
813
+ """_on_prepare_type_hierarchy forwards and structures TypeHierarchyItem list."""
814
+ from unittest.mock import AsyncMock, patch
815
+
816
+ from java_functional_lsp.server import _on_prepare_type_hierarchy
817
+
818
+ params = lsp.TypeHierarchyPrepareParams(
819
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
820
+ position=lsp.Position(line=0, character=0),
821
+ )
822
+ item = {
823
+ "name": "Foo",
824
+ "kind": 5,
825
+ "uri": "file:///mod/Foo.java",
826
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 3}},
827
+ "selectionRange": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 3}},
828
+ }
829
+ mock_forward = AsyncMock(return_value=[item])
830
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
831
+ result = await _on_prepare_type_hierarchy(params)
832
+ assert result is not None
833
+ assert len(result) == 1
834
+ assert result[0].name == "Foo"
835
+ mock_forward.assert_called_once_with("textDocument/prepareTypeHierarchy", params, "file:///mod/Foo.java")
836
+
837
+ async def test_on_type_hierarchy_supertypes_uses_item_uri(self) -> None:
838
+ """_on_type_hierarchy_supertypes uses params.item.uri for module resolution."""
839
+ from unittest.mock import AsyncMock, patch
840
+
841
+ from java_functional_lsp.server import _on_type_hierarchy_supertypes
842
+
843
+ item = lsp.TypeHierarchyItem(
844
+ name="Foo",
845
+ kind=lsp.SymbolKind.Class,
846
+ uri="file:///mod/Foo.java",
847
+ range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=3)),
848
+ selection_range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=3)),
849
+ )
850
+ params = lsp.TypeHierarchySupertypesParams(item=item)
851
+ parent = {
852
+ "name": "Base",
853
+ "kind": 5,
854
+ "uri": "file:///mod/Base.java",
855
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 4}},
856
+ "selectionRange": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 4}},
857
+ }
858
+ mock_forward = AsyncMock(return_value=[parent])
859
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
860
+ result = await _on_type_hierarchy_supertypes(params)
861
+ assert result is not None
862
+ assert len(result) == 1
863
+ assert result[0].name == "Base"
864
+ mock_forward.assert_called_once_with("typeHierarchy/supertypes", params, "file:///mod/Foo.java")
865
+
866
+ async def test_on_type_hierarchy_subtypes_uses_item_uri(self) -> None:
867
+ """_on_type_hierarchy_subtypes uses params.item.uri for module resolution."""
868
+ from unittest.mock import AsyncMock, patch
869
+
870
+ from java_functional_lsp.server import _on_type_hierarchy_subtypes
871
+
872
+ item = lsp.TypeHierarchyItem(
873
+ name="Base",
874
+ kind=lsp.SymbolKind.Class,
875
+ uri="file:///mod/Base.java",
876
+ range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=4)),
877
+ selection_range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=4)),
878
+ )
879
+ params = lsp.TypeHierarchySubtypesParams(item=item)
880
+ child = {
881
+ "name": "Impl",
882
+ "kind": 5,
883
+ "uri": "file:///mod/Impl.java",
884
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 4}},
885
+ "selectionRange": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 4}},
886
+ }
887
+ mock_forward = AsyncMock(return_value=[child])
888
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
889
+ result = await _on_type_hierarchy_subtypes(params)
890
+ assert result is not None
891
+ assert len(result) == 1
892
+ assert result[0].name == "Impl"
893
+ mock_forward.assert_called_once_with("typeHierarchy/subtypes", params, "file:///mod/Base.java")
894
+
895
+ async def test_on_type_definition_forwards_to_jdtls(self) -> None:
896
+ """_on_type_definition uses params.text_document.uri and handles list result."""
897
+ from unittest.mock import AsyncMock, patch
898
+
899
+ from java_functional_lsp.server import _on_type_definition
900
+
901
+ params = lsp.TypeDefinitionParams(
902
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
903
+ position=lsp.Position(line=0, character=0),
904
+ )
905
+ loc = {
906
+ "uri": "file:///mod/FooType.java",
907
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 7}},
908
+ }
909
+ mock_forward = AsyncMock(return_value=[loc])
910
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
911
+ result = await _on_type_definition(params)
912
+ assert result is not None
913
+ assert len(result) == 1
914
+ mock_forward.assert_called_once_with("textDocument/typeDefinition", params, "file:///mod/Foo.java")
915
+
916
+ async def test_on_declaration_forwards_to_jdtls(self) -> None:
917
+ """_on_declaration uses params.text_document.uri and handles list result."""
918
+ from unittest.mock import AsyncMock, patch
919
+
920
+ from java_functional_lsp.server import _on_declaration
921
+
922
+ params = lsp.DeclarationParams(
923
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
924
+ position=lsp.Position(line=0, character=0),
925
+ )
926
+ loc = {
927
+ "uri": "file:///mod/IFoo.java",
928
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 4}},
929
+ }
930
+ mock_forward = AsyncMock(return_value=[loc])
931
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
932
+ result = await _on_declaration(params)
933
+ assert result is not None
934
+ assert len(result) == 1
935
+ mock_forward.assert_called_once_with("textDocument/declaration", params, "file:///mod/Foo.java")
936
+
937
+ async def test_on_prepare_rename_forwards_to_jdtls(self) -> None:
938
+ """_on_prepare_rename forwards and structures a Range result."""
939
+ from unittest.mock import AsyncMock, patch
940
+
941
+ from java_functional_lsp.server import _on_prepare_rename
942
+
943
+ params = lsp.PrepareRenameParams(
944
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
945
+ position=lsp.Position(line=3, character=5),
946
+ )
947
+ raw = {"start": {"line": 3, "character": 4}, "end": {"line": 3, "character": 7}}
948
+ mock_forward = AsyncMock(return_value=raw)
949
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
950
+ result = await _on_prepare_rename(params)
951
+ assert result is not None
952
+ mock_forward.assert_called_once_with("textDocument/prepareRename", params, "file:///mod/Foo.java")
953
+
954
+ async def test_on_workspace_symbol_uses_proxy_directly(self) -> None:
955
+ """_on_workspace_symbol bypasses _ensure_module_and_forward and calls proxy.send_request."""
956
+ from unittest.mock import AsyncMock, patch
957
+
958
+ from java_functional_lsp.server import _on_workspace_symbol
959
+ from java_functional_lsp.server import server as srv
960
+
961
+ params = lsp.WorkspaceSymbolParams(query="Foo")
962
+ raw = [
963
+ {
964
+ "name": "FooService",
965
+ "kind": 5,
966
+ "location": {
967
+ "uri": "file:///mod/FooService.java",
968
+ "range": {"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 10}},
969
+ },
970
+ }
971
+ ]
972
+ mock_send = AsyncMock(return_value=raw)
973
+ with (
974
+ patch.object(srv._proxy, "_available", True),
975
+ patch.object(srv._proxy, "send_request", mock_send),
976
+ ):
977
+ result = await _on_workspace_symbol(params)
978
+ assert result is not None
979
+ assert len(result) == 1
980
+ assert result[0].name == "FooService"
981
+ mock_send.assert_called_once()
982
+ assert mock_send.call_args[0][0] == "workspace/symbol"
983
+
984
+ async def test_on_workspace_symbol_returns_none_when_unavailable(self) -> None:
985
+ """_on_workspace_symbol returns None when jdtls is not available."""
986
+ from unittest.mock import patch
987
+
988
+ from java_functional_lsp.server import _on_workspace_symbol
989
+ from java_functional_lsp.server import server as srv
990
+
991
+ params = lsp.WorkspaceSymbolParams(query="anything")
992
+ with patch.object(srv._proxy, "_available", False):
993
+ result = await _on_workspace_symbol(params)
994
+ assert result is None
995
+
996
+ async def test_on_document_highlight_returns_none_on_null(self) -> None:
997
+ from unittest.mock import AsyncMock, patch
998
+
999
+ from java_functional_lsp.server import _on_document_highlight
1000
+
1001
+ params = lsp.DocumentHighlightParams(
1002
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
1003
+ position=lsp.Position(line=0, character=0),
1004
+ )
1005
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
1006
+ assert await _on_document_highlight(params) is None
1007
+
1008
+ async def test_on_type_definition_returns_none_on_null(self) -> None:
1009
+ from unittest.mock import AsyncMock, patch
1010
+
1011
+ from java_functional_lsp.server import _on_type_definition
1012
+
1013
+ params = lsp.TypeDefinitionParams(
1014
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
1015
+ position=lsp.Position(line=0, character=0),
1016
+ )
1017
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
1018
+ assert await _on_type_definition(params) is None
1019
+
1020
+ async def test_on_declaration_returns_none_on_null(self) -> None:
1021
+ from unittest.mock import AsyncMock, patch
1022
+
1023
+ from java_functional_lsp.server import _on_declaration
1024
+
1025
+ params = lsp.DeclarationParams(
1026
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
1027
+ position=lsp.Position(line=0, character=0),
1028
+ )
1029
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
1030
+ assert await _on_declaration(params) is None
1031
+
1032
+ async def test_on_prepare_rename_returns_none_on_null(self) -> None:
1033
+ from unittest.mock import AsyncMock, patch
1034
+
1035
+ from java_functional_lsp.server import _on_prepare_rename
1036
+
1037
+ params = lsp.PrepareRenameParams(
1038
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
1039
+ position=lsp.Position(line=0, character=0),
1040
+ )
1041
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
1042
+ assert await _on_prepare_rename(params) is None
1043
+
1044
+ async def test_on_prepare_rename_returns_placeholder_when_jdtls_sends_placeholder(self) -> None:
1045
+ """_on_prepare_rename handles PrepareRenamePlaceholder (the typical jdtls response)."""
1046
+ from unittest.mock import AsyncMock, patch
1047
+
1048
+ from java_functional_lsp.server import _on_prepare_rename
1049
+
1050
+ params = lsp.PrepareRenameParams(
1051
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
1052
+ position=lsp.Position(line=3, character=5),
1053
+ )
1054
+ raw = {
1055
+ "range": {"start": {"line": 3, "character": 4}, "end": {"line": 3, "character": 7}},
1056
+ "placeholder": "foo",
1057
+ }
1058
+ mock_forward = AsyncMock(return_value=raw)
1059
+ with patch("java_functional_lsp.server._ensure_module_and_forward", mock_forward):
1060
+ result = await _on_prepare_rename(params)
1061
+ assert isinstance(result, lsp.PrepareRenamePlaceholder)
1062
+ assert result.placeholder == "foo"
1063
+
1064
+ async def test_on_prepare_type_hierarchy_returns_none_on_null(self) -> None:
1065
+ from unittest.mock import AsyncMock, patch
1066
+
1067
+ from java_functional_lsp.server import _on_prepare_type_hierarchy
1068
+
1069
+ params = lsp.TypeHierarchyPrepareParams(
1070
+ text_document=lsp.TextDocumentIdentifier(uri="file:///mod/Foo.java"),
1071
+ position=lsp.Position(line=0, character=0),
1072
+ )
1073
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
1074
+ assert await _on_prepare_type_hierarchy(params) is None
1075
+
1076
+ async def test_on_type_hierarchy_supertypes_returns_none_on_null(self) -> None:
1077
+ from unittest.mock import AsyncMock, patch
1078
+
1079
+ from java_functional_lsp.server import _on_type_hierarchy_supertypes
1080
+
1081
+ item = lsp.TypeHierarchyItem(
1082
+ name="Foo",
1083
+ kind=lsp.SymbolKind.Class,
1084
+ uri="file:///mod/Foo.java",
1085
+ range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=3)),
1086
+ selection_range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=3)),
1087
+ )
1088
+ params = lsp.TypeHierarchySupertypesParams(item=item)
1089
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
1090
+ assert await _on_type_hierarchy_supertypes(params) is None
1091
+
1092
+ async def test_on_type_hierarchy_subtypes_returns_none_on_null(self) -> None:
1093
+ from unittest.mock import AsyncMock, patch
1094
+
1095
+ from java_functional_lsp.server import _on_type_hierarchy_subtypes
1096
+
1097
+ item = lsp.TypeHierarchyItem(
1098
+ name="Base",
1099
+ kind=lsp.SymbolKind.Class,
1100
+ uri="file:///mod/Base.java",
1101
+ range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=4)),
1102
+ selection_range=lsp.Range(start=lsp.Position(line=0, character=0), end=lsp.Position(line=0, character=4)),
1103
+ )
1104
+ params = lsp.TypeHierarchySubtypesParams(item=item)
1105
+ with patch("java_functional_lsp.server._ensure_module_and_forward", AsyncMock(return_value=None)):
1106
+ assert await _on_type_hierarchy_subtypes(params) is None
1107
+
548
1108
  def test_serialize_params_camelcase(self) -> None:
549
1109
  from java_functional_lsp.server import _serialize_params
550
1110
 
@@ -611,7 +1171,11 @@ class TestServerInternals:
611
1171
  finally:
612
1172
  server.workspace.remove_text_document(uri)
613
1173
  assert result is not None
614
- assert any("Try.of" in e.new_text for e in result[0].edit.changes[uri])
1174
+ ca = result[0]
1175
+ assert isinstance(ca, lsp.CodeAction)
1176
+ assert ca.edit is not None
1177
+ assert ca.edit.changes is not None
1178
+ assert any("Try.of" in e.new_text for e in ca.edit.changes[uri])
615
1179
 
616
1180
  def test_code_action_filters_foreign(self) -> None:
617
1181
  from java_functional_lsp.server import on_code_action, server
@@ -897,7 +1461,7 @@ class TestFindLombokJar:
897
1461
  result = _find_lombok_jar()
898
1462
  assert result is None
899
1463
 
900
- def test_warns_on_missing_config_path(self, tmp_path: Any, caplog: Any) -> None:
1464
+ def test_warns_on_missing_config_path(self, caplog: Any) -> None:
901
1465
  import logging
902
1466
 
903
1467
  from java_functional_lsp.proxy import _find_lombok_jar
@@ -1027,6 +1591,7 @@ class TestLspLifecycle:
1027
1591
  assert actions is not None
1028
1592
  assert len(actions) >= 1
1029
1593
  action = actions[0]
1594
+ assert isinstance(action, lsp.CodeAction)
1030
1595
  assert action.title == "Replace with Option.none()"
1031
1596
  assert action.kind == lsp.CodeActionKind.QuickFix
1032
1597
  assert action.edit is not None
@@ -1053,6 +1618,7 @@ class TestLspLifecycle:
1053
1618
  assert actions is not None
1054
1619
  assert len(actions) >= 1
1055
1620
  action = actions[0]
1621
+ assert isinstance(action, lsp.CodeAction)
1056
1622
  assert action.title == "Convert try/catch to Try monadic flow"
1057
1623
  assert action.edit is not None
1058
1624
  assert action.edit.changes is not None
@@ -184,7 +184,7 @@ wheels = [
184
184
 
185
185
  [[package]]
186
186
  name = "java-functional-lsp"
187
- version = "0.9.0"
187
+ version = "0.9.6"
188
188
  source = { editable = "." }
189
189
  dependencies = [
190
190
  { name = "pygls" },