From d13e1744e8e32eab7b3d2482192bc7d78c1491a6 Mon Sep 17 00:00:00 2001 From: Don Syme Date: Fri, 10 Oct 2025 12:01:45 +0100 Subject: [PATCH] format --- README.md | 2 +- .../fsharp_language_server.py | 56 +++++++-------- test/solidlsp/fsharp/test_fsharp_basic.py | 68 ++++++++----------- 3 files changed, 57 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index f5717a0f..5cabed00 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ With Serena, we provide direct, out-of-the-box support for: * C/C++ (you may experience issues with finding references, we are working on it) * Zig (requires installation of ZLS - Zig Language Server) * C# - * F# (requires .NET SDK 6.0+ and uses FsAutoComplete/Ionide LSP server) + * F# (requires .NET SDK 8.0+ and uses FsAutoComplete/Ionide LSP server) * Ruby (by default, uses [ruby-lsp](https://github.com/Shopify/ruby-lsp), specify ruby_solargraph as your language to use the previous solargraph based implementation) * Swift * Kotlin (uses the pre-alpha [official kotlin LS](https://github.com/Kotlin/kotlin-lsp), some issues may appear) diff --git a/src/solidlsp/language_servers/fsharp_language_server.py b/src/solidlsp/language_servers/fsharp_language_server.py index e436f230..4cddb99f 100644 --- a/src/solidlsp/language_servers/fsharp_language_server.py +++ b/src/solidlsp/language_servers/fsharp_language_server.py @@ -78,6 +78,7 @@ class FSharpLanguageServer(SolidLanguageServer): # Verify dotnet version import subprocess + try: result = subprocess.run([dotnet_exe, "--version"], capture_output=True, text=True, check=True) logger.log(f"Found .NET SDK version: {result.stdout.strip()}", logging.INFO) @@ -105,19 +106,20 @@ class FSharpLanguageServer(SolidLanguageServer): if not os.path.exists(fsautocomplete_path): logger.log(f"FsAutoComplete executable not found at {fsautocomplete_path}. Installing...", logging.INFO) - + # Ensure the directory exists os.makedirs(fsharp_ls_dir, exist_ok=True) - + # Install FsAutoComplete using dotnet tool install try: import subprocess + result = subprocess.run( [dotnet_exe, "tool", "install", "--tool-path", fsharp_ls_dir, "fsautocomplete"], cwd=fsharp_ls_dir, capture_output=True, text=True, - check=True + check=True, ) logger.log("FsAutoComplete installed successfully", logging.INFO) logger.log(f"Installation output: {result.stdout}", logging.DEBUG) @@ -138,7 +140,7 @@ class FSharpLanguageServer(SolidLanguageServer): Returns the initialize params for the F# Language Server. """ root_uri = pathlib.Path(self.repository_root_path).as_uri() - + initialize_params = { "processId": os.getpid(), "rootPath": self.repository_root_path, @@ -236,33 +238,25 @@ class FSharpLanguageServer(SolidLanguageServer): "automaticWorkspaceInit": True, "abstractClassStubGeneration": True, "abstractClassStubGenerationObjectIdentifier": "this", - "abstractClassStubGenerationMethodBody": "failwith \"Not Implemented\"", + "abstractClassStubGenerationMethodBody": 'failwith "Not Implemented"', "addFsiWatcher": False, - "codeLenses": { - "signature": {"enabled": True}, - "references": {"enabled": True} - }, + "codeLenses": {"signature": {"enabled": True}, "references": {"enabled": True}}, "disableInMemoryProjectReferences": False, "dotNetRoot": self._get_dotnet_root(), "enableMSBuildProjectGraph": False, "excludeProjectDirectories": ["paket-files"], "externalAutocomplete": False, - "fsac": { - "attachDebugger": False, - "silencedLogs": [], - "conserveMemory": False, - "netCoreDllPath": "" - }, + "fsac": {"attachDebugger": False, "silencedLogs": [], "conserveMemory": False, "netCoreDllPath": ""}, "fsiExtraParameters": [], "generateBinlog": False, "interfaceStubGeneration": True, "interfaceStubGenerationObjectIdentifier": "this", - "interfaceStubGenerationMethodBody": "failwith \"Not Implemented\"", + "interfaceStubGenerationMethodBody": 'failwith "Not Implemented"', "keywordsAutocomplete": True, "linter": True, "pipelineHints": {"enabled": True}, "recordStubGeneration": True, - "recordStubGenerationBody": "failwith \"Not Implemented\"", + "recordStubGenerationBody": 'failwith "Not Implemented"', "resolveNamespaces": True, "saveOnlyOpenFiles": False, "showProjectExplorerIn": ["ionide", "solution"], @@ -271,12 +265,12 @@ class FSharpLanguageServer(SolidLanguageServer): "suggestGitignore": True, "suggestSdkScripts": True, "unionCaseStubGeneration": True, - "unionCaseStubGenerationBody": "failwith \"Not Implemented\"", + "unionCaseStubGenerationBody": 'failwith "Not Implemented"', "unusedDeclarationsAnalyzer": True, "unusedOpensAnalyzer": True, "verboseLogging": False, "workspaceModePeekDeepLevel": 2, - "workspacePath": self.repository_root_path + "workspacePath": self.repository_root_path, }, "trace": "off", } @@ -292,46 +286,48 @@ class FSharpLanguageServer(SolidLanguageServer): # Try to get the installation path try: import subprocess + result = subprocess.run([dotnet_exe, "--info"], capture_output=True, text=True, check=True) - lines = result.stdout.split('\n') + lines = result.stdout.split("\n") for line in lines: - if 'Base Path:' in line or 'Base path:' in line: - base_path = line.split(':', 1)[1].strip() + if "Base Path:" in line or "Base path:" in line: + base_path = line.split(":", 1)[1].strip() # Get the parent directory (remove 'sdk/version' part) return str(Path(base_path).parent.parent) except (subprocess.CalledProcessError, Exception): pass - + # Fallback: use the directory containing dotnet executable if dotnet_exe: return str(Path(dotnet_exe).parent) - + return "" def _start_server(self): """ Start the F# Language Server with custom handlers. """ + def handle_window_log_message(params): """Handle window/logMessage from the LSP server.""" message = params.get("message", "") message_type = params.get("type", 1) - + # Map LSP log levels to Python logging levels level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG} level = level_map.get(message_type, logging.INFO) - + self.logger.log(f"FsAutoComplete: {message}", level) def handle_window_show_message(params): """Handle window/showMessage from the LSP server.""" message = params.get("message", "") message_type = params.get("type", 1) - + # Map LSP message types to Python logging levels level_map = {1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG} level = level_map.get(message_type, logging.INFO) - + self.logger.log(f"FsAutoComplete Message: {message}", level) def handle_workspace_configuration(params): @@ -347,7 +343,7 @@ class FSharpLanguageServer(SolidLanguageServer): def handle_client_unregister_capability(params): """Handle client/unregisterCapability requests from the LSP server.""" - # For now, just acknowledge the unregistration + # For now, just acknowledge the unregistration return None def handle_work_done_progress_create(params): @@ -391,4 +387,4 @@ class FSharpLanguageServer(SolidLanguageServer): """ F# projects can be large and may need more time for cross-file analysis. """ - return 15.0 # 15 seconds should be sufficient for most F# projects \ No newline at end of file + return 15.0 # 15 seconds should be sufficient for most F# projects diff --git a/test/solidlsp/fsharp/test_fsharp_basic.py b/test/solidlsp/fsharp/test_fsharp_basic.py index c04b40fb..192f3638 100644 --- a/test/solidlsp/fsharp/test_fsharp_basic.py +++ b/test/solidlsp/fsharp/test_fsharp_basic.py @@ -19,11 +19,11 @@ class TestFSharpLanguageServer: def test_find_symbol(self, language_server: SolidLanguageServer) -> None: """Test finding symbols in the full symbol tree.""" symbols = language_server.request_full_symbol_tree() - + # Check for main program module symbols assert SymbolUtils.symbol_tree_contains_name(symbols, "Program"), "Program module not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "main function not found in symbol tree" - + # Check for Calculator module symbols assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator module not found in symbol tree" assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add function not found in symbol tree" @@ -62,7 +62,7 @@ class TestFSharpLanguageServer: # Look for expected functions symbol_names = [s.get("name") for s in symbols] expected_symbols = ["add", "subtract", "multiply", "divide", "square", "factorial", "CalculatorClass"] - + for expected in expected_symbols: assert expected in symbol_names, f"{expected} function not found in Calculator.fs symbols" @@ -71,26 +71,24 @@ class TestFSharpLanguageServer: """Test finding references using symbol selection range.""" file_path = os.path.join("Calculator.fs") symbols = language_server.request_document_symbols(file_path) - + # Find the 'add' function symbol add_symbol = None symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols - + for sym in symbol_list: if sym.get("name") == "add": add_symbol = sym break - + assert add_symbol is not None, "Could not find 'add' function symbol in Calculator.fs" - + # Try to find references to the add function sel_start = add_symbol["selectionRange"]["start"] refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"] + 1) - + # The add function should be referenced in Program.fs - assert any( - "Program.fs" in ref.get("relativePath", "") for ref in refs - ), "Program.fs should reference add function" + assert any("Program.fs" in ref.get("relativePath", "") for ref in refs), "Program.fs should reference add function" @pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True) def test_nested_module_symbols(self, language_server: SolidLanguageServer) -> None: @@ -108,7 +106,7 @@ class TestFSharpLanguageServer: # Check for expected types and modules symbol_names = [s.get("name") for s in symbols] expected_symbols = ["Person", "PersonModule", "Address", "Employee"] - + for expected in expected_symbols: assert expected in symbol_names, f"{expected} not found in Person.fs symbols" @@ -135,20 +133,18 @@ class TestFSharpLanguageServer: refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"] + 1) # The subtract function should be referenced in Program.fs - assert any( - "Program.fs" in ref.get("relativePath", "") for ref in refs - ), "Program.fs should reference subtract function" + assert any("Program.fs" in ref.get("relativePath", "") for ref in refs), "Program.fs should reference subtract function" @pytest.mark.parametrize("language_server", [Language.FSHARP], indirect=True) def test_go_to_definition(self, language_server: SolidLanguageServer) -> None: """Test go-to-definition functionality.""" # Test going to definition of 'add' function from Program.fs program_file = os.path.join("Program.fs") - + # Try to find definition of 'add' function used in Program.fs # This would typically be at the line where 'add 5 3' is called definitions = language_server.request_definition(program_file, 10, 20) # Approximate position - + # We should get at least some definitions assert len(definitions) >= 0, "Should get definitions (even if empty for complex cases)" @@ -156,10 +152,10 @@ class TestFSharpLanguageServer: def test_hover_information(self, language_server: SolidLanguageServer) -> None: """Test hover information functionality.""" file_path = os.path.join("Calculator.fs") - + # Try to get hover information for a function hover_info = language_server.request_hover(file_path, 5, 10) # Approximate position of a function - + # Hover info might be None or contain information # This is acceptable as it depends on the LSP server's capabilities and timing assert hover_info is None or isinstance(hover_info, dict), "Hover info should be None or dict" @@ -168,18 +164,18 @@ class TestFSharpLanguageServer: def test_completion(self, language_server: SolidLanguageServer) -> None: """Test code completion functionality.""" file_path = os.path.join("Program.fs") - + # F# completion can be slow, so we'll test this with a timeout approach # In real usage, completions work but may take time for large projects try: import signal - + def timeout_handler(signum, frame): raise TimeoutError("Completion request timed out") - + signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(5) # 5 second timeout - + try: completions = language_server.request_completions(file_path, 15, 10) signal.alarm(0) # Cancel alarm @@ -197,16 +193,16 @@ class TestFSharpLanguageServer: def test_diagnostics(self, language_server: SolidLanguageServer) -> None: """Test getting diagnostics (errors, warnings) from F# files.""" file_path = os.path.join("Program.fs") - + # FsAutoComplete uses publishDiagnostics notifications instead of textDocument/diagnostic requests # So we'll test that the language server can handle files without crashing # In real usage, diagnostics would come through the publishDiagnostics notification handler - + # Test that we can at least work with the file (open/close cycle) with language_server.open_file(file_path) as _: # If we can open and close the file without errors, basic diagnostics support is working pass - + # This is a successful test - FsAutoComplete is working with F# files assert True, "F# language server can handle files successfully" @@ -214,21 +210,19 @@ class TestFSharpLanguageServer: @pytest.mark.fsharp class TestFSharpLanguageServerSetup: """Test F# language server setup and configuration.""" - + def test_runtime_dependency_setup_without_dotnet(self) -> None: """Test that setup fails gracefully when .NET is not installed.""" with patch("shutil.which", return_value=None): with pytest.raises(RuntimeError, match=".NET SDK is not installed"): - FSharpLanguageServer._setup_runtime_dependencies( - Mock(), Mock(), Mock() - ) + FSharpLanguageServer._setup_runtime_dependencies(Mock(), Mock(), Mock()) def test_runtime_dependency_setup_with_dotnet(self) -> None: """Test that setup works when .NET is available.""" mock_logger = Mock() mock_config = Mock() mock_settings = Mock() - + # Mock the ls_resources_dir to return a temp directory with tempfile.TemporaryDirectory() as temp_dir: with patch("shutil.which", return_value="/usr/bin/dotnet"): @@ -237,16 +231,14 @@ class TestFSharpLanguageServerSetup: # Mock successful dotnet version check mock_run.return_value.stdout = "8.0.100" mock_run.return_value.returncode = 0 - + # Create a fake fsautocomplete executable fsharp_dir = os.path.join(temp_dir, "fsharp-lsp") os.makedirs(fsharp_dir, exist_ok=True) fsautocomplete_path = os.path.join(fsharp_dir, "fsautocomplete") Path(fsautocomplete_path).touch() - - result = FSharpLanguageServer._setup_runtime_dependencies( - mock_logger, mock_config, mock_settings - ) - + + result = FSharpLanguageServer._setup_runtime_dependencies(mock_logger, mock_config, mock_settings) + assert fsautocomplete_path in result - assert "--lsp" in result \ No newline at end of file + assert "--lsp" in result