diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e76ce72..bdf534b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Status of the `main` branch. Changes prior to the next official version change w - Add **Angular** (experimental) via a dual-server architecture: `@angular/language-server` (ngserver) handles standalone `.html` template files, while a companion `typescript-language-server` with `@angular/language-service` loaded as a tsserver plugin handles all `.ts` operations including inline templates. Provides type-aware navigation between templates and component classes. Requires Node.js, npm, and `@angular/core` installed in the project (`npm install` in the project root). Subsumes `typescript`+`html` for `.ts`/`.html` files when active; SCSS is not subsumed. - Add **HTML** (experimental) using `vscode-html-language-server` from the `vscode-langservers-extracted` npm package. Provides in-file element/id symbols via documentSymbol; cross-file references are not meaningful for HTML. Also used as a companion server by the Angular LS for plain HTML documentSymbol support. - Add **SCSS / Sass / CSS** (experimental) using [some-sass-language-server](https://github.com/wkillerud/some-sass). Handles `.scss`, `.sass`, and `.css` through one server, with full `@use`/`@forward` workspace-wide go-to-definition and find-references for variables, mixins, and functions across Sass files. The `.css` path uses the same `vscode-css-languageservice` engine that powers the standalone CSS LS; CSS feature toggles default off upstream and are flipped on at startup so symbols, hover, completion, and syntax-level diagnostics work for plain CSS as well. + - Add support for more filenames to be considered by ccls and clangd. * Hooks: - `serena-hooks auto-approve` now also emits an `allow` decision when Claude Code reports diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index e00d942e..cc062b29 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -13,13 +13,17 @@ if TYPE_CHECKING: class FilenameMatcher: - def __init__(self, *file_extensions: str) -> None: + def __init__(self, *file_extensions: str, case_sensitive: bool = True) -> None: """ :param file_extensions: file extensions, e.g., `.py, .yml` + :param case_sensitive: whether the file extensions are case-sensitive. """ - self._file_extensions = file_extensions + self._file_extensions = list(set(file_extensions)) if case_sensitive else list(set(ext.lower() for ext in file_extensions)) + self._case_sensitive = case_sensitive def is_relevant_filename(self, fn: str) -> bool: + if not self._case_sensitive: + fn = fn.lower() for ext in self._file_extensions: if fn.endswith(ext): return True @@ -30,6 +34,8 @@ class FilenameMatcher: a *complete* extension — i.e. the extension must either end the string or be followed by a non-extension-character (anything other than a letter, digit, or underscore). """ + if self._case_sensitive: + string = string.lower() for ext in self._file_extensions: if re.search(rf"{re.escape(ext)}(?:\W|$)", string): return True @@ -284,8 +290,68 @@ class Language(str, Enum): return FilenameMatcher(".rb", ".erb") case self.RUBY_SOLARGRAPH: return FilenameMatcher(".rb") - case self.CPP | self.CPP_CCLS: - return FilenameMatcher(".cpp", ".h", ".hpp", ".c", ".hxx", ".cc", ".cxx") + case self.CPP: + # From llvm-project/clang/lib/Driver/Types.cpp types::lookupTypeForExtension: + return FilenameMatcher( + # C + ".c", + ".h", + # C++ + ".c++", + ".cc", + ".cp", + ".cpp", + ".cxx", + ".hh", + ".hpp", + ".hxx", + # C++ include files + ".inl", + ".ipp", + ".tpp", + ".txx", + # Objective-C + ".m", + ".mm", + # C++20 module interface files + ".c++m", + ".cppm", + ".cxxm", + ".ixx", + # CUDA + ".cu", + # HIP + ".hip", + # OpenCL + ".cl", + ".clcpp", + case_sensitive=False, + ) + case self.CPP_CCLS: + # From llvm-project/clang/lib/Driver/Types.cpp types::lookupTypeForExtension: + return FilenameMatcher( + # C + ".c", + ".h", + # C++ + ".c++", + ".cc", + ".cp", + ".cpp", + ".cxx", + ".hh", + ".hpp", + ".hxx", + # C++ include files + ".inl", + ".ipp", + ".tpp", + ".txx", + # Objective-C + ".m", + ".mm", + case_sensitive=False, + ) case self.KOTLIN: return FilenameMatcher(".kt", ".kts") case self.DART: @@ -341,9 +407,7 @@ class Language(str, Enum): case self.JULIA: return FilenameMatcher(".jl") case self.FORTRAN: - return FilenameMatcher( - ".f90", ".F90", ".f95", ".F95", ".f03", ".F03", ".f08", ".F08", ".f", ".F", ".for", ".FOR", ".fpp", ".FPP" - ) + return FilenameMatcher(".f90", ".f95", ".f03", ".f08", ".f", ".for", ".fpp", case_sensitive=False) case self.HASKELL: return FilenameMatcher(".hs", ".lhs") case self.HAXE: diff --git a/test/conftest.py b/test/conftest.py index aefd34ea..e05fe340 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -310,6 +310,9 @@ def _determine_disabled_languages() -> list[Language]: # Disable CPP_CCLS tests if ccls is not available ccls_tests_enabled = _sh.which("ccls") is not None + # Skip ccls tests on Windows since no recent binary is available and version + # 0.20220729 from chocolatey crashes when parsing the test files. + ccls_tests_enabled = ccls_tests_enabled and not is_windows if not ccls_tests_enabled: result.append(Language.CPP_CCLS) diff --git a/test/resources/repos/cpp/test_repo/C/a.c b/test/resources/repos/cpp/test_repo/C/a.c new file mode 100644 index 00000000..fe5fbea7 --- /dev/null +++ b/test/resources/repos/cpp/test_repo/C/a.c @@ -0,0 +1,7 @@ +#include "b.h" + +int main() +{ + int x = add(3, 4); + return x; +} diff --git a/test/resources/repos/cpp/test_repo/C/b.h b/test/resources/repos/cpp/test_repo/C/b.h new file mode 100644 index 00000000..e404974d --- /dev/null +++ b/test/resources/repos/cpp/test_repo/C/b.h @@ -0,0 +1,3 @@ +#pragma once + +int add(int a, int b); diff --git a/test/resources/repos/cpp/test_repo/CUDA/a.cu b/test/resources/repos/cpp/test_repo/CUDA/a.cu new file mode 100644 index 00000000..e32d79f4 --- /dev/null +++ b/test/resources/repos/cpp/test_repo/CUDA/a.cu @@ -0,0 +1,16 @@ +#include +#include + +__global__ auto +cuda_hello() -> void +{ + printf("Hello World from GPU!\n"); +} + +auto +main() -> int +{ + cuda_hello<<<1, 1>>>(); + cudaDeviceSynchronize(); + return EXIT_SUCCESS; +} diff --git a/test/resources/repos/cpp/test_repo/CXX20/a.cpp b/test/resources/repos/cpp/test_repo/CXX20/a.cpp new file mode 100644 index 00000000..79d86039 --- /dev/null +++ b/test/resources/repos/cpp/test_repo/CXX20/a.cpp @@ -0,0 +1,9 @@ +#include + +import b; + +auto +main() noexcept -> int +{ + return EXIT_SUCCESS; +} diff --git a/test/resources/repos/cpp/test_repo/CXX20/b.cpp b/test/resources/repos/cpp/test_repo/CXX20/b.cpp new file mode 100644 index 00000000..1b95a679 --- /dev/null +++ b/test/resources/repos/cpp/test_repo/CXX20/b.cpp @@ -0,0 +1,17 @@ +module; + +#include + +module b; + +auto +add(auto x, auto y) -> decltype(x + y) +{ + return x + y; +} + +auto +add_f32(float x, float y) -> float +{ + return add(x, y); +} diff --git a/test/resources/repos/cpp/test_repo/CXX20/b.cppm b/test/resources/repos/cpp/test_repo/CXX20/b.cppm new file mode 100644 index 00000000..d1e7dcbe --- /dev/null +++ b/test/resources/repos/cpp/test_repo/CXX20/b.cppm @@ -0,0 +1,16 @@ +module; + +#include + +export module b; + +export auto +add(auto x, auto y) -> decltype(x + y); + +export auto +add_f32(float x, float y) -> float; + +auto +add_u32(std::int32_t x, std::int32_t y) -> std::int32_t { + return x + y; +} diff --git a/test/resources/repos/cpp/test_repo/HIP/a.hip b/test/resources/repos/cpp/test_repo/HIP/a.hip new file mode 100644 index 00000000..910b9876 --- /dev/null +++ b/test/resources/repos/cpp/test_repo/HIP/a.hip @@ -0,0 +1,10 @@ +// -*- mode: C -*- + +#include + +__global__ +void add_vectors(const float* A, const float* B, float* C) +{ + auto const idx = blockIdx.x * blockDim.x + threadIdx.x; + C[idx] = A[idx] + B[idx]; +} diff --git a/test/resources/repos/cpp/test_repo/Objective-C/a.m b/test/resources/repos/cpp/test_repo/Objective-C/a.m new file mode 100644 index 00000000..6487dfb8 --- /dev/null +++ b/test/resources/repos/cpp/test_repo/Objective-C/a.m @@ -0,0 +1,7 @@ +@interface Hello +- (void)hello; +@end + +int main(int argc, const char * argv[]) { + return 0; +} diff --git a/test/resources/repos/cpp/test_repo/OpenCL/a.cl b/test/resources/repos/cpp/test_repo/OpenCL/a.cl new file mode 100644 index 00000000..78fe1a68 --- /dev/null +++ b/test/resources/repos/cpp/test_repo/OpenCL/a.cl @@ -0,0 +1,8 @@ +// -*- mode: C -*- + +__kernel +void add_vectors(__global const float *A, __global const float *B, __global float *C) +{ + int idx = get_global_id(0); + C[idx] = A[idx] + B[idx]; +} diff --git a/test/resources/repos/cpp/test_repo/compile_commands.json b/test/resources/repos/cpp/test_repo/compile_commands.json index cb096a60..3ecc0e7d 100644 --- a/test/resources/repos/cpp/test_repo/compile_commands.json +++ b/test/resources/repos/cpp/test_repo/compile_commands.json @@ -13,5 +13,40 @@ "directory": ".", "command": "g++ -std=c++17 -I . -c diagnostics_sample.cpp", "file": "diagnostics_sample.cpp" + }, + { + "directory": "C", + "command": "clang -std=gnu23 -c a.c", + "file": "a.c" + }, + { + "directory": "CUDA", + "command": "nvcc -std=c++20 -c a.cu", + "file": "a.cu" + }, + { + "directory": "CXX20", + "command": "clang++ -std=gnu++20 -fmodule-file=b=b.pcm -c a.cpp", + "file": "a.cpp" + }, + { + "directory": "CXX20", + "command": "clang++ -std=gnu++20 -fmodule-file=b=b.pcm -c b.cpp", + "file": "b.cpp" + }, + { + "directory": "CXX20", + "command": "clang++ -std=gnu++20 -c b.cppm", + "file": "b.cppm" + }, + { + "directory": "OpenCL", + "command": "clang -std=cl3.0 -c a.cl", + "file": "a.cl" + }, + { + "directory": "HIP", + "command": "hipcc -c a.hip", + "file": "a.hip" } ] diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 3e1829f3..c1e2578f 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -327,7 +327,7 @@ FIND_DEFINING_SYMBOL_CASES = [ expected_definition_file="utils.ps1", ).to_pytest_param(), FindDefiningSymbolCase( - language=Language.CPP_CCLS, + language=Language.CPP, id="cpp_add_in_a", relative_path="a.cpp", identifier="add", @@ -502,7 +502,7 @@ FIND_SYMBOL_REFERENCES_CASES = [ expected_file="main.ps1", ).to_pytest_param(), FindSymbolCase( - language=Language.CPP_CCLS, id="cpp_add_function", symbol_name="add", expected_kind="Function", expected_file="b.cpp" + language=Language.CPP, id="cpp_add_function", symbol_name="add", expected_kind="Function", expected_file="b.cpp" ).to_pytest_param(), FindSymbolCase( language=Language.LEAN4, id="lean_add_method", symbol_name="add", expected_kind="Method", expected_file="Helper.lean" @@ -580,7 +580,7 @@ FIND_REFERENCE_CASES = [ reference_file="main.ps1", ).to_pytest_param(), FindReferenceCase( - language=Language.CPP_CCLS, id="cpp_add_refs", symbol_name="add", definition_file="b.cpp", reference_file="a.cpp" + language=Language.CPP, id="cpp_add_refs", symbol_name="add", definition_file="b.cpp", reference_file="a.cpp" ).to_pytest_param(), FindReferenceCase( language=Language.LEAN4, id="lean_add_refs", symbol_name="add", definition_file="Helper.lean", reference_file="Main.lean" @@ -775,7 +775,7 @@ def serena_config(): Language.CLOJURE, Language.FSHARP, Language.POWERSHELL, - Language.CPP_CCLS, + Language.CPP, Language.HAXE, Language.LEAN4, Language.MSL, diff --git a/test/solidlsp/cpp/test_ccls_languages.py b/test/solidlsp/cpp/test_ccls_languages.py new file mode 100644 index 00000000..70cfac6a --- /dev/null +++ b/test/solidlsp/cpp/test_ccls_languages.py @@ -0,0 +1,24 @@ +import os + +import pytest + +from solidlsp.ls_config import Language +from test.conftest import start_ls_context + + +@pytest.mark.cpp +class TestCCLSLanguages: + @pytest.mark.parametrize( + "lang, unit, names", + [ + ("C", "a.c", {"main"}), + ("Objective-C", "a.m", {"main", "Hello", "-hello"}), + ], + ) + def test_get_document_symbols(self, lang: str, unit: str, names: set[str]) -> None: + with start_ls_context(Language.CPP) as ccls: + path = os.path.join(lang, unit) + symbols = ccls.request_document_symbols(path).get_all_symbols_and_roots() + symbols = symbols[0] if symbols and isinstance(symbols[0], list) else symbols + symbols = {s.get("name") for s in symbols} + assert names == symbols, f"Expected '{names}' in document symbols, got: {symbols}" diff --git a/test/solidlsp/cpp/test_clangd_languages.py b/test/solidlsp/cpp/test_clangd_languages.py new file mode 100644 index 00000000..8bf3a30f --- /dev/null +++ b/test/solidlsp/cpp/test_clangd_languages.py @@ -0,0 +1,30 @@ +import os + +import pytest + +from solidlsp.ls_config import Language +from test.conftest import start_ls_context + + +@pytest.mark.cpp +class TestClangdLanguages: + @pytest.mark.parametrize( + "lang, unit, names", + [ + ("C", "a.c", {"main"}), + ("CUDA", "a.cu", {"main", "cuda_hello"}), + ("CXX20", "a.cpp", {"main"}), + ("CXX20", "b.cpp", {"add", "add_f32"}), + ("CXX20", "b.cppm", {"add", "add_f32", "add_u32"}), + ("HIP", "a.hip", {"add_vectors"}), + ("OpenCL", "a.cl", {"add_vectors"}), + ("Objective-C", "a.m", {"main", "Hello", "-hello"}), + ], + ) + def test_get_document_symbols(self, lang: str, unit: str, names: set[str]) -> None: + with start_ls_context(Language.CPP) as clangd: + path = os.path.join(lang, unit) + symbols = clangd.request_document_symbols(path).get_all_symbols_and_roots() + symbols = symbols[0] if symbols and isinstance(symbols[0], list) else symbols + symbols = {s.get("name") for s in symbols} + assert names == symbols, f"Expected '{names}' in document symbols, got: {symbols}" diff --git a/test/solidlsp/cpp/test_cpp_basic.py b/test/solidlsp/cpp/test_cpp_basic.py index dfb18881..21b90452 100644 --- a/test/solidlsp/cpp/test_cpp_basic.py +++ b/test/solidlsp/cpp/test_cpp_basic.py @@ -16,16 +16,11 @@ import pytest from solidlsp import SolidLanguageServer from solidlsp.ls_config import Language from solidlsp.ls_utils import SymbolUtils -from test.conftest import get_repo_path, start_ls_context +from test.conftest import get_repo_path, language_tests_enabled, start_ls_context from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols - -def _ccls_available() -> bool: - return shutil.which("ccls") is not None - - _cpp_servers: list[Language] = [Language.CPP] -if _ccls_available(): +if language_tests_enabled(Language.CPP_CCLS): _cpp_servers.append(Language.CPP_CCLS)