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 <liyuan851277048@icloud.com>
Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-04-24 11:41:50 +02:00
1 parent 9ae2d1af77
commit e4da2db1d7
10 files changed
+314 -1

No files matched your search

+1
View File
@@ -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`):
+1 -1
View File
@@ -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
@@ -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)
+1
View File
@@ -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++",
@@ -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")
+13
View File
@@ -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
@@ -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
}
}
@@ -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"]
}
]
}
View File
Whitespace-only changes.
+78
View File
@@ -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,
)