From e4da2db1d7a59b1f003b7763b9f1794e610baecd Mon Sep 17 00:00:00 2001 From: Octopus Date: Fri, 24 Apr 2026 17:41:50 +0800 Subject: [PATCH] feat: add JSON language server support via vscode-json-languageserver (#1404) * feat: add JSON language server support via vscode-json-languageserver (fixes #1391) Add support for JSON files using the `vscode-json-languageserver` npm package from Microsoft. This provides document symbol navigation and hover information for JSON files. The implementation follows the existing YAML language server pattern: - Auto-installs `vscode-json-languageserver` via npm on first use - Requires Node.js and npm - Marked as experimental (must be explicitly specified) Co-Authored-By: Octopus Co-authored-by: octo-patch Co-authored-by: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> --- CHANGELOG.md | 1 + README.md | 2 +- docs/01-about/020_programming-languages.md | 2 + pyproject.toml | 1 + .../language_servers/json_language_server.py | 164 ++++++++++++++++++ src/solidlsp/ls_config.py | 13 ++ .../repos/json/test_repo/config.json | 24 +++ test/resources/repos/json/test_repo/data.json | 30 ++++ test/solidlsp/json_ls/__init__.py | 0 test/solidlsp/json_ls/test_json_basic.py | 78 +++++++++ 10 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 src/solidlsp/language_servers/json_language_server.py create mode 100644 test/resources/repos/json/test_repo/config.json create mode 100644 test/resources/repos/json/test_repo/data.json create mode 100644 test/solidlsp/json_ls/__init__.py create mode 100644 test/solidlsp/json_ls/test_json_basic.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 678be1e2..b4e968a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Fix: clangd capability checks now tolerate valid initialize response shape differences and invalidate cached C++ document symbols when clangd/compile commands context changes #1359 - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files - Fix: Lean4 stale cache — empty document symbol responses (returned before `lake build` completes) are no longer persisted, preventing symbols from being permanently hidden #1356 + - Add JSON language server support via `vscode-json-languageserver` (experimental) #1391 Dashboard: - Add configurable dashboard interface mode (new global configuration setting `web_dashboard_interface`): diff --git a/README.md b/README.md index d26bdad8..7ee19813 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Serena incorporates a powerful abstraction layer for the integration of language The underlying language servers are typically open-source projects or at least freely available for use. When using Serena's language server backend, we provide **support for over 40 programming languages**, including -AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. +AL, Ansible, Bash, C#, C/C++, Clojure, Crystal, Dart, Elixir, Elm, Erlang, Fortran, F#, GLSL, Go, Groovy, Haskell, Haxe, HLSL, Java, JavaScript, JSON, Julia, Kotlin, Lean 4, Lua, Luau, Markdown, MATLAB, mSL, Nix, OCaml, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Solidity, Swift, TOML, TypeScript, WGSL, YAML, and Zig. ### The Serena JetBrains Plugin diff --git a/docs/01-about/020_programming-languages.md b/docs/01-about/020_programming-languages.md index 999782ee..dff0a228 100644 --- a/docs/01-about/020_programming-languages.md +++ b/docs/01-about/020_programming-languages.md @@ -115,6 +115,8 @@ Some languages require additional installations or setup steps, as noted. * **Vue** (3.x with TypeScript; requires Node.js v18+ and npm; supports .vue Single File Components with monorepo detection) * **YAML** +* **JSON** + (experimental; must be explicitly added to the languages list; requires Node.js and npm) * **Zig** (requires installation of ZLS - Zig Language Server) diff --git a/pyproject.toml b/pyproject.toml index df90518d..da1aa5b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -336,6 +336,7 @@ markers = [ "haskell: Haskell language server tests", "haxe: Haxe language server tests", "yaml: language server running for YAML", + "json: language server running for JSON (uses vscode-json-languageserver)", "powershell: language server running for PowerShell", "pascal: language server running for Pascal (Free Pascal/Lazarus)", "cpp: language server running for C/C++", diff --git a/src/solidlsp/language_servers/json_language_server.py b/src/solidlsp/language_servers/json_language_server.py new file mode 100644 index 00000000..bf46992a --- /dev/null +++ b/src/solidlsp/language_servers/json_language_server.py @@ -0,0 +1,164 @@ +""" +Provides JSON specific instantiation of the LanguageServer class using vscode-json-languageserver. +Contains various configurations and settings specific to JSON files. +""" + +import logging +import os +import pathlib +import shutil +from typing import Any + +from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection, build_npm_install_command +from solidlsp.ls import LanguageServerDependencyProvider, LanguageServerDependencyProviderSinglePath, SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.settings import SolidLSPSettings + +log = logging.getLogger(__name__) + + +class JsonLanguageServer(SolidLanguageServer): + """ + Provides JSON specific instantiation of the LanguageServer class using vscode-json-languageserver. + Contains various configurations and settings specific to JSON files. + + Note: Cross-file references are not supported for JSON (the language server only provides + document symbols and hover). JSON is useful for getting a structured overview of JSON files + and navigating their contents. + """ + + def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings): + """ + Creates a JsonLanguageServer instance. This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + super().__init__( + config, + repository_root_path, + None, + "json", + solidlsp_settings, + ) + + def _create_dependency_provider(self) -> LanguageServerDependencyProvider: + return self.DependencyProvider(self._custom_settings, self._ls_resources_dir) + + class DependencyProvider(LanguageServerDependencyProviderSinglePath): + def _get_or_install_core_dependency(self) -> str: + """ + Setup runtime dependencies for JSON Language Server and return the path to the executable. + """ + is_node_installed = shutil.which("node") is not None + assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again." + is_npm_installed = shutil.which("npm") is not None + assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again." + + json_language_server_version = self._custom_settings.get("json_language_server_version", "1.3.4") + npm_registry = self._custom_settings.get("npm_registry") + + deps = RuntimeDependencyCollection( + [ + RuntimeDependency( + id="vscode-json-languageserver", + description="vscode-json-languageserver package (Microsoft)", + command=build_npm_install_command("vscode-json-languageserver", json_language_server_version, npm_registry), + platform_id="any", + ), + ] + ) + + json_ls_dir = os.path.join(self._ls_resources_dir, "json-lsp") + json_executable_path = os.path.join(json_ls_dir, "node_modules", ".bin", "vscode-json-languageserver") + + if os.name == "nt": + json_executable_path += ".cmd" + + if not os.path.exists(json_executable_path): + log.info(f"JSON Language Server executable not found at {json_executable_path}. Installing...") + deps.install(json_ls_dir) + log.info("JSON language server dependencies installed successfully") + + if not os.path.exists(json_executable_path): + raise FileNotFoundError( + f"vscode-json-languageserver executable not found at {json_executable_path}, something went wrong with the installation." + ) + + return json_executable_path + + def _create_launch_command(self, core_path: str) -> list[str]: + return [core_path, "--stdio"] + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the JSON Language Server. + """ + root_uri = pathlib.Path(repository_absolute_path).as_uri() + initialize_params = { + "locale": "en", + "capabilities": { + "textDocument": { + "synchronization": {"didSave": True, "dynamicRegistration": True}, + "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}}, + "definition": {"dynamicRegistration": True}, + "references": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "hierarchicalDocumentSymbolSupport": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + }, + "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]}, + }, + "workspace": { + "workspaceFolders": True, + "didChangeConfiguration": {"dynamicRegistration": True}, + }, + }, + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + return initialize_params # type: ignore + + def _start_server(self) -> None: + """ + Starts the JSON Language Server, waits for the server to be ready and yields the LanguageServer instance. + """ + + def register_capability_handler(params: Any) -> None: + return + + def do_nothing(params: Any) -> None: + return + + def window_log_message(msg: dict) -> None: + log.info(f"LSP: window/logMessage: {msg}") + + self.server.on_request("client/registerCapability", register_capability_handler) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + log.info("Starting JSON server process") + self.server.start() + initialize_params = self._get_initialize_params(self.repository_root_path) + + log.info("Sending initialize request from LSP client to LSP server and awaiting response") + init_response = self.server.send.initialize(initialize_params) + log.debug(f"Received initialize response from JSON server: {init_response}") + + if "documentSymbolProvider" in init_response.get("capabilities", {}): + log.info("JSON server supports document symbols") + else: + log.warning("Warning: JSON server does not report document symbol support") + + self.server.notify.initialized({}) + + log.info("JSON server initialization complete") diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index ff137894..eb527dc3 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -122,6 +122,12 @@ class Language(str, Enum): """YAML language server (experimental). Must be explicitly specified as the main language, not auto-detected. """ + JSON = "json" + """JSON language server using vscode-json-languageserver (experimental). + Provides document symbol navigation and hover for JSON files. + Must be explicitly specified as the main language, not auto-detected. + Requires Node.js and npm. + """ TOML = "toml" """TOML language server using Taplo. Supports TOML validation, formatting, and schema support. @@ -174,6 +180,7 @@ class Language(str, Enum): self.PHP_PHPACTOR, self.MARKDOWN, self.YAML, + self.JSON, self.TOML, self.GROOVY, self.CPP_CCLS, @@ -252,6 +259,8 @@ class Language(str, Enum): return FilenameMatcher("*.cr") case self.YAML: return FilenameMatcher("*.yaml", "*.yml") + case self.JSON: + return FilenameMatcher("*.json", "*.jsonc") case self.TOML: return FilenameMatcher("*.toml") case self.ZIG: @@ -446,6 +455,10 @@ class Language(str, Enum): from solidlsp.language_servers.yaml_language_server import YamlLanguageServer return YamlLanguageServer + case self.JSON: + from solidlsp.language_servers.json_language_server import JsonLanguageServer + + return JsonLanguageServer case self.TOML: from solidlsp.language_servers.taplo_server import TaploServer diff --git a/test/resources/repos/json/test_repo/config.json b/test/resources/repos/json/test_repo/config.json new file mode 100644 index 00000000..78ec641a --- /dev/null +++ b/test/resources/repos/json/test_repo/config.json @@ -0,0 +1,24 @@ +{ + "app": { + "name": "test-application", + "version": "1.0.0", + "port": 8080, + "debug": true + }, + "database": { + "host": "localhost", + "port": 5432, + "name": "testdb", + "username": "admin" + }, + "logging": { + "level": "info", + "format": "json", + "outputs": ["console", "file"] + }, + "features": { + "authentication": true, + "caching": false, + "monitoring": true + } +} diff --git a/test/resources/repos/json/test_repo/data.json b/test/resources/repos/json/test_repo/data.json new file mode 100644 index 00000000..329491ba --- /dev/null +++ b/test/resources/repos/json/test_repo/data.json @@ -0,0 +1,30 @@ +{ + "users": [ + { + "id": 1, + "name": "Alice", + "email": "alice@example.com", + "roles": ["admin", "user"] + }, + { + "id": 2, + "name": "Bob", + "email": "bob@example.com", + "roles": ["user"] + } + ], + "projects": [ + { + "id": 101, + "name": "Project Alpha", + "status": "active", + "tags": ["backend", "api"] + }, + { + "id": 102, + "name": "Project Beta", + "status": "planning", + "tags": ["frontend"] + } + ] +} diff --git a/test/solidlsp/json_ls/__init__.py b/test/solidlsp/json_ls/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/solidlsp/json_ls/test_json_basic.py b/test/solidlsp/json_ls/test_json_basic.py new file mode 100644 index 00000000..c2e8031a --- /dev/null +++ b/test/solidlsp/json_ls/test_json_basic.py @@ -0,0 +1,78 @@ +""" +Basic integration tests for the JSON language server functionality. + +These tests validate the functionality of the language server APIs +like request_document_symbols using the JSON test repository. +""" + +from pathlib import Path + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language +from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols + + +@pytest.mark.json +class TestJsonLanguageServerBasics: + """Test basic functionality of the JSON language server.""" + + @pytest.mark.parametrize("language_server", [Language.JSON], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.JSON], indirect=True) + def test_json_language_server_initialization(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + """Test that JSON language server can be initialized successfully.""" + assert language_server is not None + assert language_server.language == Language.JSON + assert language_server.is_running() + assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve() + + @pytest.mark.parametrize("language_server", [Language.JSON], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.JSON], indirect=True) + def test_json_config_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + """Test document symbols detection in config.json with specific symbol verification.""" + all_symbols, root_symbols = language_server.request_document_symbols("config.json").get_all_symbols_and_roots() + + assert all_symbols is not None, "Should return symbols for config.json" + assert len(all_symbols) > 0, f"Should find symbols in config.json, found {len(all_symbols)}" + + symbol_names = [sym.get("name") for sym in all_symbols] + assert "app" in symbol_names, "Should detect 'app' key in config.json" + assert "database" in symbol_names, "Should detect 'database' key in config.json" + assert "logging" in symbol_names, "Should detect 'logging' key in config.json" + assert "features" in symbol_names, "Should detect 'features' key in config.json" + + # Verify nested symbols + assert "name" in symbol_names, "Should detect nested 'name' key" + assert "port" in symbol_names, "Should detect nested 'port' key" + assert "debug" in symbol_names, "Should detect nested 'debug' key" + + @pytest.mark.parametrize("language_server", [Language.JSON], indirect=True) + @pytest.mark.parametrize("repo_path", [Language.JSON], indirect=True) + def test_json_data_file_symbols(self, language_server: SolidLanguageServer, repo_path: Path) -> None: + """Test symbol detection in data.json with array structures.""" + all_symbols, root_symbols = language_server.request_document_symbols("data.json").get_all_symbols_and_roots() + + assert all_symbols is not None, "Should return symbols for data.json" + assert len(all_symbols) > 0, f"Should find symbols in data.json, found {len(all_symbols)}" + + symbol_names = [sym.get("name") for sym in all_symbols] + assert "users" in symbol_names, "Should detect 'users' array" + assert "projects" in symbol_names, "Should detect 'projects' array" + assert "name" in symbol_names, "Should detect 'name' fields" + assert "email" in symbol_names, "Should detect 'email' fields" + assert "id" in symbol_names, "Should detect 'id' fields" + + @pytest.mark.parametrize("language_server", [Language.JSON], indirect=True) + def test_bare_symbol_names(self, language_server: SolidLanguageServer) -> None: + """Test that symbol names do not contain malformed characters.""" + all_symbols = request_all_symbols(language_server) + malformed_symbols = [] + for s in all_symbols: + if has_malformed_name(s, period_allowed=True): + malformed_symbols.append(s) + if malformed_symbols: + pytest.fail( + f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", + pytrace=False, + )