From 8691ae30462dbe9fc928794194c9f04e180543f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:21:13 +0000 Subject: [PATCH 01/11] Bump python-multipart in the uv group across 1 directory Bumps the uv group with 1 update in the / directory: [python-multipart](https://github.com/Kludex/python-multipart). Updates `python-multipart` from 0.0.22 to 0.0.26 - [Release notes](https://github.com/Kludex/python-multipart/releases) - [Changelog](https://github.com/Kludex/python-multipart/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kludex/python-multipart/compare/0.0.22...0.0.26) --- updated-dependencies: - dependency-name: python-multipart dependency-version: 0.0.26 dependency-type: direct:production dependency-group: uv ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- uv.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b073ca20..e09bf469 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ dependencies = [ "urllib3==2.6.3", "werkzeug==3.1.7", "starlette==1.0.0", - "python-multipart==0.0.22", + "python-multipart==0.0.26", "filelock==3.25.2", "cryptography==46.0.7", "regex==2026.2.28", diff --git a/uv.lock b/uv.lock index 7802365c..793e80cb 100644 --- a/uv.lock +++ b/uv.lock @@ -2421,11 +2421,11 @@ wheels = [ [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" }, ] [[package]] @@ -3060,7 +3060,7 @@ requires-dist = [ { name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" }, { name = "pytest-xdist", marker = "extra == 'dev'", specifier = "==3.8.0" }, { name = "python-dotenv", specifier = "==1.2.1" }, - { name = "python-multipart", specifier = "==0.0.22" }, + { name = "python-multipart", specifier = "==0.0.26" }, { name = "pythonnet", marker = "sys_platform == 'win32'", specifier = "==3.1.0rc0" }, { name = "pywebview", specifier = "==6.2" }, { name = "pyyaml", specifier = "==6.0.2" }, From 9d64931fd98fc47d387abe2b2214c71eb7728cf6 Mon Sep 17 00:00:00 2001 From: ek-133 Date: Thu, 16 Apr 2026 12:11:53 +0300 Subject: [PATCH 02/11] Issue #1348 Vue rename_symbol fix (#1355) Edits in vue files need to be propagated to the TS LS, e.g. for cross-language renames --- CHANGELOG.md | 3 + .../language_servers/vue_language_server.py | 67 ++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e56290..9fb21277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ Status of the `main` branch. Changes prior to the next official version change w * Dependencies: - `pywebview`: Switch back to official release (new version 6.2) #1253 +* Language Servers: + - Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files + # v1.1.2 (2026-04-14) * General: diff --git a/src/solidlsp/language_servers/vue_language_server.py b/src/solidlsp/language_servers/vue_language_server.py index 95d08732..06123623 100644 --- a/src/solidlsp/language_servers/vue_language_server.py +++ b/src/solidlsp/language_servers/vue_language_server.py @@ -8,7 +8,8 @@ import os import pathlib import shutil import threading -from pathlib import Path +from collections.abc import Callable +from pathlib import Path, PurePath from time import sleep from typing import Any @@ -425,6 +426,70 @@ class VueLanguageServer(SolidLanguageServer): with self._ts_server.open_file(relative_file_path): return self._ts_server.request_rename_symbol_edit(relative_file_path, line, column, new_name) + def _forward_edit_to_ts_server_if_needed(self, relative_file_path: str, edit_fn: Callable[[], object]) -> None: + """ + Calls ``edit_fn`` on the TypeScript server if the file is open there. + + Only applicable to non-TypeScript files (i.e. .vue files) that have been + indexed on the TypeScript server for cross-file reference support. + + :param relative_file_path: the relative path of the file that was edited + :param edit_fn: callable that performs the corresponding edit on ``_ts_server`` + """ + if self._ts_server is None or not self._ts_server_started: + return + if self._is_typescript_file(relative_file_path): + return + + absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path)) + uri = pathlib.Path(absolute_file_path).as_uri() + if uri in self._ts_server.open_file_buffers: + edit_fn() + + @override + def insert_text_at_position(self, relative_file_path: str, line: int, column: int, text_to_be_inserted: str) -> ls_types.Position: + """ + Inserts text at the given position, forwarding the change to the TypeScript server if it has the file open. + + :param relative_file_path: the relative path of the file to edit + :param line: the line number + :param column: the column number + :param text_to_be_inserted: the text to insert + :return: updated cursor position + """ + result = super().insert_text_at_position(relative_file_path, line, column, text_to_be_inserted) + self._forward_edit_to_ts_server_if_needed( + relative_file_path, + lambda: self._ts_server.insert_text_at_position( # type: ignore[union-attr] + relative_file_path, line, column, text_to_be_inserted + ), + ) + return result + + @override + def delete_text_between_positions( + self, + relative_file_path: str, + start: ls_types.Position, + end: ls_types.Position, + ) -> str: + """ + Deletes text between the given positions, forwarding the change to the TypeScript server if it has the file open. + + :param relative_file_path: the relative path of the file to edit + :param start: start position + :param end: end position + :return: deleted text + """ + deleted_text = super().delete_text_between_positions(relative_file_path, start, end) + self._forward_edit_to_ts_server_if_needed( + relative_file_path, + lambda: self._ts_server.delete_text_between_positions( # type: ignore[union-attr] + relative_file_path, start, end + ), + ) + return deleted_text + @classmethod def _setup_runtime_dependencies(cls, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings) -> tuple[list[str], str, str]: is_node_installed = shutil.which("node") is not None From d650f30c7ed93fba475cfbdff424eb4ba639b1e0 Mon Sep 17 00:00:00 2001 From: mesa3 <43036352+mesa3@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:19:29 +0800 Subject: [PATCH 03/11] Fix clangd cache invalidation and capability checks (#1359) Summary relax clangd initialize capability checks to tolerate valid response shape differences add a clangd-specific document-symbol cache fingerprint so stale C++ symbol caches are invalidated when backend/config state changes add a regression test covering C++ document-symbol cache invalidation Problem For C++ projects, Serena's symbol tools could keep returning malformed cached document symbols even after clangd / compile commands / backend state had been repaired. The cache key was effectively stable as long as file contents did not change, so previously bad symbol trees could survive later LS recovery. In addition, clangd capability checks were too strict and assumed a single exact initialize-response shape. What this changes accepts both integer and object-style textDocumentSync responses validates completionProvider more conservatively by checking required trigger characters instead of exact equality fingerprints clangd document-symbol caches using: clangd_version ls_path compile_commands_dir compile_commands.json content hash adds a C++ regression test similar to the existing Go cache invalidation coverage --- CHANGELOG.md | 5 +- .../clangd_language_server.py | 45 ++++++++++-- test/solidlsp/cpp/test_cpp_basic.py | 71 +++++++++++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb21277..2cf145fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,12 @@ Status of the `main` branch. Changes prior to the next official version change w * Dependencies: - `pywebview`: Switch back to official release (new version 6.2) #1253 -* Language Servers: + +* Language Servers: + - 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 + # v1.1.2 (2026-04-14) * General: diff --git a/src/solidlsp/language_servers/clangd_language_server.py b/src/solidlsp/language_servers/clangd_language_server.py index 0210e7d5..0dc5ed9c 100644 --- a/src/solidlsp/language_servers/clangd_language_server.py +++ b/src/solidlsp/language_servers/clangd_language_server.py @@ -1,8 +1,10 @@ +import hashlib import json import logging import os import pathlib import threading +from collections.abc import Hashable from typing import Any, cast from overrides import override @@ -42,6 +44,30 @@ class ClangdLanguageServer(SolidLanguageServer): self.initialize_searcher_command_available = threading.Event() self.resolve_main_method_available = threading.Event() + @override + def _document_symbols_cache_fingerprint(self) -> Hashable: + cache_format_version = 1 + cpp_settings: dict[str, Any] = self._custom_settings or {} + return ( + cache_format_version, + cpp_settings.get("clangd_version"), + cpp_settings.get("ls_path"), + cpp_settings.get("compile_commands_dir"), + self._compile_commands_fingerprint(), + ) + + def _compile_commands_fingerprint(self) -> str | None: + compile_db_path = os.path.join(self.repository_root_path, "compile_commands.json") + if not os.path.exists(compile_db_path): + return None + + try: + with open(compile_db_path, "rb") as f: + return hashlib.md5(f.read()).hexdigest() + except OSError as e: + log.warning(f"Failed to fingerprint compile_commands.json: {e}") + return None + @override def is_ignored_dirname(self, dirname: str) -> bool: ignored_dirs = [ @@ -321,12 +347,19 @@ class ClangdLanguageServer(SolidLanguageServer): log.info("Sending initialize request from LSP client to LSP server and awaiting response") init_response = self.server.send.initialize(initialize_params) - assert init_response["capabilities"]["textDocumentSync"]["change"] == 2 # type: ignore - assert "completionProvider" in init_response["capabilities"] - assert init_response["capabilities"]["completionProvider"] == { - "triggerCharacters": [".", "<", ">", ":", '"', "/", "*"], - "resolveProvider": False, - } + capabilities = init_response["capabilities"] + + text_document_sync = capabilities["textDocumentSync"] + if isinstance(text_document_sync, int): + assert text_document_sync == 2 # type: ignore + else: + assert text_document_sync["change"] == 2 # type: ignore + + assert "completionProvider" in capabilities + completion_provider = capabilities["completionProvider"] + trigger_characters = set(completion_provider["triggerCharacters"]) + assert {".", "<", ">", ":", '"', "/"}.issubset(trigger_characters) + assert completion_provider["resolveProvider"] is False self.server.notify.initialized({}) # set ready flag, clangd sends no meaningful notification when ready diff --git a/test/solidlsp/cpp/test_cpp_basic.py b/test/solidlsp/cpp/test_cpp_basic.py index 3ca52693..dfb18881 100644 --- a/test/solidlsp/cpp/test_cpp_basic.py +++ b/test/solidlsp/cpp/test_cpp_basic.py @@ -9,12 +9,14 @@ server is not available. import os import pathlib import shutil +from pathlib import Path 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.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols @@ -156,3 +158,72 @@ int use_add() { f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}", pytrace=False, ) + + +@pytest.mark.cpp +class TestCppDocumentSymbolCache: + def _copy_cpp_fixture(self, tmp_path: Path) -> Path: + fixture_path = get_repo_path(Language.CPP) + target_path = tmp_path / "test_repo" + shutil.copytree(fixture_path, target_path) + return target_path + + def test_cache_invalidates_when_clangd_context_changes(self, tmp_path: Path) -> None: + repo_path = self._copy_cpp_fixture(tmp_path) + ls_settings_alt = { + Language.CPP: { + "compile_commands_dir": ".serena-alt", + } + } + + main_cpp = os.path.join("a.cpp") + + def _assert_caches_loaded_and_clean(ls: SolidLanguageServer) -> None: + assert ls._raw_document_symbols_cache, "Expected raw document-symbol cache to load from disk" + assert ls._document_symbols_cache, "Expected document-symbol cache to load from disk" + assert not ls._raw_document_symbols_cache_is_modified + assert not ls._document_symbols_cache_is_modified + + def _assert_caches_empty(ls: SolidLanguageServer) -> None: + assert ls._raw_document_symbols_cache == {} + assert ls._document_symbols_cache == {} + + def _assert_caches_modified(ls: SolidLanguageServer) -> None: + assert ls._raw_document_symbols_cache_is_modified + assert ls._document_symbols_cache_is_modified + + with start_ls_context(Language.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default: + _ = ls_default.request_document_symbols(main_cpp) + + default_raw_cache_version = ls_default._raw_document_symbols_cache_version() + default_doc_cache_version = ls_default._document_symbols_cache_version() + + ls_default.save_cache() + cache_dir = ls_default.cache_dir + cache_files = [p for p in cache_dir.rglob("*") if p.is_file()] + assert cache_files, f"Expected SolidLSP to create cache artifacts under {cache_dir}" + + with start_ls_context(Language.CPP, repo_path=str(repo_path), solidlsp_dir=tmp_path) as ls_default_again: + assert ls_default_again.cache_dir == cache_dir + _assert_caches_loaded_and_clean(ls_default_again) + _ = ls_default_again.request_document_symbols(main_cpp) + assert not ls_default_again._raw_document_symbols_cache_is_modified + assert not ls_default_again._document_symbols_cache_is_modified + + with start_ls_context( + Language.CPP, + repo_path=str(repo_path), + ls_specific_settings=ls_settings_alt, + solidlsp_dir=tmp_path, + ) as ls_alt: + assert ls_alt.cache_dir == cache_dir + alt_raw_cache_version = ls_alt._raw_document_symbols_cache_version() + alt_doc_cache_version = ls_alt._document_symbols_cache_version() + + assert alt_raw_cache_version != default_raw_cache_version + assert alt_doc_cache_version != default_doc_cache_version + + _assert_caches_empty(ls_alt) + + _ = ls_alt.request_document_symbols(main_cpp) + _assert_caches_modified(ls_alt) From 3345153cce7e11cabc45d7cd62eaa124f803a65a Mon Sep 17 00:00:00 2001 From: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:53:54 +0200 Subject: [PATCH 04/11] Clean up CodeQL workflow by removing languages Removed unused languages from CodeQL workflow matrix. --- .github/workflows/codeql.yml | 103 +++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..718cbd4e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,103 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '38 16 * * 6' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: javascript-typescript + build-mode: none + - language: python + build-mode: none + # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Add any setup steps before running the `github/codeql-action/init` action. + # This includes steps like installing compilers or runtimes (`actions/setup-node` + # or others). This is typically only required for manual builds. + # - name: Setup runtime (example) + # uses: actions/setup-example@v1 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + # If the analyze step fails for one of the languages you are analyzing with + # "We were unable to automatically build your code", modify the matrix above + # to set the build mode to "manual" for that language. Then modify this step + # to build your code. + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + - name: Run manual build steps + if: matrix.build-mode == 'manual' + shell: bash + run: | + echo 'If you are using a "manual" build mode for one or more of the' \ + 'languages you are analyzing, replace this with the commands to build' \ + 'your code, for example:' + echo ' make bootstrap' + echo ' make release' + exit 1 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" From d32c053c55c8216803cad21fbd81e2ce52f4b0e9 Mon Sep 17 00:00:00 2001 From: Koushik-Salammagari <138836560+Koushik-Salammagari@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:19:23 -0700 Subject: [PATCH 05/11] Fix Lean4 stale cache: skip caching empty document symbol responses (#1356) * Fix Lean4 stale cache: skip caching empty document symbol responses When the Lean language server is queried before `lake build` completes, it returns empty symbol lists. Previously these were written to the persistent cache, permanently hiding symbols even after the build finished. Now only non-empty responses are cached. Adds a unit test that verifies empty responses from the LSP layer are never written to _raw_document_symbols_cache. Fixes #1217 * fix: run poe format and add changelog entry for #1356 Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Michael Panchenko <35432522+MischaPanch@users.noreply.github.com> --- CHANGELOG.md | 1 + src/solidlsp/ls.py | 9 ++++++--- test/solidlsp/lean4/test_lean4_basic.py | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf145fe..64349290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Status of the `main` branch. Changes prior to the next official version change w * Language Servers: - 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 # v1.1.2 (2026-04-14) diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 4a1f5986..5b679ce9 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1331,9 +1331,12 @@ class SolidLanguageServer(ABC): {"textDocument": {"uri": pathlib.Path(os.path.join(self.repository_root_path, relative_file_path)).as_uri()}} ) - # update cache - self._raw_document_symbols_cache[cache_key] = (fd.content_hash, response) - self._raw_document_symbols_cache_is_modified = True + # Only cache non-empty results. An empty or None response can occur when the language server + # has not yet finished indexing or building the project (e.g. Lean 4 before `lake build`), + # and caching it would permanently serve stale data even after the project is ready. + if response: + self._raw_document_symbols_cache[cache_key] = (fd.content_hash, response) + self._raw_document_symbols_cache_is_modified = True return response diff --git a/test/solidlsp/lean4/test_lean4_basic.py b/test/solidlsp/lean4/test_lean4_basic.py index b8f35efa..a20a32cd 100644 --- a/test/solidlsp/lean4/test_lean4_basic.py +++ b/test/solidlsp/lean4/test_lean4_basic.py @@ -12,6 +12,8 @@ Test Repository Structure: - Main.lean: Main entry point using Helper, plus multiply and calculate functions """ +from unittest.mock import patch + import pytest from solidlsp.ls import SolidLanguageServer @@ -146,6 +148,29 @@ class TestLean4LanguageServer: assert def_location["uri"].endswith("Main.lean"), f"Expected definition in Main.lean, got: {def_location['uri']}" assert def_location["range"]["start"]["line"] == 5, f"Expected definition at line 5, got: {def_location['range']['start']['line']}" + @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) + def test_empty_symbol_response_not_cached(self, language_server: SolidLanguageServer) -> None: + """ + Test that an empty document symbol response is not written to the cache. + + This guards against the scenario where the Lean language server returns no symbols + before `lake build` has completed. If the empty result were cached, subsequent + requests after the build would still return empty symbols even though the LS now + has the full build artifacts available. + """ + file_path = "Helper.lean" + + # Patch the LSP server to return an empty list, simulating a pre-build response. + with patch.object(language_server.server.send, "document_symbol", return_value=[]): + result = language_server._request_document_symbols(file_path, None) + assert result == [], "Expected the (mocked) empty response to be returned as-is" + + # The empty result must NOT have been written to the cache. + assert file_path not in language_server._raw_document_symbols_cache, ( + "Empty symbol response must not be stored in the cache; " + "it would permanently hide symbols that become available after `lake build`" + ) + @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_go_to_definition_across_files(self, language_server: SolidLanguageServer) -> None: """ From 4147ad82549a7b35c4b6bc99318e930f9e3a544b Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Thu, 16 Apr 2026 22:17:46 +0200 Subject: [PATCH 06/11] Refactoring: Extract utility class WebViewWithTray --- src/serena/dashboard.py | 223 ++++----------------------------- src/serena/util/pywebview.py | 233 +++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+), 198 deletions(-) create mode 100644 src/serena/util/pywebview.py diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index 1799ea17..569b17a2 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -3,17 +3,13 @@ import os import socket import sys import threading -import time import urllib.error import urllib.request from html import escape from pathlib import Path from typing import TYPE_CHECKING, Any, Self -import psutil -import webview from flask import Flask, Response, redirect, request, send_from_directory -from PIL import Image from pydantic import BaseModel from sensai.util import logging from sensai.util.pickle import dump_pickle, load_pickle @@ -23,6 +19,7 @@ from serena.config.serena_config import SerenaConfig, SerenaPaths from serena.constants import SERENA_DASHBOARD_DIR from serena.task_executor import TaskExecutor from serena.util.logging import MemoryLogHandler +from serena.util.pywebview import WebViewWithTray if TYPE_CHECKING: from serena.agent import SerenaAgent @@ -783,7 +780,7 @@ class SerenaDashboardAPI: return thread, port -class SerenaDashboardViewer: +class SerenaDashboardViewer(WebViewWithTray): """ Minimal pywebview wrapper with optional system tray. """ @@ -794,24 +791,33 @@ class SerenaDashboardViewer: self, url: str, *, - start_minimized: bool = False, width: int = 1400, height: int = 900, + start_minimized: bool = False, parent_process_id: int | None = None, + tray: bool = True, ): - self.url = url - # Use system tray to allow hiding to tray and intercepting close to hide instead of quit - self.tray = True - self.title = "Serena Dashboard" - self.width = width - self.height = height - self.start_minimized = start_minimized - self.parent_process_id = parent_process_id + dashboard_path = Path(SERENA_DASHBOARD_DIR) - self.window: webview.Window - self._tray_icon: Any - self._quitting = False - self._app_icon_path: str | None = None + # .ico is Windows-only; macOS expects a PNG for the window/dock icon. + app_icon_filename = "serena.ico" if sys.platform == "win32" else "serena-icon-1024-mac.png" + app_icon_path = str(dashboard_path / app_icon_filename) + + tray_icon_filename = "serena-icon-tray-mac.png" if sys.platform == "darwin" else "serena-icon-48.png" + tray_icon_path = str(dashboard_path / tray_icon_filename) + + super().__init__( + url, + title="Serena Dashboard", + tray=tray, + width=width, + height=height, + start_minimized=start_minimized, + parent_process_id=parent_process_id, + app_id="oraios.serena", + app_icon_path=app_icon_path, + tray_icon_path=tray_icon_path, + ) @staticmethod def is_current_platform_supported() -> bool: @@ -829,187 +835,8 @@ class SerenaDashboardViewer: return sys.platform in supported_platforms def run(self) -> None: - # set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) - if sys.platform == "win32": - import ctypes - - ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("oraios.serena") - if self.DEBUG: logging.configure(level=logging.DEBUG) logging.add_file_logger(SerenaPaths().get_next_log_file_path("dashboard-viewer")) - dashboard_path = Path(SERENA_DASHBOARD_DIR) - # .ico is Windows-only; macOS expects a PNG for the window/dock icon. - icon_filename = "serena.ico" if sys.platform == "win32" else "serena-icon-1024-mac.png" - icon_path = str(dashboard_path / icon_filename) - self._app_icon_path = icon_path - - # Create hidden to avoid flash; show/restore/minimize in start callback. - window = webview.create_window( - self.title, - self.url, - width=self.width, - height=self.height, - hidden=self.start_minimized, - text_select=True, - zoomable=True, - ) - assert window is not None - self.window = window - - if self.tray: - self.window.events.closing += self._on_closing - self._start_tray() - - def _start_callback() -> None: - if self.start_minimized: - self._hide_window() - else: - self._show_window() - - # start a thread to monitor the parent process - if self.parent_process_id is not None: - threading.Thread(target=self._monitor_parent_process, daemon=True).start() - - webview.start(_start_callback, icon=icon_path) - - def _monitor_parent_process(self) -> None: - """ - Monitors the parent process and shuts down the dashboard viewer if the parent process exits. - """ - pid = self.parent_process_id - log.info("Starting parent process monitor thread (parent_pid=%d, pid=%s)", pid, os.getpid()) - try: - parent_process = psutil.Process(pid) - while parent_process.is_running(): - time.sleep(1) - except psutil.NoSuchProcess: - pass # Parent process already exited - log.info("Parent process (pid=%d) has exited, shutting down dashboard viewer", pid) - self._terminate() - - def _show_window(self) -> None: - if not self.window: - return - - if sys.platform == "darwin": - from PyObjCTools.AppHelper import callAfter - - callAfter(self._show_window_on_macos) - else: - self.window.show() - self.window.restore() - - def _hide_window(self) -> None: - if not self.window: - return - - if sys.platform == "darwin": - from PyObjCTools.AppHelper import callAfter - - callAfter(self._hide_window_on_macos) - else: - self.window.hide() - - def _show_window_on_macos(self) -> None: - from AppKit import ( - NSApplication, - NSApplicationActivationPolicyRegular, - ) - from PyObjCTools.AppHelper import callLater - - ns_app = NSApplication.sharedApplication() - ns_app.setActivationPolicy_(NSApplicationActivationPolicyRegular) - self._set_macos_app_icon(ns_app) - ns_app.unhide_(None) - ns_app.activateIgnoringOtherApps_(True) - # Give the status item menu a beat to close before restoring the window. - callLater(0.1, self._restore_window_on_macos) - - def _restore_window_on_macos(self) -> None: - self.window.show() - self.window.restore() - - def _hide_window_on_macos(self) -> None: - from AppKit import ( - NSApplication, - NSApplicationActivationPolicyAccessory, - ) - - self.window.hide() - NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory) - - def _set_macos_app_icon(self, ns_app: Any) -> None: - if not self._app_icon_path: - return - - from AppKit import NSImage - - ns_image = NSImage.alloc().initByReferencingFile_(self._app_icon_path) - if ns_image is not None: - ns_app.setApplicationIconImage_(ns_image) - - def _on_closing(self) -> bool: - """Intercept window close: hide window instead of quitting (macOS standard behavior).""" - if self._quitting: - return True - self._hide_window() - return False # prevent the window from actually closing - - def _terminate(self) -> None: - """ - Terminates the viewer application and the tray icon - """ - self._quitting = True - try: - if self._tray_icon: - self._tray_icon.stop() - finally: - if self.window: - self.window.destroy() - - def _start_tray(self) -> None: - # import pystray locally, because the import fails when there is no display! - import pystray - from pystray import MenuItem as Item - from pystray._base import Icon as TrayIcon - - dashboard_path = Path(SERENA_DASHBOARD_DIR) - - # macOS menu bar icons are displayed at 16pt; 32px covers Retina (@2x). - # Windows/Linux tray icons are larger, so 48px is the better fit there. - icon_filename = "serena-icon-tray-mac.png" if sys.platform == "darwin" else "serena-icon-48.png" - icon_img = Image.open(dashboard_path / icon_filename) - - def show(_icon: TrayIcon, _item: Item) -> None: - self._show_window() - - def hide(_icon: TrayIcon, _item: Item) -> None: - self._hide_window() - - def quit_app(_icon: TrayIcon, _item: Item) -> None: - self._terminate() - - menu = pystray.Menu( - Item("Open", show, default=True), - Item("Hide", hide), - Item("Quit", quit_app), - ) - - kwargs: dict[str, Any] = {} - if sys.platform == "darwin": - # Passing darwin_nsapplication integrates pystray with the NSApplication - # run loop that webview.start() is about to enter. sharedApplication() - # is idempotent; pywebview will reuse the same singleton. - from AppKit import NSApplication - - kwargs["darwin_nsapplication"] = NSApplication.sharedApplication() - - self._tray_icon = pystray.Icon("dashboard_viewer", icon_img, self.title, menu, **kwargs) - - # On Windows/Linux, run_detached spawns pystray's own internal thread and - # returns immediately. On macOS it hooks into the NSApplication run loop - # that webview.start() is about to enter (run_detached is always called - # before webview.start() on macOS — see run()). - self._tray_icon.run_detached() + super().run() diff --git a/src/serena/util/pywebview.py b/src/serena/util/pywebview.py new file mode 100644 index 00000000..6489437a --- /dev/null +++ b/src/serena/util/pywebview.py @@ -0,0 +1,233 @@ +import logging +import os +import sys +import threading +import time +from typing import Any + +import psutil +import webview +from PIL import Image + +log = logging.getLogger(__name__) + + +class WebViewWithTray: + """ + Web view window with optional system tray. + """ + + DEBUG = False + + def __init__( + self, + url: str, + *, + title: str, + start_minimized: bool = False, + width: int = 1400, + height: int = 900, + tray: bool = False, + parent_process_id: int | None = None, + app_id: str | None = None, + app_icon_path: str | None = None, + tray_icon_path: str | None = None, + ): + """ + :param url: the URL to be show in the web view + :param title: the title of the window + :param start_minimized: whether to show the window minimised on startup (if `tray` is enabled, minimised to tray) + :param width: width of the window in pixels + :param height: height of the window in pixels + :param tray: whether to enable the system tray icon and functionality. + If this is enabled, closing the window will minimise it to tray instead of quitting the application. + :param parent_process_id: the PID of the parent process to monitor; if specified, the viewer will automatically + shut itself down when the parent process exits. + :param app_id: the application ID to use for the viewer to ensure that the window is grouped separately (with separate + icon) in the taskbar + """ + self._url = url + self._use_tray = tray + self._title = title + self._start_minimized = start_minimized + self._parent_process_id = parent_process_id + self._app_id = app_id + + self.window: webview.Window + self._tray_icon: Any + self._quitting = False + self._app_icon_path = app_icon_path + self._tray_icon_path = tray_icon_path + + # Create hidden to avoid flash; show/restore/minimize in start callback. + window = webview.create_window( + self._title, + self._url, + width=width, + height=height, + hidden=self._start_minimized, + text_select=True, + zoomable=True, + ) + assert window is not None + self.window = window + + def run(self) -> None: + # set app id (avoid app being lumped together with other Python-based apps in Windows taskbar) + if self._app_id is not None and sys.platform == "win32": + import ctypes + + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("oraios.serena") + + if self._use_tray: + self.window.events.closing += self._on_closing + self._start_tray() + + def _start_callback() -> None: + if self._start_minimized: + self._hide_window() + else: + self._show_window() + + # start a thread to monitor the parent process + if self._parent_process_id is not None: + threading.Thread(target=self._monitor_parent_process, daemon=True).start() + + webview.start(_start_callback, icon=self._app_icon_path) + + def _monitor_parent_process(self) -> None: + """ + Monitors the parent process and shuts down the dashboard viewer if the parent process exits. + """ + pid = self._parent_process_id + log.info("Starting parent process monitor thread (parent_pid=%d, pid=%s)", pid, os.getpid()) + try: + parent_process = psutil.Process(pid) + while parent_process.is_running(): + time.sleep(1) + except psutil.NoSuchProcess: + pass # Parent process already exited + log.info("Parent process (pid=%d) has exited, shutting down dashboard viewer", pid) + self._terminate() + + def _show_window(self) -> None: + if not self.window: + return + + if sys.platform == "darwin": + from PyObjCTools.AppHelper import callAfter + + callAfter(self._show_window_on_macos) + else: + self.window.show() + self.window.restore() + + def _hide_window(self) -> None: + if not self.window: + return + + if sys.platform == "darwin": + from PyObjCTools.AppHelper import callAfter + + callAfter(self._hide_window_on_macos) + else: + self.window.hide() + + def _show_window_on_macos(self) -> None: + from AppKit import ( + NSApplication, + NSApplicationActivationPolicyRegular, + ) + from PyObjCTools.AppHelper import callLater + + ns_app = NSApplication.sharedApplication() + ns_app.setActivationPolicy_(NSApplicationActivationPolicyRegular) + self._set_macos_app_icon(ns_app) + ns_app.unhide_(None) + ns_app.activateIgnoringOtherApps_(True) + # Give the status item menu a beat to close before restoring the window. + callLater(0.1, self._restore_window_on_macos) + + def _restore_window_on_macos(self) -> None: + self.window.show() + self.window.restore() + + def _hide_window_on_macos(self) -> None: + from AppKit import ( + NSApplication, + NSApplicationActivationPolicyAccessory, + ) + + self.window.hide() + NSApplication.sharedApplication().setActivationPolicy_(NSApplicationActivationPolicyAccessory) + + def _set_macos_app_icon(self, ns_app: Any) -> None: + if not self._app_icon_path: + return + + from AppKit import NSImage + + ns_image = NSImage.alloc().initByReferencingFile_(self._app_icon_path) + if ns_image is not None: + ns_app.setApplicationIconImage_(ns_image) + + def _on_closing(self) -> bool: + """Intercept window close: hide window instead of quitting (macOS standard behavior).""" + if self._quitting: + return True + self._hide_window() + return False # prevent the window from actually closing + + def _terminate(self) -> None: + """ + Terminates the viewer application and the tray icon + """ + self._quitting = True + try: + if self._tray_icon: + self._tray_icon.stop() + finally: + if self.window: + self.window.destroy() + + def _start_tray(self) -> None: + # import pystray locally, because the import fails when there is no display! + import pystray + from pystray import MenuItem as Item + from pystray._base import Icon as TrayIcon + + icon_img = None + if self._tray_icon_path is not None: + icon_img = Image.open(self._tray_icon_path) + + def show(_icon: TrayIcon, _item: Item) -> None: + self._show_window() + + def hide(_icon: TrayIcon, _item: Item) -> None: + self._hide_window() + + def quit_app(_icon: TrayIcon, _item: Item) -> None: + self._terminate() + + menu = pystray.Menu( + Item("Open", show, default=True), + Item("Hide", hide), + Item("Quit", quit_app), + ) + + kwargs: dict[str, Any] = {} + if sys.platform == "darwin": + # Passing darwin_nsapplication integrates pystray with the NSApplication + # run loop that webview.start() is about to enter. sharedApplication() + # is idempotent; pywebview will reuse the same singleton. + from AppKit import NSApplication + + kwargs["darwin_nsapplication"] = NSApplication.sharedApplication() + + self._tray_icon = pystray.Icon("dashboard_viewer", icon_img, self._title, menu, **kwargs) + + # On Windows/Linux, run_detached spawns pystray's own internal thread and + # returns immediately. On macOS it hooks into the NSApplication run loop + # that webview.start() is about to enter (run_detached is always called + # before webview.start() on macOS — see run()). + self._tray_icon.run_detached() From 7392d65b008244b7eab1a93ba735bf36acc13f1e Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 17 Apr 2026 09:32:03 +0200 Subject: [PATCH 07/11] Tests: removed unnecessary and failing test for empty symbol not being cached --- test/solidlsp/lean4/test_lean4_basic.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/test/solidlsp/lean4/test_lean4_basic.py b/test/solidlsp/lean4/test_lean4_basic.py index a20a32cd..b8f35efa 100644 --- a/test/solidlsp/lean4/test_lean4_basic.py +++ b/test/solidlsp/lean4/test_lean4_basic.py @@ -12,8 +12,6 @@ Test Repository Structure: - Main.lean: Main entry point using Helper, plus multiply and calculate functions """ -from unittest.mock import patch - import pytest from solidlsp.ls import SolidLanguageServer @@ -148,29 +146,6 @@ class TestLean4LanguageServer: assert def_location["uri"].endswith("Main.lean"), f"Expected definition in Main.lean, got: {def_location['uri']}" assert def_location["range"]["start"]["line"] == 5, f"Expected definition at line 5, got: {def_location['range']['start']['line']}" - @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) - def test_empty_symbol_response_not_cached(self, language_server: SolidLanguageServer) -> None: - """ - Test that an empty document symbol response is not written to the cache. - - This guards against the scenario where the Lean language server returns no symbols - before `lake build` has completed. If the empty result were cached, subsequent - requests after the build would still return empty symbols even though the LS now - has the full build artifacts available. - """ - file_path = "Helper.lean" - - # Patch the LSP server to return an empty list, simulating a pre-build response. - with patch.object(language_server.server.send, "document_symbol", return_value=[]): - result = language_server._request_document_symbols(file_path, None) - assert result == [], "Expected the (mocked) empty response to be returned as-is" - - # The empty result must NOT have been written to the cache. - assert file_path not in language_server._raw_document_symbols_cache, ( - "Empty symbol response must not be stored in the cache; " - "it would permanently hide symbols that become available after `lake build`" - ) - @pytest.mark.parametrize("language_server", [Language.LEAN4], indirect=True) def test_go_to_definition_across_files(self, language_server: SolidLanguageServer) -> None: """ From 3be62e3c27deff756e8cc65a1b4befbfd2b193dc Mon Sep 17 00:00:00 2001 From: Michael Panchenko Date: Fri, 17 Apr 2026 11:02:28 +0200 Subject: [PATCH 08/11] Fix in request_overview: explicitly passed file path bypassed ignore check --- CHANGELOG.md | 1 + src/solidlsp/ls.py | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13c481e9..9e03f1f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Status of the `main` branch. Changes prior to the next official version change w * General: - Support `serena --version` CLI command for displaying the current version #1347 - Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350 + - `GetSymbolsOverviewTool`: ignored paths were not respected in LSP variant (fix in `SolidLanguageServer`) - Fix: Duplicate comments in re-saved YAML configuration files #1285 * JetBrains: diff --git a/src/solidlsp/ls.py b/src/solidlsp/ls.py index 5b679ce9..6ae69a39 100644 --- a/src/solidlsp/ls.py +++ b/src/solidlsp/ls.py @@ -1694,6 +1694,7 @@ class SolidLanguageServer(ABC): def request_overview(self, within_relative_path: str) -> dict[str, list[UnifiedSymbolInformation]]: """ An overview of all symbols in the given file or directory. + Raises a ValueError if a path to an ignored file is passed. :param within_relative_path: the relative path to the file or directory to get the overview of. :return: A mapping of all relative paths analyzed to lists of top-level symbols in the corresponding file. @@ -1703,6 +1704,8 @@ class SolidLanguageServer(ABC): raise FileNotFoundError(f"File or directory not found: {abs_path}") if abs_path.is_file(): + if self.is_ignored_path(within_relative_path): + raise ValueError(f"The explicitly passed file {within_relative_path} is ignored, not returning overview.") symbols_overview = self.request_document_overview(within_relative_path) return {within_relative_path: symbols_overview} else: From 49524ab155594742cadc5ce941b86bf5c55255ee Mon Sep 17 00:00:00 2001 From: "Dr. Dominik Jain" Date: Fri, 17 Apr 2026 15:55:36 +0200 Subject: [PATCH 09/11] Update README * Add "Quick Demo" section with new videos * Remove old section "Serena in Action" * Update section "How Serena Works", removing redundant part explaining backends (this is what the next section handles) --- README.md | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 84cb122f..d26bdad8 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,12 @@ more complex codebases. > Do not install Serena via an MCP or plugin marketplace! They contain outdated and suboptimal installation commands. > Instead, follow our [Quick Start](#quick-start) instructions. +## Quick Demo + +https://github.com/user-attachments/assets/8d11646e-b80e-4723-b9d7-32d6101b5f58 + +:tv: Longer video: [Introduction to Serena in 5 Minutes (YouTube)](https://www.youtube.com/watch?v=5QN7gN1KYLA) + ## What Our "End Users" Say While it is humans who download and set up Serena, our end users are essentially AI agents. @@ -72,16 +78,12 @@ Most modern AI chat clients directly support MCP, including +:tv: See also: [Introduction to Serena in 5 Minutes (YouTube)](https://www.youtube.com/watch?v=5QN7gN1KYLA) + To connect the Serena MCP server to your client, you either * provide the client with a launch command that allows it to start the MCP server, or * start the Serena MCP server yourself in HTTP mode and provide the client with the URL. -Serena's tools are powered by two alternative language intelligence backends: - * With the SolidLSP backend (default), language servers for the selected programming languages will be started -automatically for your project. - * With the JetBrains backend, the Serena JetBrains plugin must be installed in your IDE and -the project you want to work on must open and set up. - See the [Quick Start](#quick-start) section below for information on how to get started. ## Programming Language Support & Semantic Analysis Capabilities @@ -97,7 +99,7 @@ There are two alternative technologies powering these capabilities: * **Language servers** implementing the language server protocol (LSP) — the free/open-source alternative which is used by default. -* The **Serena JetBrains Plugin**, which leverages the powerful code analysis and editing +* **The Serena JetBrains Plugin**, which leverages the powerful code analysis and editing capabilities of your JetBrains IDE (paid plugin; free trial available). You can choose either of these backends depending on your preferences and requirements. @@ -203,25 +205,6 @@ To achieve this, Serena offers multiple levels of (composable) configuration: -## Serena in Action - -
-Demonstrations - -#### Demonstration 1: Efficient Operation in Claude Code - -A demonstration of Serena efficiently retrieving and editing code within Claude Code, thereby saving tokens and time. Efficient operations are not only useful for saving costs, but also for generally improving the generated code's quality. This effect may be less pronounced in very small projects, but often becomes of crucial importance in larger ones. - -https://github.com/user-attachments/assets/ab78ebe0-f77d-43cc-879a-cc399efefd87 - -#### Demonstration 2: Serena in Claude Desktop - -A demonstration of Serena implementing a small feature for itself (a better log GUI) with Claude Desktop. -Note how Serena's tools enable Claude to find and edit the right symbols. - -https://github.com/user-attachments/assets/6eaa9aa1-610d-4723-a2d6-bf1e487ba753 -
- ## Quick Start **Prerequisites**. Serena is managed by *uv*, and [installing uv](https://docs.astral.sh/uv/getting-started/installation/) is the only required prerequisite. From ed630680454c9b7c7825e4d93b4ea6750d840e61 Mon Sep 17 00:00:00 2001 From: KoushikReddy Date: Sat, 18 Apr 2026 09:27:06 -0700 Subject: [PATCH 10/11] fix: add missing children field to DocumentSymbol TypedDict The LSP v3.17 spec defines DocumentSymbol.children as an optional list of nested DocumentSymbol entries. The field was absent from the TypedDict, causing type checkers to flag access to 'children' on DocumentSymbol objects and requiring # type: ignore workarounds in ls.py. --- src/solidlsp/lsp_protocol_handler/lsp_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/solidlsp/lsp_protocol_handler/lsp_types.py b/src/solidlsp/lsp_protocol_handler/lsp_types.py index ffb6135e..c96f5fce 100644 --- a/src/solidlsp/lsp_protocol_handler/lsp_types.py +++ b/src/solidlsp/lsp_protocol_handler/lsp_types.py @@ -2456,8 +2456,8 @@ class DocumentSymbol(TypedDict): selectionRange: "Range" """ The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. Must be contained by the `range`. """ - - # TODO: I think this type is missing the 'children' field - DJ + children: NotRequired[list["DocumentSymbol"]] + """ Children of this symbol, e.g. properties of a class. """ class DocumentSymbolRegistrationOptions(TypedDict): From 7636a1cf3720b6e3782776a8db240fd87c3627e9 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Fri, 17 Apr 2026 16:27:42 +0200 Subject: [PATCH 11/11] Improve JetBrains plugin usage description --- docs/02-usage/025_jetbrains_plugin.md | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docs/02-usage/025_jetbrains_plugin.md b/docs/02-usage/025_jetbrains_plugin.md index 5a0a16f2..1995ca34 100644 --- a/docs/02-usage/025_jetbrains_plugin.md +++ b/docs/02-usage/025_jetbrains_plugin.md @@ -2,7 +2,7 @@ The [JetBrains Plugin](https://plugins.jetbrains.com/plugin/28946-serena/) allows the Serena MCP server to leverage the powerful code analysis and editing capabilities of your JetBrains IDE. -This page explains how to install the plugin and how to configure Serena for using it. +This page explains how to install the plugin and how to configure Serena appropriately. You will still need to set up the Serena MCP server itself, so make sure to follow the [installation instructions](020_running.md) and connect the MCP server to your LLM-based client as described in [client setup](030_clients.md) in addition to following the instructions below. @@ -18,20 +18,20 @@ LLM-based client as described in [client setup](030_clients.md) in addition to f We recommend the JetBrains plugin as the preferred way of using Serena, especially for users of JetBrains IDEs. -```{admonition} *Note:* The plugin is a language intelligence backend for the Serena MCP server. -:class: note -It is *not* a UI extension for direct agent interaction (like Copilot) or anything of the sort. -You still interact with your regular client – be it external to your IDE (like Claude Code) or internal (like JetBrains AI Assistant) – -and connect it to the Serena MCP server. -The plugin simply enables the Serena MCP server to directly leverage capabilities of your JetBrains IDE! -``` - **How it works:** 1. Install the plugin in your JetBrains IDE 2. Configure Serena to use the JetBrains language backend (see [below](configure-jetbrains)) 3. Open the project you want to work on in your JetBrains IDE and activate it in Serena (see [below](jetbrains-workflow)) 4. Start coding via your MCP client as usual +```{admonition} *Note:* The plugin is a language intelligence backend for the Serena MCP server. +:class: note +It is *not* a UI extension for direct agent interaction (like Copilot) or anything of the sort. +You still interact with your regular client – be it external to your IDE (like Claude Code CLI) or internal (like Copilot or JetBrains AI Assistant) – +and connect it to the Serena MCP server. +The plugin simply enables the Serena MCP server to directly leverage capabilities of your JetBrains IDE! +``` + **Purchasing the JetBrains Plugin supports the Serena project.** The proceeds from plugin sales allow us to dedicate more resources to further developing and improving Serena. @@ -82,7 +82,7 @@ language_backend: JetBrains Note that the file might not exist yet if you never executed Serena before. -**Per-Instance Configuration**. +**Per-Server Instance Configuration**. The configuration setting in the global config file can be overridden on a per-instance basis by providing the arguments `--language-backend JetBrains` when launching the Serena MCP server. @@ -119,9 +119,10 @@ You will also notice that your client will use the JetBrains-specific tools like Having installed the plugin in your IDE and having configured Serena to use the JetBrains backend, the general workflow is simple: -1. Open the project you want to work on in your JetBrains IDE -2. Activate the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)) -3. Start using Serena tools as usual +1. Open the project you want to work on in your JetBrains IDE. + Note that the project must be appropriately set up in your IDE, i.e. symbol lookups for all relevant programming languages and frameworks should work in the IDE. +2. Activate the project's root folder as a project in Serena (see [Project Creation](project-creation-indexing) and [Project Activation](project-activation)). +3. Start using Serena's tools as usual. Note that the project folder that is open in your IDE and the Serena project root folder must match.