mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-11 03:13:51 +00:00
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:
10 files changed
+314
-1
No files matched your search
@@ -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`):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Whitespace-only changes.
@@ -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,
|
||||
)
|
||||
Reference in new issue
Block a user