mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 16:15:33 +00:00
CCLS Support added (#975)
Also added a test suite for both clangd and ccls
This commit is contained in:
1 parent
01dc6f8734
commit
71a6f1ff82
15 files changed
+337
-5
No files matched your search
@@ -110,6 +110,22 @@ jobs:
|
||||
uses: DeLaGuardo/setup-clojure@13.4
|
||||
with:
|
||||
cli: latest
|
||||
- name: Install ccls (C/C++ Language Server)
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ccls
|
||||
elif [[ "${{ runner.os }}" == "macOS" ]]; then
|
||||
brew install ccls
|
||||
fi
|
||||
# Windows: ccls requires building from source with MSYS2/LLVM, skipped in CI
|
||||
# Verify installation
|
||||
if command -v ccls &> /dev/null; then
|
||||
echo "ccls installed: $(ccls --version 2>&1 | head -1)"
|
||||
else
|
||||
echo "ccls not available on this platform (expected on Windows)"
|
||||
fi
|
||||
- name: Setup Java (for JVM based languages)
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
|
||||
@@ -61,6 +61,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
* **Add support for PowerShell** via PowerShell Editor Services (PSES). Requires `pwsh` (PowerShell Core) to be installed and available in PATH. Supports symbol navigation, go-to-definition, and within-file references for .ps1 files.
|
||||
* **Add support for MATLAB** via the official MathWorks MATLAB Language Server. Requires MATLAB R2021b or later and Node.js. Set `MATLAB_PATH` environment variable or configure `matlab_path` in `ls_specific_settings`. Supports .m, .mlx, and .mlapp files with code completion, diagnostics, go-to-definition, find references, document symbols, formatting, and rename.
|
||||
* **Add support for Pascal** via the official Pascal Language Server.
|
||||
* **C/C++ alternate LS (ccls)**: Add experimental, opt-in support for ccls as an alternative backend to clangd. Enable via `cpp_ccls` in project configuration. Requires `ccls` installed and ideally a `compile_commands.json` at repo root.
|
||||
|
||||
|
||||
# 0.1.4
|
||||
|
||||
@@ -84,6 +84,8 @@ The underlying language servers are typically open-source projects (like Serena)
|
||||
With Serena's LSP library, we provide **support for over 30 programming languages**, including
|
||||
AL, Bash, C#, C/C++, Clojure, Dart, Elixir, Elm, Erlang, Fortran, Go, Groovy (partial support), Haskell, Java, Javascript, Julia, Kotlin, Lua, Markdown, MATLAB, Nix, Perl, PHP, PowerShell, Python, R, Ruby, Rust, Scala, Swift, TOML, TypeScript, YAML, and Zig.
|
||||
|
||||
Note: For **C/C++**, Serena uses **clangd** by default and also supports **ccls** as an experimental, opt-in alternative (`cpp_ccls`). For best results, provide a `compile_commands.json` at the repository root.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Some language servers require additional dependencies to be installed; see the [Language Support](https://oraios.github.io/serena/01-about/020_programming-languages.html) page for details.
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ Some languages require additional installations or setup steps, as noted.
|
||||
* **Bash**
|
||||
* **C#**
|
||||
* **C/C++**
|
||||
(you may experience issues with finding references, we are working on it)
|
||||
Default: clangd. Optional alternate: ccls (experimental, opt-in).
|
||||
For best results, provide a `compile_commands.json` at the repository root.
|
||||
* **Clojure**
|
||||
* **Dart**
|
||||
* **Elixir**
|
||||
|
||||
@@ -313,6 +313,7 @@ markers = [
|
||||
"yaml: language server running for YAML",
|
||||
"powershell: language server running for PowerShell",
|
||||
"pascal: language server running for Pascal (Free Pascal/Lazarus)",
|
||||
"cpp: language server running for C/C++",
|
||||
"slow: tests that require additional Expert instances and have long startup times (~60-90s each)",
|
||||
"toml: language server running for TOML",
|
||||
"matlab: language server running for MATLAB (requires MATLAB R2021b+)",
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
Provides C/C++ specific instantiation of the LanguageServer class using ccls.
|
||||
|
||||
This is an alternative to clangd for large C++ codebases where ccls may perform
|
||||
better for indexing and navigation. Requires ccls to be installed and available
|
||||
on PATH, or configured via ls_specific_settings with key "ls_path".
|
||||
|
||||
For best results, ensure a compile_commands.json exists at the repository root.
|
||||
|
||||
Installation
|
||||
------------
|
||||
ccls must be installed manually as there are no prebuilt binaries available for
|
||||
direct download. Install using your system package manager:
|
||||
|
||||
**Linux:**
|
||||
- Ubuntu/Debian (22.04+): ``sudo apt-get install ccls``
|
||||
- Fedora/RHEL: ``sudo dnf install ccls``
|
||||
- Arch Linux: ``sudo pacman -S ccls``
|
||||
- openSUSE Tumbleweed: ``sudo zypper install ccls``
|
||||
- Gentoo: ``sudo emerge dev-util/ccls``
|
||||
|
||||
**macOS:**
|
||||
- Homebrew: ``brew install ccls``
|
||||
|
||||
**Windows:**
|
||||
- MSYS2 (MinGW): Build from source using MSYS2 toolchain
|
||||
- No native prebuilt binaries available; must build from source
|
||||
|
||||
For build-from-source instructions (required on Windows), see:
|
||||
https://github.com/MaskRay/ccls/wiki/Build
|
||||
|
||||
Official documentation:
|
||||
https://github.com/MaskRay/ccls
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import threading
|
||||
from typing import Any, cast
|
||||
|
||||
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 CclsLanguageServer(SolidLanguageServer):
|
||||
"""
|
||||
C/C++ language server implementation using ccls.
|
||||
|
||||
Notes:
|
||||
- ccls should be installed and on PATH (or specify ls_path in settings)
|
||||
- compile_commands.json at repo root is recommended for accurate indexing
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, config: LanguageServerConfig, repository_root_path: str, solidlsp_settings: SolidLSPSettings):
|
||||
"""
|
||||
Creates a CclsLanguageServer instance. This class is not meant to be instantiated directly.
|
||||
Use LanguageServer.create() instead.
|
||||
"""
|
||||
super().__init__(config, repository_root_path, None, "cpp", solidlsp_settings)
|
||||
self.server_ready = threading.Event()
|
||||
|
||||
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:
|
||||
"""
|
||||
Resolve ccls path from system or raise helpful error if missing.
|
||||
Allows override via ls_specific_settings[language].ls_path.
|
||||
"""
|
||||
import shutil
|
||||
|
||||
ccls_path = shutil.which("ccls")
|
||||
if not ccls_path:
|
||||
raise FileNotFoundError(
|
||||
"ccls is not installed on your system.\n"
|
||||
"Please install ccls using your system package manager:\n"
|
||||
" Linux (Ubuntu/Debian): sudo apt-get install ccls\n"
|
||||
" Linux (Fedora/RHEL): sudo dnf install ccls\n"
|
||||
" Linux (Arch): sudo pacman -S ccls\n"
|
||||
" macOS (Homebrew): brew install ccls\n"
|
||||
" Windows: Build from source (see wiki)\n\n"
|
||||
"For build instructions and more details, see:\n"
|
||||
" https://github.com/MaskRay/ccls/wiki/Build"
|
||||
)
|
||||
log.info(f"Using system-installed ccls at {ccls_path}")
|
||||
return ccls_path
|
||||
|
||||
def _create_launch_command(self, core_path: str) -> list[str] | str:
|
||||
return [core_path]
|
||||
|
||||
@staticmethod
|
||||
def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
|
||||
"""
|
||||
Returns the initialize params for the ccls 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},
|
||||
},
|
||||
"workspace": {"workspaceFolders": True, "didChangeConfiguration": {"dynamicRegistration": True}},
|
||||
},
|
||||
"processId": os.getpid(),
|
||||
"rootPath": repository_absolute_path,
|
||||
"rootUri": root_uri,
|
||||
"workspaceFolders": [
|
||||
{
|
||||
"uri": root_uri,
|
||||
"name": "$name",
|
||||
}
|
||||
],
|
||||
# ccls supports initializationOptions but none are required for basic functionality
|
||||
}
|
||||
return cast(InitializeParams, initialize_params)
|
||||
|
||||
def _start_server(self) -> None:
|
||||
"""
|
||||
Starts the ccls language server and initializes the LSP connection.
|
||||
"""
|
||||
|
||||
def do_nothing(params: Any) -> None:
|
||||
pass
|
||||
|
||||
def window_log_message(msg: dict) -> None:
|
||||
log.info(f"LSP: window/logMessage: {msg}")
|
||||
|
||||
# Register minimal handlers
|
||||
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 ccls server process")
|
||||
self.server.start()
|
||||
initialize_params = self._get_initialize_params(self.repository_root_path)
|
||||
|
||||
log.info("Sending initialize request from LSP client to ccls and awaiting response")
|
||||
self.server.send.initialize(initialize_params)
|
||||
# Do not assert clangd-specific capability shapes; ccls differs
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# Basic readiness
|
||||
self.completions_available.set()
|
||||
self.server_ready.set()
|
||||
@@ -41,6 +41,7 @@ class Language(str, Enum):
|
||||
RUBY = "ruby"
|
||||
DART = "dart"
|
||||
CPP = "cpp"
|
||||
CPP_CCLS = "cpp_ccls"
|
||||
PHP = "php"
|
||||
R = "r"
|
||||
PERL = "perl"
|
||||
@@ -124,6 +125,7 @@ class Language(str, Enum):
|
||||
self.YAML,
|
||||
self.TOML,
|
||||
self.GROOVY,
|
||||
self.CPP_CCLS,
|
||||
}
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -170,7 +172,7 @@ class Language(str, Enum):
|
||||
return FilenameMatcher("*.rb", "*.erb")
|
||||
case self.RUBY_SOLARGRAPH:
|
||||
return FilenameMatcher("*.rb")
|
||||
case self.CPP:
|
||||
case self.CPP | self.CPP_CCLS:
|
||||
return FilenameMatcher("*.cpp", "*.h", "*.hpp", "*.c", "*.hxx", "*.cc", "*.cxx")
|
||||
case self.KOTLIN:
|
||||
return FilenameMatcher("*.kt", "*.kts")
|
||||
@@ -304,6 +306,10 @@ class Language(str, Enum):
|
||||
from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer
|
||||
|
||||
return ClangdLanguageServer
|
||||
case self.CPP_CCLS:
|
||||
from solidlsp.language_servers.ccls_language_server import CclsLanguageServer
|
||||
|
||||
return CclsLanguageServer
|
||||
case self.PHP:
|
||||
from solidlsp.language_servers.intelephense import Intelephense
|
||||
|
||||
|
||||
+27
-3
@@ -1,12 +1,20 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil as _sh
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from sensai.util.logging import configure
|
||||
|
||||
try:
|
||||
from sensai.util.logging import configure
|
||||
except Exception:
|
||||
# Fallback if 'sensai' is not installed: basic logging configuration
|
||||
def configure(level: int = logging.INFO) -> None:
|
||||
logging.basicConfig(level=level)
|
||||
|
||||
|
||||
from serena.config.serena_config import SerenaPaths
|
||||
from serena.constants import SERENA_MANAGED_DIR_NAME
|
||||
@@ -35,7 +43,9 @@ class LanguageParamRequest:
|
||||
|
||||
|
||||
def get_repo_path(language: Language) -> Path:
|
||||
return Path(__file__).parent / "resources" / "repos" / language / "test_repo"
|
||||
# Map both CPP variants to the shared cpp test repo
|
||||
repo_dir = "cpp" if language in (Language.CPP, Language.CPP_CCLS) else str(language)
|
||||
return Path(__file__).parent / "resources" / "repos" / repo_dir / "test_repo"
|
||||
|
||||
|
||||
def _create_ls(
|
||||
@@ -52,7 +62,11 @@ def _create_ls(
|
||||
gitignore_parser = GitignoreParser(str(repo_path))
|
||||
for spec in gitignore_parser.get_ignore_specs():
|
||||
ignored_paths.extend(spec.patterns)
|
||||
config = LanguageServerConfig(code_language=language, ignored_paths=ignored_paths, trace_lsp_communication=trace_lsp_communication)
|
||||
config = LanguageServerConfig(
|
||||
code_language=language,
|
||||
ignored_paths=ignored_paths,
|
||||
trace_lsp_communication=trace_lsp_communication,
|
||||
)
|
||||
effective_solidlsp_dir = solidlsp_dir if solidlsp_dir is not None else SerenaPaths().serena_user_home_dir
|
||||
return SolidLanguageServer.create(
|
||||
config,
|
||||
@@ -212,6 +226,16 @@ def _determine_disabled_languages() -> list[Language]:
|
||||
if not clojure_tests_enabled:
|
||||
result.append(Language.CLOJURE)
|
||||
|
||||
# Disable CPP_CCLS tests if ccls is not available
|
||||
ccls_tests_enabled = _sh.which("ccls") is not None
|
||||
if not ccls_tests_enabled:
|
||||
result.append(Language.CPP_CCLS)
|
||||
|
||||
# Disable CPP (clangd) tests if clangd is not available
|
||||
clangd_tests_enabled = _sh.which("clangd") is not None
|
||||
if not clangd_tests_enabled:
|
||||
result.append(Language.CPP)
|
||||
|
||||
al_tests_enabled = True
|
||||
if not al_tests_enabled:
|
||||
result.append(Language.AL)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#include "b.hpp"
|
||||
|
||||
int main() {
|
||||
int x = add(3, 4);
|
||||
return x;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#include "b.hpp"
|
||||
|
||||
int add(int a, int b) {
|
||||
return a + b;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
int add(int a, int b);
|
||||
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"directory": ".",
|
||||
"command": "g++ -I . -c a.cpp",
|
||||
"file": "a.cpp"
|
||||
},
|
||||
{
|
||||
"directory": ".",
|
||||
"command": "g++ -I . -c b.cpp",
|
||||
"file": "b.cpp"
|
||||
}
|
||||
]
|
||||
@@ -36,6 +36,7 @@ def serena_config():
|
||||
Language.CLOJURE,
|
||||
Language.FSHARP,
|
||||
Language.POWERSHELL,
|
||||
Language.CPP_CCLS,
|
||||
]:
|
||||
repo_path = get_repo_path(language)
|
||||
if repo_path.exists():
|
||||
@@ -118,6 +119,7 @@ class TestSerenaAgent:
|
||||
pytest.param(Language.CSHARP, "Calculator", "Class", "Program.cs", marks=pytest.mark.csharp),
|
||||
pytest.param(Language.FSHARP, "Calculator", "Module", "Calculator.fs", marks=pytest.mark.fsharp),
|
||||
pytest.param(Language.POWERSHELL, "function Greet-User ()", "Function", "main.ps1", marks=pytest.mark.powershell),
|
||||
pytest.param(Language.CPP_CCLS, "add", "Function", "b.cpp", marks=pytest.mark.cpp),
|
||||
],
|
||||
indirect=["serena_agent"],
|
||||
)
|
||||
@@ -186,6 +188,7 @@ class TestSerenaAgent:
|
||||
pytest.param(Language.CSHARP, "Calculator", "Program.cs", "Program.cs", marks=pytest.mark.csharp),
|
||||
pytest.param(Language.FSHARP, "add", "Calculator.fs", "Program.fs", marks=pytest.mark.fsharp),
|
||||
pytest.param(Language.POWERSHELL, "function Greet-User ()", "main.ps1", "main.ps1", marks=pytest.mark.powershell),
|
||||
pytest.param(Language.CPP_CCLS, "add", "b.cpp", "a.cpp", marks=pytest.mark.cpp),
|
||||
],
|
||||
indirect=["serena_agent"],
|
||||
)
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1,92 @@
|
||||
"""
|
||||
Basic tests for C/C++ language server integration (clangd and ccls).
|
||||
|
||||
This module tests both Language.CPP (clangd) and Language.CPP_CCLS (ccls)
|
||||
using the same test repository. Tests are skipped if the respective language
|
||||
server is not available.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from solidlsp import SolidLanguageServer
|
||||
from solidlsp.ls_config import Language
|
||||
from solidlsp.ls_utils import SymbolUtils
|
||||
|
||||
|
||||
def _clangd_available() -> bool:
|
||||
return shutil.which("clangd") is not None
|
||||
|
||||
|
||||
def _ccls_available() -> bool:
|
||||
return shutil.which("ccls") is not None
|
||||
|
||||
|
||||
# Build parametrize list based on availability
|
||||
_cpp_servers: list[Language] = []
|
||||
if _clangd_available():
|
||||
_cpp_servers.append(Language.CPP)
|
||||
if _ccls_available():
|
||||
_cpp_servers.append(Language.CPP_CCLS)
|
||||
|
||||
|
||||
@pytest.mark.cpp
|
||||
@pytest.mark.skipif(not _cpp_servers, reason="No C++ language server (clangd or ccls) available")
|
||||
class TestCppLanguageServer:
|
||||
"""Tests for C/C++ language servers (clangd and ccls)."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", _cpp_servers, indirect=True)
|
||||
def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test that symbol tree contains expected functions."""
|
||||
symbols = language_server.request_full_symbol_tree()
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "Function 'add' not found in symbol tree"
|
||||
assert SymbolUtils.symbol_tree_contains_name(symbols, "main"), "Function 'main' not found in symbol tree"
|
||||
|
||||
@pytest.mark.parametrize("language_server", _cpp_servers, indirect=True)
|
||||
def test_get_document_symbols(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test document symbols for a.cpp."""
|
||||
file_path = os.path.join("a.cpp")
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
# Flatten nested structure if needed
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
names = [s.get("name") for s in symbol_list]
|
||||
assert "main" in names, f"Expected 'main' in document symbols, got: {names}"
|
||||
|
||||
@pytest.mark.parametrize("language_server", _cpp_servers, indirect=True)
|
||||
def test_find_referencing_symbols_across_files(self, language_server: SolidLanguageServer) -> None:
|
||||
"""Test finding references to 'add' function across files."""
|
||||
# Locate 'add' in b.cpp
|
||||
file_path = os.path.join("b.cpp")
|
||||
symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
|
||||
symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols
|
||||
add_symbol = None
|
||||
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 b.cpp"
|
||||
|
||||
sel_start = add_symbol["selectionRange"]["start"]
|
||||
refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"] + 1)
|
||||
ref_files = cast(list[str], [ref.get("relativePath", "") for ref in refs])
|
||||
assert any("a.cpp" in ref_file for ref_file in ref_files), "Should find reference in a.cpp"
|
||||
|
||||
# Verify second call returns same results (stability check)
|
||||
def _ref_key(ref: dict) -> tuple:
|
||||
rp = ref.get("relativePath", "")
|
||||
rng = ref.get("range") or {}
|
||||
s = rng.get("start") or {}
|
||||
e = rng.get("end") or {}
|
||||
return (
|
||||
rp,
|
||||
s.get("line", -1),
|
||||
s.get("character", -1),
|
||||
e.get("line", -1),
|
||||
e.get("character", -1),
|
||||
)
|
||||
|
||||
refs2 = language_server.request_references(file_path, sel_start["line"], sel_start["character"] + 1)
|
||||
assert sorted(map(_ref_key, refs2)) == sorted(map(_ref_key, refs)), "Reference results should be stable across calls"
|
||||
Reference in new issue
Block a user