#
tokens: 47034/50000 14/294 files (page 5/14)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 5 of 14. Use http://codebase.md/oraios/serena?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .devcontainer
│   └── devcontainer.json
├── .dockerignore
├── .env.example
├── .github
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── config.yml
│   │   ├── feature_request.md
│   │   └── issue--bug--performance-problem--question-.md
│   └── workflows
│       ├── codespell.yml
│       ├── docker.yml
│       ├── junie.yml
│       ├── lint_and_docs.yaml
│       ├── publish.yml
│       └── pytest.yml
├── .gitignore
├── .serena
│   ├── memories
│   │   ├── adding_new_language_support_guide.md
│   │   ├── serena_core_concepts_and_architecture.md
│   │   ├── serena_repository_structure.md
│   │   └── suggested_commands.md
│   └── project.yml
├── .vscode
│   └── settings.json
├── CHANGELOG.md
├── CLAUDE.md
├── compose.yaml
├── CONTRIBUTING.md
├── docker_build_and_run.sh
├── DOCKER.md
├── Dockerfile
├── docs
│   ├── custom_agent.md
│   └── serena_on_chatgpt.md
├── flake.lock
├── flake.nix
├── lessons_learned.md
├── LICENSE
├── llms-install.md
├── public
│   └── .gitignore
├── pyproject.toml
├── README.md
├── resources
│   ├── serena-icons.cdr
│   ├── serena-logo-dark-mode.svg
│   ├── serena-logo.cdr
│   ├── serena-logo.svg
│   └── vscode_sponsor_logo.png
├── roadmap.md
├── scripts
│   ├── agno_agent.py
│   ├── demo_run_tools.py
│   ├── gen_prompt_factory.py
│   ├── mcp_server.py
│   ├── print_mode_context_options.py
│   └── print_tool_overview.py
├── src
│   ├── interprompt
│   │   ├── __init__.py
│   │   ├── .syncCommitId.remote
│   │   ├── .syncCommitId.this
│   │   ├── jinja_template.py
│   │   ├── multilang_prompt.py
│   │   ├── prompt_factory.py
│   │   └── util
│   │       ├── __init__.py
│   │       └── class_decorators.py
│   ├── README.md
│   ├── serena
│   │   ├── __init__.py
│   │   ├── agent.py
│   │   ├── agno.py
│   │   ├── analytics.py
│   │   ├── cli.py
│   │   ├── code_editor.py
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   ├── context_mode.py
│   │   │   └── serena_config.py
│   │   ├── constants.py
│   │   ├── dashboard.py
│   │   ├── generated
│   │   │   └── generated_prompt_factory.py
│   │   ├── gui_log_viewer.py
│   │   ├── mcp.py
│   │   ├── project.py
│   │   ├── prompt_factory.py
│   │   ├── resources
│   │   │   ├── config
│   │   │   │   ├── contexts
│   │   │   │   │   ├── agent.yml
│   │   │   │   │   ├── chatgpt.yml
│   │   │   │   │   ├── codex.yml
│   │   │   │   │   ├── context.template.yml
│   │   │   │   │   ├── desktop-app.yml
│   │   │   │   │   ├── ide-assistant.yml
│   │   │   │   │   └── oaicompat-agent.yml
│   │   │   │   ├── internal_modes
│   │   │   │   │   └── jetbrains.yml
│   │   │   │   ├── modes
│   │   │   │   │   ├── editing.yml
│   │   │   │   │   ├── interactive.yml
│   │   │   │   │   ├── mode.template.yml
│   │   │   │   │   ├── no-onboarding.yml
│   │   │   │   │   ├── onboarding.yml
│   │   │   │   │   ├── one-shot.yml
│   │   │   │   │   └── planning.yml
│   │   │   │   └── prompt_templates
│   │   │   │       ├── simple_tool_outputs.yml
│   │   │   │       └── system_prompt.yml
│   │   │   ├── dashboard
│   │   │   │   ├── dashboard.js
│   │   │   │   ├── index.html
│   │   │   │   ├── jquery.min.js
│   │   │   │   ├── serena-icon-16.png
│   │   │   │   ├── serena-icon-32.png
│   │   │   │   ├── serena-icon-48.png
│   │   │   │   ├── serena-logs-dark-mode.png
│   │   │   │   └── serena-logs.png
│   │   │   ├── project.template.yml
│   │   │   └── serena_config.template.yml
│   │   ├── symbol.py
│   │   ├── text_utils.py
│   │   ├── tools
│   │   │   ├── __init__.py
│   │   │   ├── cmd_tools.py
│   │   │   ├── config_tools.py
│   │   │   ├── file_tools.py
│   │   │   ├── jetbrains_plugin_client.py
│   │   │   ├── jetbrains_tools.py
│   │   │   ├── memory_tools.py
│   │   │   ├── symbol_tools.py
│   │   │   ├── tools_base.py
│   │   │   └── workflow_tools.py
│   │   └── util
│   │       ├── class_decorators.py
│   │       ├── exception.py
│   │       ├── file_system.py
│   │       ├── general.py
│   │       ├── git.py
│   │       ├── inspection.py
│   │       ├── logging.py
│   │       ├── shell.py
│   │       └── thread.py
│   └── solidlsp
│       ├── __init__.py
│       ├── .gitignore
│       ├── language_servers
│       │   ├── al_language_server.py
│       │   ├── bash_language_server.py
│       │   ├── clangd_language_server.py
│       │   ├── clojure_lsp.py
│       │   ├── common.py
│       │   ├── csharp_language_server.py
│       │   ├── dart_language_server.py
│       │   ├── eclipse_jdtls.py
│       │   ├── elixir_tools
│       │   │   ├── __init__.py
│       │   │   ├── elixir_tools.py
│       │   │   └── README.md
│       │   ├── elm_language_server.py
│       │   ├── erlang_language_server.py
│       │   ├── gopls.py
│       │   ├── intelephense.py
│       │   ├── jedi_server.py
│       │   ├── kotlin_language_server.py
│       │   ├── lua_ls.py
│       │   ├── marksman.py
│       │   ├── nixd_ls.py
│       │   ├── omnisharp
│       │   │   ├── initialize_params.json
│       │   │   ├── runtime_dependencies.json
│       │   │   └── workspace_did_change_configuration.json
│       │   ├── omnisharp.py
│       │   ├── perl_language_server.py
│       │   ├── pyright_server.py
│       │   ├── r_language_server.py
│       │   ├── ruby_lsp.py
│       │   ├── rust_analyzer.py
│       │   ├── solargraph.py
│       │   ├── sourcekit_lsp.py
│       │   ├── terraform_ls.py
│       │   ├── typescript_language_server.py
│       │   ├── vts_language_server.py
│       │   └── zls.py
│       ├── ls_config.py
│       ├── ls_exceptions.py
│       ├── ls_handler.py
│       ├── ls_logger.py
│       ├── ls_request.py
│       ├── ls_types.py
│       ├── ls_utils.py
│       ├── ls.py
│       ├── lsp_protocol_handler
│       │   ├── lsp_constants.py
│       │   ├── lsp_requests.py
│       │   ├── lsp_types.py
│       │   └── server.py
│       ├── settings.py
│       └── util
│           ├── subprocess_util.py
│           └── zip.py
├── test
│   ├── __init__.py
│   ├── conftest.py
│   ├── resources
│   │   └── repos
│   │       ├── al
│   │       │   └── test_repo
│   │       │       ├── app.json
│   │       │       └── src
│   │       │           ├── Codeunits
│   │       │           │   ├── CustomerMgt.Codeunit.al
│   │       │           │   └── PaymentProcessorImpl.Codeunit.al
│   │       │           ├── Enums
│   │       │           │   └── CustomerType.Enum.al
│   │       │           ├── Interfaces
│   │       │           │   └── IPaymentProcessor.Interface.al
│   │       │           ├── Pages
│   │       │           │   ├── CustomerCard.Page.al
│   │       │           │   └── CustomerList.Page.al
│   │       │           ├── TableExtensions
│   │       │           │   └── Item.TableExt.al
│   │       │           └── Tables
│   │       │               └── Customer.Table.al
│   │       ├── bash
│   │       │   └── test_repo
│   │       │       ├── config.sh
│   │       │       ├── main.sh
│   │       │       └── utils.sh
│   │       ├── clojure
│   │       │   └── test_repo
│   │       │       ├── deps.edn
│   │       │       └── src
│   │       │           └── test_app
│   │       │               ├── core.clj
│   │       │               └── utils.clj
│   │       ├── csharp
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── Models
│   │       │       │   └── Person.cs
│   │       │       ├── Program.cs
│   │       │       ├── serena.sln
│   │       │       └── TestProject.csproj
│   │       ├── dart
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── helper.dart
│   │       │       │   ├── main.dart
│   │       │       │   └── models.dart
│   │       │       └── pubspec.yaml
│   │       ├── elixir
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── lib
│   │       │       │   ├── examples.ex
│   │       │       │   ├── ignored_dir
│   │       │       │   │   └── ignored_module.ex
│   │       │       │   ├── models.ex
│   │       │       │   ├── services.ex
│   │       │       │   ├── test_repo.ex
│   │       │       │   └── utils.ex
│   │       │       ├── mix.exs
│   │       │       ├── mix.lock
│   │       │       ├── scripts
│   │       │       │   └── build_script.ex
│   │       │       └── test
│   │       │           ├── models_test.exs
│   │       │           └── test_repo_test.exs
│   │       ├── elm
│   │       │   └── test_repo
│   │       │       ├── elm.json
│   │       │       ├── Main.elm
│   │       │       └── Utils.elm
│   │       ├── erlang
│   │       │   └── test_repo
│   │       │       ├── hello.erl
│   │       │       ├── ignored_dir
│   │       │       │   └── ignored_module.erl
│   │       │       ├── include
│   │       │       │   ├── records.hrl
│   │       │       │   └── types.hrl
│   │       │       ├── math_utils.erl
│   │       │       ├── rebar.config
│   │       │       ├── src
│   │       │       │   ├── app.erl
│   │       │       │   ├── models.erl
│   │       │       │   ├── services.erl
│   │       │       │   └── utils.erl
│   │       │       └── test
│   │       │           ├── models_tests.erl
│   │       │           └── utils_tests.erl
│   │       ├── go
│   │       │   └── test_repo
│   │       │       └── main.go
│   │       ├── java
│   │       │   └── test_repo
│   │       │       ├── pom.xml
│   │       │       └── src
│   │       │           └── main
│   │       │               └── java
│   │       │                   └── test_repo
│   │       │                       ├── Main.java
│   │       │                       ├── Model.java
│   │       │                       ├── ModelUser.java
│   │       │                       └── Utils.java
│   │       ├── kotlin
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── build.gradle.kts
│   │       │       └── src
│   │       │           └── main
│   │       │               └── kotlin
│   │       │                   └── test_repo
│   │       │                       ├── Main.kt
│   │       │                       ├── Model.kt
│   │       │                       ├── ModelUser.kt
│   │       │                       └── Utils.kt
│   │       ├── lua
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── main.lua
│   │       │       ├── src
│   │       │       │   ├── calculator.lua
│   │       │       │   └── utils.lua
│   │       │       └── tests
│   │       │           └── test_calculator.lua
│   │       ├── markdown
│   │       │   └── test_repo
│   │       │       ├── api.md
│   │       │       ├── CONTRIBUTING.md
│   │       │       ├── guide.md
│   │       │       └── README.md
│   │       ├── nix
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── default.nix
│   │       │       ├── flake.nix
│   │       │       ├── lib
│   │       │       │   └── utils.nix
│   │       │       ├── modules
│   │       │       │   └── example.nix
│   │       │       └── scripts
│   │       │           └── hello.sh
│   │       ├── perl
│   │       │   └── test_repo
│   │       │       ├── helper.pl
│   │       │       └── main.pl
│   │       ├── php
│   │       │   └── test_repo
│   │       │       ├── helper.php
│   │       │       ├── index.php
│   │       │       └── simple_var.php
│   │       ├── python
│   │       │   └── test_repo
│   │       │       ├── .gitignore
│   │       │       ├── custom_test
│   │       │       │   ├── __init__.py
│   │       │       │   └── advanced_features.py
│   │       │       ├── examples
│   │       │       │   ├── __init__.py
│   │       │       │   └── user_management.py
│   │       │       ├── ignore_this_dir_with_postfix
│   │       │       │   └── ignored_module.py
│   │       │       ├── scripts
│   │       │       │   ├── __init__.py
│   │       │       │   └── run_app.py
│   │       │       └── test_repo
│   │       │           ├── __init__.py
│   │       │           ├── complex_types.py
│   │       │           ├── models.py
│   │       │           ├── name_collisions.py
│   │       │           ├── nested_base.py
│   │       │           ├── nested.py
│   │       │           ├── overloaded.py
│   │       │           ├── services.py
│   │       │           ├── utils.py
│   │       │           └── variables.py
│   │       ├── r
│   │       │   └── test_repo
│   │       │       ├── .Rbuildignore
│   │       │       ├── DESCRIPTION
│   │       │       ├── examples
│   │       │       │   └── analysis.R
│   │       │       ├── NAMESPACE
│   │       │       └── R
│   │       │           ├── models.R
│   │       │           └── utils.R
│   │       ├── ruby
│   │       │   └── test_repo
│   │       │       ├── .solargraph.yml
│   │       │       ├── examples
│   │       │       │   └── user_management.rb
│   │       │       ├── lib.rb
│   │       │       ├── main.rb
│   │       │       ├── models.rb
│   │       │       ├── nested.rb
│   │       │       ├── services.rb
│   │       │       └── variables.rb
│   │       ├── rust
│   │       │   ├── test_repo
│   │       │   │   ├── Cargo.lock
│   │       │   │   ├── Cargo.toml
│   │       │   │   └── src
│   │       │   │       ├── lib.rs
│   │       │   │       └── main.rs
│   │       │   └── test_repo_2024
│   │       │       ├── Cargo.lock
│   │       │       ├── Cargo.toml
│   │       │       └── src
│   │       │           ├── lib.rs
│   │       │           └── main.rs
│   │       ├── swift
│   │       │   └── test_repo
│   │       │       ├── Package.swift
│   │       │       └── src
│   │       │           ├── main.swift
│   │       │           └── utils.swift
│   │       ├── terraform
│   │       │   └── test_repo
│   │       │       ├── data.tf
│   │       │       ├── main.tf
│   │       │       ├── outputs.tf
│   │       │       └── variables.tf
│   │       ├── typescript
│   │       │   └── test_repo
│   │       │       ├── .serena
│   │       │       │   └── project.yml
│   │       │       ├── index.ts
│   │       │       ├── tsconfig.json
│   │       │       └── use_helper.ts
│   │       └── zig
│   │           └── test_repo
│   │               ├── .gitignore
│   │               ├── build.zig
│   │               ├── src
│   │               │   ├── calculator.zig
│   │               │   ├── main.zig
│   │               │   └── math_utils.zig
│   │               └── zls.json
│   ├── serena
│   │   ├── __init__.py
│   │   ├── __snapshots__
│   │   │   └── test_symbol_editing.ambr
│   │   ├── config
│   │   │   ├── __init__.py
│   │   │   └── test_serena_config.py
│   │   ├── test_edit_marker.py
│   │   ├── test_mcp.py
│   │   ├── test_serena_agent.py
│   │   ├── test_symbol_editing.py
│   │   ├── test_symbol.py
│   │   ├── test_text_utils.py
│   │   ├── test_tool_parameter_types.py
│   │   └── util
│   │       ├── test_exception.py
│   │       └── test_file_system.py
│   └── solidlsp
│       ├── al
│       │   └── test_al_basic.py
│       ├── bash
│       │   ├── __init__.py
│       │   └── test_bash_basic.py
│       ├── clojure
│       │   ├── __init__.py
│       │   └── test_clojure_basic.py
│       ├── csharp
│       │   └── test_csharp_basic.py
│       ├── dart
│       │   ├── __init__.py
│       │   └── test_dart_basic.py
│       ├── elixir
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_elixir_basic.py
│       │   ├── test_elixir_ignored_dirs.py
│       │   ├── test_elixir_integration.py
│       │   └── test_elixir_symbol_retrieval.py
│       ├── elm
│       │   └── test_elm_basic.py
│       ├── erlang
│       │   ├── __init__.py
│       │   ├── conftest.py
│       │   ├── test_erlang_basic.py
│       │   ├── test_erlang_ignored_dirs.py
│       │   └── test_erlang_symbol_retrieval.py
│       ├── go
│       │   └── test_go_basic.py
│       ├── java
│       │   └── test_java_basic.py
│       ├── kotlin
│       │   └── test_kotlin_basic.py
│       ├── lua
│       │   └── test_lua_basic.py
│       ├── markdown
│       │   ├── __init__.py
│       │   └── test_markdown_basic.py
│       ├── nix
│       │   └── test_nix_basic.py
│       ├── perl
│       │   └── test_perl_basic.py
│       ├── php
│       │   └── test_php_basic.py
│       ├── python
│       │   ├── test_python_basic.py
│       │   ├── test_retrieval_with_ignored_dirs.py
│       │   └── test_symbol_retrieval.py
│       ├── r
│       │   ├── __init__.py
│       │   └── test_r_basic.py
│       ├── ruby
│       │   ├── test_ruby_basic.py
│       │   └── test_ruby_symbol_retrieval.py
│       ├── rust
│       │   ├── test_rust_2024_edition.py
│       │   └── test_rust_basic.py
│       ├── swift
│       │   └── test_swift_basic.py
│       ├── terraform
│       │   └── test_terraform_basic.py
│       ├── typescript
│       │   └── test_typescript_basic.py
│       ├── util
│       │   └── test_zip.py
│       └── zig
│           └── test_zig_basic.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/clojure_lsp.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Provides Clojure specific instantiation of the LanguageServer class. Contains various configurations and settings specific to Clojure.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import shutil
  9 | import subprocess
 10 | import threading
 11 | 
 12 | from solidlsp.ls import SolidLanguageServer
 13 | from solidlsp.ls_config import LanguageServerConfig
 14 | from solidlsp.ls_logger import LanguageServerLogger
 15 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 16 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 17 | from solidlsp.settings import SolidLSPSettings
 18 | 
 19 | from .common import RuntimeDependency, RuntimeDependencyCollection
 20 | 
 21 | 
 22 | def run_command(cmd: list, capture_output: bool = True) -> subprocess.CompletedProcess:
 23 |     return subprocess.run(
 24 |         cmd, stdout=subprocess.PIPE if capture_output else None, stderr=subprocess.STDOUT if capture_output else None, text=True, check=True
 25 |     )
 26 | 
 27 | 
 28 | def verify_clojure_cli():
 29 |     install_msg = "Please install the official Clojure CLI from:\n  https://clojure.org/guides/getting_started"
 30 |     if shutil.which("clojure") is None:
 31 |         raise FileNotFoundError("`clojure` not found.\n" + install_msg)
 32 | 
 33 |     help_proc = run_command(["clojure", "--help"])
 34 |     if "-Aaliases" not in help_proc.stdout:
 35 |         raise RuntimeError("Detected a Clojure executable, but it does not support '-Aaliases'.\n" + install_msg)
 36 | 
 37 |     spath_proc = run_command(["clojure", "-Spath"], capture_output=False)
 38 |     if spath_proc.returncode != 0:
 39 |         raise RuntimeError("`clojure -Spath` failed; please upgrade to Clojure CLI ≥ 1.10.")
 40 | 
 41 | 
 42 | class ClojureLSP(SolidLanguageServer):
 43 |     """
 44 |     Provides a clojure-lsp specific instantiation of the LanguageServer class. Contains various configurations and settings specific to clojure.
 45 |     """
 46 | 
 47 |     clojure_lsp_releases = "https://github.com/clojure-lsp/clojure-lsp/releases/latest/download"
 48 |     runtime_dependencies = RuntimeDependencyCollection(
 49 |         [
 50 |             RuntimeDependency(
 51 |                 id="clojure-lsp",
 52 |                 url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-aarch64.zip",
 53 |                 platform_id="osx-arm64",
 54 |                 archive_type="zip",
 55 |                 binary_name="clojure-lsp",
 56 |             ),
 57 |             RuntimeDependency(
 58 |                 id="clojure-lsp",
 59 |                 url=f"{clojure_lsp_releases}/clojure-lsp-native-macos-amd64.zip",
 60 |                 platform_id="osx-x64",
 61 |                 archive_type="zip",
 62 |                 binary_name="clojure-lsp",
 63 |             ),
 64 |             RuntimeDependency(
 65 |                 id="clojure-lsp",
 66 |                 url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-aarch64.zip",
 67 |                 platform_id="linux-arm64",
 68 |                 archive_type="zip",
 69 |                 binary_name="clojure-lsp",
 70 |             ),
 71 |             RuntimeDependency(
 72 |                 id="clojure-lsp",
 73 |                 url=f"{clojure_lsp_releases}/clojure-lsp-native-linux-amd64.zip",
 74 |                 platform_id="linux-x64",
 75 |                 archive_type="zip",
 76 |                 binary_name="clojure-lsp",
 77 |             ),
 78 |             RuntimeDependency(
 79 |                 id="clojure-lsp",
 80 |                 url=f"{clojure_lsp_releases}/clojure-lsp-native-windows-amd64.zip",
 81 |                 platform_id="win-x64",
 82 |                 archive_type="zip",
 83 |                 binary_name="clojure-lsp.exe",
 84 |             ),
 85 |         ]
 86 |     )
 87 | 
 88 |     def __init__(
 89 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 90 |     ):
 91 |         """
 92 |         Creates a ClojureLSP instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 93 |         """
 94 |         clojure_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 95 |         super().__init__(
 96 |             config,
 97 |             logger,
 98 |             repository_root_path,
 99 |             ProcessLaunchInfo(cmd=clojure_lsp_executable_path, cwd=repository_root_path),
100 |             "clojure",
101 |             solidlsp_settings,
102 |         )
103 |         self.server_ready = threading.Event()
104 |         self.initialize_searcher_command_available = threading.Event()
105 |         self.resolve_main_method_available = threading.Event()
106 |         self.service_ready_event = threading.Event()
107 | 
108 |     @classmethod
109 |     def _setup_runtime_dependencies(
110 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
111 |     ) -> str:
112 |         """Setup runtime dependencies for clojure-lsp and return the command to start the server."""
113 |         verify_clojure_cli()
114 |         deps = ClojureLSP.runtime_dependencies
115 |         dependency = deps.get_single_dep_for_current_platform()
116 | 
117 |         clojurelsp_ls_dir = cls.ls_resources_dir(solidlsp_settings)
118 |         clojurelsp_executable_path = deps.binary_path(clojurelsp_ls_dir)
119 |         if not os.path.exists(clojurelsp_executable_path):
120 |             logger.log(
121 |                 f"Downloading and extracting clojure-lsp from {dependency.url} to {clojurelsp_ls_dir}",
122 |                 logging.INFO,
123 |             )
124 |             deps.install(logger, clojurelsp_ls_dir)
125 |         if not os.path.exists(clojurelsp_executable_path):
126 |             raise FileNotFoundError(f"Download failed? Could not find clojure-lsp executable at {clojurelsp_executable_path}")
127 |         os.chmod(clojurelsp_executable_path, 0o755)
128 |         return clojurelsp_executable_path
129 | 
130 |     @staticmethod
131 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
132 |         """Returns the init params for clojure-lsp."""
133 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
134 |         return {  # type: ignore
135 |             "processId": os.getpid(),
136 |             "rootPath": repository_absolute_path,
137 |             "rootUri": root_uri,
138 |             "capabilities": {
139 |                 "workspace": {
140 |                     "applyEdit": True,
141 |                     "workspaceEdit": {"documentChanges": True},
142 |                     "symbol": {"symbolKind": {"valueSet": list(range(1, 27))}},
143 |                     "workspaceFolders": True,
144 |                 },
145 |                 "textDocument": {
146 |                     "synchronization": {"didSave": True},
147 |                     "publishDiagnostics": {"relatedInformation": True, "tagSupport": {"valueSet": [1, 2]}},
148 |                     "definition": {"linkSupport": True},
149 |                     "references": {},
150 |                     "hover": {"contentFormat": ["markdown", "plaintext"]},
151 |                     "documentSymbol": {
152 |                         "hierarchicalDocumentSymbolSupport": True,
153 |                         "symbolKind": {"valueSet": list(range(1, 27))},  #
154 |                     },
155 |                 },
156 |                 "general": {"positionEncodings": ["utf-16"]},
157 |             },
158 |             "initializationOptions": {"dependency-scheme": "jar", "text-document-sync-kind": "incremental"},
159 |             "trace": "off",
160 |             "workspaceFolders": [{"uri": root_uri, "name": os.path.basename(repository_absolute_path)}],
161 |         }
162 | 
163 |     def _start_server(self):
164 |         def register_capability_handler(params):
165 |             assert "registrations" in params
166 |             for registration in params["registrations"]:
167 |                 if registration["method"] == "workspace/executeCommand":
168 |                     self.initialize_searcher_command_available.set()
169 |                     self.resolve_main_method_available.set()
170 |             return
171 | 
172 |         def lang_status_handler(params):
173 |             # TODO: Should we wait for
174 |             # server -> client: {'jsonrpc': '2.0', 'method': 'language/status', 'params': {'type': 'ProjectStatus', 'message': 'OK'}}
175 |             # Before proceeding?
176 |             if params["type"] == "ServiceReady" and params["message"] == "ServiceReady":
177 |                 self.service_ready_event.set()
178 | 
179 |         def execute_client_command_handler(params):
180 |             return []
181 | 
182 |         def do_nothing(params):
183 |             return
184 | 
185 |         def check_experimental_status(params):
186 |             if params["quiescent"] == True:
187 |                 self.server_ready.set()
188 | 
189 |         def window_log_message(msg):
190 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
191 | 
192 |         self.server.on_request("client/registerCapability", register_capability_handler)
193 |         self.server.on_notification("language/status", lang_status_handler)
194 |         self.server.on_notification("window/logMessage", window_log_message)
195 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
196 |         self.server.on_notification("$/progress", do_nothing)
197 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
198 |         self.server.on_notification("language/actionableNotification", do_nothing)
199 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
200 | 
201 |         self.logger.log("Starting clojure-lsp server process", logging.INFO)
202 |         self.server.start()
203 | 
204 |         initialize_params = self._get_initialize_params(self.repository_root_path)
205 | 
206 |         self.logger.log(
207 |             "Sending initialize request from LSP client to LSP server and awaiting response",
208 |             logging.INFO,
209 |         )
210 |         init_response = self.server.send.initialize(initialize_params)
211 |         assert init_response["capabilities"]["textDocumentSync"]["change"] == 2
212 |         assert "completionProvider" in init_response["capabilities"]
213 |         # Clojure-lsp completion provider capabilities are more flexible than other servers'
214 |         completion_provider = init_response["capabilities"]["completionProvider"]
215 |         assert completion_provider["resolveProvider"] == True
216 |         assert "triggerCharacters" in completion_provider
217 |         self.server.notify.initialized({})
218 |         # after initialize, Clojure-lsp is ready to serve
219 |         self.server_ready.set()
220 |         self.completions_available.set()
221 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/vts_language_server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Language Server implementation for TypeScript/JavaScript using https://github.com/yioneko/vtsls,
  3 | which provides TypeScript language server functionality via VSCode's TypeScript extension
  4 | (contrary to typescript-language-server, which uses the TypeScript compiler directly).
  5 | """
  6 | 
  7 | import logging
  8 | import os
  9 | import pathlib
 10 | import shutil
 11 | import threading
 12 | 
 13 | from overrides import override
 14 | 
 15 | from solidlsp.ls import SolidLanguageServer
 16 | from solidlsp.ls_config import LanguageServerConfig
 17 | from solidlsp.ls_logger import LanguageServerLogger
 18 | from solidlsp.ls_utils import PlatformId, PlatformUtils
 19 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 20 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 21 | from solidlsp.settings import SolidLSPSettings
 22 | 
 23 | from .common import RuntimeDependency, RuntimeDependencyCollection
 24 | 
 25 | 
 26 | class VtsLanguageServer(SolidLanguageServer):
 27 |     """
 28 |     Provides TypeScript specific instantiation of the LanguageServer class using vtsls.
 29 |     Contains various configurations and settings specific to TypeScript via vtsls wrapper.
 30 |     """
 31 | 
 32 |     def __init__(
 33 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 34 |     ):
 35 |         """
 36 |         Creates a VtsLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 37 |         """
 38 |         vts_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 39 |         super().__init__(
 40 |             config,
 41 |             logger,
 42 |             repository_root_path,
 43 |             ProcessLaunchInfo(cmd=vts_lsp_executable_path, cwd=repository_root_path),
 44 |             "typescript",
 45 |             solidlsp_settings,
 46 |         )
 47 |         self.server_ready = threading.Event()
 48 |         self.initialize_searcher_command_available = threading.Event()
 49 | 
 50 |     @override
 51 |     def is_ignored_dirname(self, dirname: str) -> bool:
 52 |         return super().is_ignored_dirname(dirname) or dirname in [
 53 |             "node_modules",
 54 |             "dist",
 55 |             "build",
 56 |             "coverage",
 57 |         ]
 58 | 
 59 |     @classmethod
 60 |     def _setup_runtime_dependencies(
 61 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
 62 |     ) -> str:
 63 |         """
 64 |         Setup runtime dependencies for VTS Language Server and return the command to start the server.
 65 |         """
 66 |         platform_id = PlatformUtils.get_platform_id()
 67 | 
 68 |         valid_platforms = [
 69 |             PlatformId.LINUX_x64,
 70 |             PlatformId.LINUX_arm64,
 71 |             PlatformId.OSX,
 72 |             PlatformId.OSX_x64,
 73 |             PlatformId.OSX_arm64,
 74 |             PlatformId.WIN_x64,
 75 |             PlatformId.WIN_arm64,
 76 |         ]
 77 |         assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for vtsls at the moment"
 78 | 
 79 |         deps = RuntimeDependencyCollection(
 80 |             [
 81 |                 RuntimeDependency(
 82 |                     id="vtsls",
 83 |                     description="vtsls language server package",
 84 |                     command="npm install --prefix ./ @vtsls/[email protected]",
 85 |                     platform_id="any",
 86 |                 ),
 87 |             ]
 88 |         )
 89 |         vts_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "vts-lsp")
 90 |         vts_executable_path = os.path.join(vts_ls_dir, "vtsls")
 91 | 
 92 |         # Verify both node and npm are installed
 93 |         is_node_installed = shutil.which("node") is not None
 94 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
 95 |         is_npm_installed = shutil.which("npm") is not None
 96 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
 97 | 
 98 |         # Install vtsls if not already installed
 99 |         if not os.path.exists(vts_ls_dir):
100 |             os.makedirs(vts_ls_dir, exist_ok=True)
101 |             deps.install(logger, vts_ls_dir)
102 | 
103 |         vts_executable_path = os.path.join(vts_ls_dir, "node_modules", ".bin", "vtsls")
104 | 
105 |         assert os.path.exists(vts_executable_path), "vtsls executable not found. Please install @vtsls/language-server and try again."
106 |         return f"{vts_executable_path} --stdio"
107 | 
108 |     @staticmethod
109 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
110 |         """
111 |         Returns the initialize params for the VTS Language Server.
112 |         """
113 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
114 |         initialize_params = {
115 |             "locale": "en",
116 |             "capabilities": {
117 |                 "textDocument": {
118 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
119 |                     "definition": {"dynamicRegistration": True},
120 |                     "references": {"dynamicRegistration": True},
121 |                     "documentSymbol": {
122 |                         "dynamicRegistration": True,
123 |                         "hierarchicalDocumentSymbolSupport": True,
124 |                         "symbolKind": {"valueSet": list(range(1, 27))},
125 |                     },
126 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
127 |                     "signatureHelp": {"dynamicRegistration": True},
128 |                     "codeAction": {"dynamicRegistration": True},
129 |                 },
130 |                 "workspace": {
131 |                     "workspaceFolders": True,
132 |                     "didChangeConfiguration": {"dynamicRegistration": True},
133 |                     "symbol": {"dynamicRegistration": True},
134 |                     "configuration": True,  # This might be needed for vtsls
135 |                 },
136 |             },
137 |             "processId": os.getpid(),
138 |             "rootPath": repository_absolute_path,
139 |             "rootUri": root_uri,
140 |             "workspaceFolders": [
141 |                 {
142 |                     "uri": root_uri,
143 |                     "name": os.path.basename(repository_absolute_path),
144 |                 }
145 |             ],
146 |         }
147 |         return initialize_params
148 | 
149 |     def _start_server(self):
150 |         """
151 |         Starts the VTS Language Server, waits for the server to be ready and yields the LanguageServer instance.
152 | 
153 |         Usage:
154 |         ```
155 |         async with lsp.start_server():
156 |             # LanguageServer has been initialized and ready to serve requests
157 |             await lsp.request_definition(...)
158 |             await lsp.request_references(...)
159 |             # Shutdown the LanguageServer on exit from scope
160 |         # LanguageServer has been shutdown
161 |         """
162 | 
163 |         def register_capability_handler(params):
164 |             assert "registrations" in params
165 |             for registration in params["registrations"]:
166 |                 if registration["method"] == "workspace/executeCommand":
167 |                     self.initialize_searcher_command_available.set()
168 |             return
169 | 
170 |         def execute_client_command_handler(params):
171 |             return []
172 | 
173 |         def workspace_configuration_handler(params):
174 |             # VTS may request workspace configuration
175 |             # Return empty configuration for each requested item
176 |             if "items" in params:
177 |                 return [{}] * len(params["items"])
178 |             return {}
179 | 
180 |         def do_nothing(params):
181 |             return
182 | 
183 |         def window_log_message(msg):
184 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
185 | 
186 |         def check_experimental_status(params):
187 |             """
188 |             Also listen for experimental/serverStatus as a backup signal
189 |             """
190 |             if params.get("quiescent") is True:
191 |                 self.server_ready.set()
192 |                 self.completions_available.set()
193 | 
194 |         self.server.on_request("client/registerCapability", register_capability_handler)
195 |         self.server.on_notification("window/logMessage", window_log_message)
196 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
197 |         self.server.on_request("workspace/configuration", workspace_configuration_handler)
198 |         self.server.on_notification("$/progress", do_nothing)
199 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
200 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
201 | 
202 |         self.logger.log("Starting VTS server process", logging.INFO)
203 |         self.server.start()
204 |         initialize_params = self._get_initialize_params(self.repository_root_path)
205 | 
206 |         self.logger.log(
207 |             "Sending initialize request from LSP client to LSP server and awaiting response",
208 |             logging.INFO,
209 |         )
210 |         init_response = self.server.send.initialize(initialize_params)
211 | 
212 |         # VTS-specific capability checks
213 |         # Be more flexible with capabilities since vtsls might have different structure
214 |         self.logger.log(f"VTS init response capabilities: {init_response['capabilities']}", logging.DEBUG)
215 | 
216 |         # Basic checks to ensure essential capabilities are present
217 |         assert "textDocumentSync" in init_response["capabilities"]
218 |         assert "completionProvider" in init_response["capabilities"]
219 | 
220 |         # Log the actual values for debugging
221 |         self.logger.log(f"textDocumentSync: {init_response['capabilities']['textDocumentSync']}", logging.DEBUG)
222 |         self.logger.log(f"completionProvider: {init_response['capabilities']['completionProvider']}", logging.DEBUG)
223 | 
224 |         self.server.notify.initialized({})
225 |         if self.server_ready.wait(timeout=1.0):
226 |             self.logger.log("VTS server is ready", logging.INFO)
227 |         else:
228 |             self.logger.log("Timeout waiting for VTS server to become ready, proceeding anyway", logging.INFO)
229 |             # Fallback: assume server is ready after timeout
230 |             self.server_ready.set()
231 |         self.completions_available.set()
232 | 
233 |     @override
234 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
235 |         return 1
236 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/bash_language_server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
  3 | Contains various configurations and settings specific to Bash scripting.
  4 | """
  5 | 
  6 | import logging
  7 | import os
  8 | import pathlib
  9 | import shutil
 10 | import threading
 11 | 
 12 | from solidlsp import ls_types
 13 | from solidlsp.language_servers.common import RuntimeDependency, RuntimeDependencyCollection
 14 | from solidlsp.ls import SolidLanguageServer
 15 | from solidlsp.ls_config import LanguageServerConfig
 16 | from solidlsp.ls_logger import LanguageServerLogger
 17 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 18 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 19 | from solidlsp.settings import SolidLSPSettings
 20 | 
 21 | 
 22 | class BashLanguageServer(SolidLanguageServer):
 23 |     """
 24 |     Provides Bash specific instantiation of the LanguageServer class using bash-language-server.
 25 |     Contains various configurations and settings specific to Bash scripting.
 26 |     """
 27 | 
 28 |     def __init__(
 29 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 30 |     ):
 31 |         """
 32 |         Creates a BashLanguageServer instance. This class is not meant to be instantiated directly.
 33 |         Use LanguageServer.create() instead.
 34 |         """
 35 |         bash_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 36 |         super().__init__(
 37 |             config,
 38 |             logger,
 39 |             repository_root_path,
 40 |             ProcessLaunchInfo(cmd=bash_lsp_executable_path, cwd=repository_root_path),
 41 |             "bash",
 42 |             solidlsp_settings,
 43 |         )
 44 |         self.server_ready = threading.Event()
 45 |         self.initialize_searcher_command_available = threading.Event()
 46 | 
 47 |     @classmethod
 48 |     def _setup_runtime_dependencies(
 49 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
 50 |     ) -> str:
 51 |         """
 52 |         Setup runtime dependencies for Bash Language Server and return the command to start the server.
 53 |         """
 54 |         # Verify both node and npm are installed
 55 |         is_node_installed = shutil.which("node") is not None
 56 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
 57 |         is_npm_installed = shutil.which("npm") is not None
 58 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
 59 | 
 60 |         deps = RuntimeDependencyCollection(
 61 |             [
 62 |                 RuntimeDependency(
 63 |                     id="bash-language-server",
 64 |                     description="bash-language-server package",
 65 |                     command="npm install --prefix ./ [email protected]",
 66 |                     platform_id="any",
 67 |                 ),
 68 |             ]
 69 |         )
 70 | 
 71 |         # Install bash-language-server if not already installed
 72 |         bash_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "bash-lsp")
 73 |         bash_executable_path = os.path.join(bash_ls_dir, "node_modules", ".bin", "bash-language-server")
 74 | 
 75 |         # Handle Windows executable extension
 76 |         if os.name == "nt":
 77 |             bash_executable_path += ".cmd"
 78 | 
 79 |         if not os.path.exists(bash_executable_path):
 80 |             logger.log(f"Bash Language Server executable not found at {bash_executable_path}. Installing...", logging.INFO)
 81 |             deps.install(logger, bash_ls_dir)
 82 |             logger.log("Bash language server dependencies installed successfully", logging.INFO)
 83 | 
 84 |         if not os.path.exists(bash_executable_path):
 85 |             raise FileNotFoundError(
 86 |                 f"bash-language-server executable not found at {bash_executable_path}, something went wrong with the installation."
 87 |             )
 88 |         return f"{bash_executable_path} start"
 89 | 
 90 |     @staticmethod
 91 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
 92 |         """
 93 |         Returns the initialize params for the Bash Language Server.
 94 |         """
 95 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
 96 |         initialize_params = {
 97 |             "locale": "en",
 98 |             "capabilities": {
 99 |                 "textDocument": {
100 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
101 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
102 |                     "definition": {"dynamicRegistration": True},
103 |                     "references": {"dynamicRegistration": True},
104 |                     "documentSymbol": {
105 |                         "dynamicRegistration": True,
106 |                         "hierarchicalDocumentSymbolSupport": True,
107 |                         "symbolKind": {"valueSet": list(range(1, 27))},
108 |                     },
109 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
110 |                     "signatureHelp": {"dynamicRegistration": True},
111 |                     "codeAction": {"dynamicRegistration": True},
112 |                 },
113 |                 "workspace": {
114 |                     "workspaceFolders": True,
115 |                     "didChangeConfiguration": {"dynamicRegistration": True},
116 |                     "symbol": {"dynamicRegistration": True},
117 |                 },
118 |             },
119 |             "processId": os.getpid(),
120 |             "rootPath": repository_absolute_path,
121 |             "rootUri": root_uri,
122 |             "workspaceFolders": [
123 |                 {
124 |                     "uri": root_uri,
125 |                     "name": os.path.basename(repository_absolute_path),
126 |                 }
127 |             ],
128 |         }
129 |         return initialize_params
130 | 
131 |     def _start_server(self):
132 |         """
133 |         Starts the Bash Language Server, waits for the server to be ready and yields the LanguageServer instance.
134 |         """
135 | 
136 |         def register_capability_handler(params):
137 |             assert "registrations" in params
138 |             for registration in params["registrations"]:
139 |                 if registration["method"] == "workspace/executeCommand":
140 |                     self.initialize_searcher_command_available.set()
141 |             return
142 | 
143 |         def execute_client_command_handler(params):
144 |             return []
145 | 
146 |         def do_nothing(params):
147 |             return
148 | 
149 |         def window_log_message(msg):
150 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
151 |             # Check for bash-language-server ready signals
152 |             message_text = msg.get("message", "")
153 |             if "Analyzing" in message_text or "analysis complete" in message_text.lower():
154 |                 self.logger.log("Bash language server analysis signals detected", logging.INFO)
155 |                 self.server_ready.set()
156 |                 self.completions_available.set()
157 | 
158 |         self.server.on_request("client/registerCapability", register_capability_handler)
159 |         self.server.on_notification("window/logMessage", window_log_message)
160 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
161 |         self.server.on_notification("$/progress", do_nothing)
162 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
163 | 
164 |         self.logger.log("Starting Bash server process", logging.INFO)
165 |         self.server.start()
166 |         initialize_params = self._get_initialize_params(self.repository_root_path)
167 | 
168 |         self.logger.log(
169 |             "Sending initialize request from LSP client to LSP server and awaiting response",
170 |             logging.INFO,
171 |         )
172 |         init_response = self.server.send.initialize(initialize_params)
173 |         self.logger.log(f"Received initialize response from bash server: {init_response}", logging.DEBUG)
174 | 
175 |         # Enhanced capability checks for bash-language-server 5.6.0
176 |         assert init_response["capabilities"]["textDocumentSync"] in [1, 2]  # Full or Incremental
177 |         assert "completionProvider" in init_response["capabilities"]
178 | 
179 |         # Verify document symbol support is available
180 |         if "documentSymbolProvider" in init_response["capabilities"]:
181 |             self.logger.log("Bash server supports document symbols", logging.INFO)
182 |         else:
183 |             self.logger.log("Warning: Bash server does not report document symbol support", logging.WARNING)
184 | 
185 |         self.server.notify.initialized({})
186 | 
187 |         # Wait for server readiness with timeout
188 |         self.logger.log("Waiting for Bash language server to be ready...", logging.INFO)
189 |         if not self.server_ready.wait(timeout=3.0):
190 |             # Fallback: assume server is ready after timeout
191 |             self.logger.log("Timeout waiting for bash server ready signal, proceeding anyway", logging.WARNING)
192 |             self.server_ready.set()
193 |             self.completions_available.set()
194 |         else:
195 |             self.logger.log("Bash server initialization complete", logging.INFO)
196 | 
197 |     def request_document_symbols(
198 |         self, relative_file_path: str, include_body: bool = False
199 |     ) -> tuple[list[ls_types.UnifiedSymbolInformation], list[ls_types.UnifiedSymbolInformation]]:
200 |         """
201 |         Request document symbols from bash-language-server via LSP.
202 | 
203 |         Uses the standard LSP documentSymbol request which provides reliable function detection
204 |         for all bash function syntaxes including:
205 |         - function name() { ... } (with function keyword)
206 |         - name() { ... } (traditional syntax)
207 |         - Functions with various indentation levels
208 |         - Functions with comments before/after/inside
209 | 
210 |         Args:
211 |             relative_file_path: Path to the bash file relative to repository root
212 |             include_body: Whether to include function bodies in symbol information
213 | 
214 |         Returns:
215 |             Tuple of (all_symbols, root_symbols) detected by the LSP server
216 | 
217 |         """
218 |         self.logger.log(f"Requesting document symbols via LSP for {relative_file_path}", logging.DEBUG)
219 | 
220 |         # Use the standard LSP approach - bash-language-server handles all function syntaxes correctly
221 |         all_symbols, root_symbols = super().request_document_symbols(relative_file_path, include_body)
222 | 
223 |         # Log detection results for debugging
224 |         functions = [s for s in all_symbols if s.get("kind") == 12]
225 |         self.logger.log(
226 |             f"LSP function detection for {relative_file_path}: Found {len(functions)} functions",
227 |             logging.INFO,
228 |         )
229 | 
230 |         return all_symbols, root_symbols
231 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/zls.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Provides Zig specific instantiation of the LanguageServer class using ZLS (Zig Language Server).
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import platform
  9 | import shutil
 10 | import subprocess
 11 | import threading
 12 | 
 13 | from overrides import override
 14 | 
 15 | from solidlsp.ls import SolidLanguageServer
 16 | from solidlsp.ls_config import LanguageServerConfig
 17 | from solidlsp.ls_logger import LanguageServerLogger
 18 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 19 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 20 | from solidlsp.settings import SolidLSPSettings
 21 | 
 22 | 
 23 | class ZigLanguageServer(SolidLanguageServer):
 24 |     """
 25 |     Provides Zig specific instantiation of the LanguageServer class using ZLS.
 26 |     """
 27 | 
 28 |     @override
 29 |     def is_ignored_dirname(self, dirname: str) -> bool:
 30 |         # For Zig projects, we should ignore:
 31 |         # - zig-cache: build cache directory
 32 |         # - zig-out: default build output directory
 33 |         # - .zig-cache: alternative cache location
 34 |         # - node_modules: if the project has JavaScript components
 35 |         return super().is_ignored_dirname(dirname) or dirname in ["zig-cache", "zig-out", ".zig-cache", "node_modules", "build", "dist"]
 36 | 
 37 |     @staticmethod
 38 |     def _get_zig_version():
 39 |         """Get the installed Zig version or None if not found."""
 40 |         try:
 41 |             result = subprocess.run(["zig", "version"], capture_output=True, text=True, check=False)
 42 |             if result.returncode == 0:
 43 |                 return result.stdout.strip()
 44 |         except FileNotFoundError:
 45 |             return None
 46 |         return None
 47 | 
 48 |     @staticmethod
 49 |     def _get_zls_version():
 50 |         """Get the installed ZLS version or None if not found."""
 51 |         try:
 52 |             result = subprocess.run(["zls", "--version"], capture_output=True, text=True, check=False)
 53 |             if result.returncode == 0:
 54 |                 return result.stdout.strip()
 55 |         except FileNotFoundError:
 56 |             return None
 57 |         return None
 58 | 
 59 |     @staticmethod
 60 |     def _check_zls_installed():
 61 |         """Check if ZLS is installed in the system."""
 62 |         return shutil.which("zls") is not None
 63 | 
 64 |     @staticmethod
 65 |     def _setup_runtime_dependency():
 66 |         """
 67 |         Check if required Zig runtime dependencies are available.
 68 |         Raises RuntimeError with helpful message if dependencies are missing.
 69 |         """
 70 |         # Check for Windows and provide error message
 71 |         if platform.system() == "Windows":
 72 |             raise RuntimeError(
 73 |                 "Windows is not supported by ZLS in this integration. "
 74 |                 "Cross-file references don't work reliably on Windows. Reason unknown."
 75 |             )
 76 | 
 77 |         zig_version = ZigLanguageServer._get_zig_version()
 78 |         if not zig_version:
 79 |             raise RuntimeError(
 80 |                 "Zig is not installed. Please install Zig from https://ziglang.org/download/ and make sure it is added to your PATH."
 81 |             )
 82 | 
 83 |         if not ZigLanguageServer._check_zls_installed():
 84 |             zls_version = ZigLanguageServer._get_zls_version()
 85 |             if not zls_version:
 86 |                 raise RuntimeError(
 87 |                     "Found Zig but ZLS (Zig Language Server) is not installed.\n"
 88 |                     "Please install ZLS from https://github.com/zigtools/zls\n"
 89 |                     "You can install it via:\n"
 90 |                     "  - Package managers (brew install zls, scoop install zls, etc.)\n"
 91 |                     "  - Download pre-built binaries from GitHub releases\n"
 92 |                     "  - Build from source with: zig build -Doptimize=ReleaseSafe\n\n"
 93 |                     "After installation, make sure 'zls' is added to your PATH."
 94 |                 )
 95 | 
 96 |         return True
 97 | 
 98 |     def __init__(
 99 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
100 |     ):
101 |         self._setup_runtime_dependency()
102 | 
103 |         super().__init__(
104 |             config,
105 |             logger,
106 |             repository_root_path,
107 |             ProcessLaunchInfo(cmd="zls", cwd=repository_root_path),
108 |             "zig",
109 |             solidlsp_settings,
110 |         )
111 |         self.server_ready = threading.Event()
112 |         self.request_id = 0
113 | 
114 |     @staticmethod
115 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
116 |         """
117 |         Returns the initialize params for the Zig Language Server.
118 |         """
119 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
120 |         initialize_params = {
121 |             "locale": "en",
122 |             "capabilities": {
123 |                 "textDocument": {
124 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
125 |                     "definition": {"dynamicRegistration": True},
126 |                     "references": {"dynamicRegistration": True},
127 |                     "documentSymbol": {
128 |                         "dynamicRegistration": True,
129 |                         "hierarchicalDocumentSymbolSupport": True,
130 |                         "symbolKind": {"valueSet": list(range(1, 27))},
131 |                     },
132 |                     "completion": {
133 |                         "dynamicRegistration": True,
134 |                         "completionItem": {
135 |                             "snippetSupport": True,
136 |                             "commitCharactersSupport": True,
137 |                             "documentationFormat": ["markdown", "plaintext"],
138 |                             "deprecatedSupport": True,
139 |                             "preselectSupport": True,
140 |                         },
141 |                     },
142 |                     "hover": {
143 |                         "dynamicRegistration": True,
144 |                         "contentFormat": ["markdown", "plaintext"],
145 |                     },
146 |                 },
147 |                 "workspace": {
148 |                     "workspaceFolders": True,
149 |                     "didChangeConfiguration": {"dynamicRegistration": True},
150 |                     "configuration": True,
151 |                 },
152 |             },
153 |             "processId": os.getpid(),
154 |             "rootPath": repository_absolute_path,
155 |             "rootUri": root_uri,
156 |             "workspaceFolders": [
157 |                 {
158 |                     "uri": root_uri,
159 |                     "name": os.path.basename(repository_absolute_path),
160 |                 }
161 |             ],
162 |             "initializationOptions": {
163 |                 # ZLS specific options based on schema.json
164 |                 # Critical paths for ZLS to understand the project
165 |                 "zig_exe_path": shutil.which("zig"),  # Path to zig executable
166 |                 "zig_lib_path": None,  # Let ZLS auto-detect
167 |                 "build_runner_path": None,  # Let ZLS use its built-in runner
168 |                 "global_cache_path": None,  # Let ZLS use default cache
169 |                 # Build configuration
170 |                 "enable_build_on_save": True,  # Enable to analyze project structure
171 |                 "build_on_save_args": ["build"],
172 |                 # Features
173 |                 "enable_snippets": True,
174 |                 "enable_argument_placeholders": True,
175 |                 "semantic_tokens": "full",
176 |                 "warn_style": False,
177 |                 "highlight_global_var_declarations": False,
178 |                 "skip_std_references": False,
179 |                 "prefer_ast_check_as_child_process": True,
180 |                 "completion_label_details": True,
181 |                 # Inlay hints configuration
182 |                 "inlay_hints_show_variable_type_hints": True,
183 |                 "inlay_hints_show_struct_literal_field_type": True,
184 |                 "inlay_hints_show_parameter_name": True,
185 |                 "inlay_hints_show_builtin": True,
186 |                 "inlay_hints_exclude_single_argument": True,
187 |                 "inlay_hints_hide_redundant_param_names": False,
188 |                 "inlay_hints_hide_redundant_param_names_last_token": False,
189 |             },
190 |         }
191 |         return initialize_params
192 | 
193 |     def _start_server(self):
194 |         """Start ZLS server process"""
195 | 
196 |         def register_capability_handler(params):
197 |             return
198 | 
199 |         def window_log_message(msg):
200 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
201 | 
202 |         def do_nothing(params):
203 |             return
204 | 
205 |         self.server.on_request("client/registerCapability", register_capability_handler)
206 |         self.server.on_notification("window/logMessage", window_log_message)
207 |         self.server.on_notification("$/progress", do_nothing)
208 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
209 | 
210 |         self.logger.log("Starting ZLS server process", logging.INFO)
211 |         self.server.start()
212 |         initialize_params = self._get_initialize_params(self.repository_root_path)
213 | 
214 |         self.logger.log(
215 |             "Sending initialize request from LSP client to LSP server and awaiting response",
216 |             logging.INFO,
217 |         )
218 |         init_response = self.server.send.initialize(initialize_params)
219 | 
220 |         # Verify server capabilities
221 |         assert "textDocumentSync" in init_response["capabilities"]
222 |         assert "definitionProvider" in init_response["capabilities"]
223 |         assert "documentSymbolProvider" in init_response["capabilities"]
224 |         assert "referencesProvider" in init_response["capabilities"]
225 | 
226 |         self.server.notify.initialized({})
227 |         self.completions_available.set()
228 | 
229 |         # ZLS server is ready after initialization
230 |         self.server_ready.set()
231 |         self.server_ready.wait()
232 | 
233 |         # Open build.zig if it exists to help ZLS understand project structure
234 |         build_zig_path = os.path.join(self.repository_root_path, "build.zig")
235 |         if os.path.exists(build_zig_path):
236 |             try:
237 |                 with open(build_zig_path, encoding="utf-8") as f:
238 |                     content = f.read()
239 |                     uri = pathlib.Path(build_zig_path).as_uri()
240 |                     self.server.notify.did_open_text_document(
241 |                         {
242 |                             "textDocument": {
243 |                                 "uri": uri,
244 |                                 "languageId": "zig",
245 |                                 "version": 1,
246 |                                 "text": content,
247 |                             }
248 |                         }
249 |                     )
250 |                     self.logger.log("Opened build.zig to provide project context to ZLS", logging.INFO)
251 |             except Exception as e:
252 |                 self.logger.log(f"Failed to open build.zig: {e}", logging.WARNING)
253 | 
```

--------------------------------------------------------------------------------
/test/serena/test_symbol.py:
--------------------------------------------------------------------------------

```python
  1 | import pytest
  2 | 
  3 | from src.serena.symbol import LanguageServerSymbol
  4 | 
  5 | 
  6 | class TestSymbolNameMatching:
  7 |     def _create_assertion_error_message(
  8 |         self,
  9 |         name_path_pattern: str,
 10 |         symbol_name_path_parts: list[str],
 11 |         is_substring_match: bool,
 12 |         expected_result: bool,
 13 |         actual_result: bool,
 14 |     ) -> str:
 15 |         """Helper to create a detailed error message for assertions."""
 16 |         qnp_repr = "/".join(symbol_name_path_parts)
 17 | 
 18 |         return (
 19 |             f"Pattern '{name_path_pattern}' (substring: {is_substring_match}) vs "
 20 |             f"Qualname parts {symbol_name_path_parts} (as '{qnp_repr}'). "
 21 |             f"Expected: {expected_result}, Got: {actual_result}"
 22 |         )
 23 | 
 24 |     @pytest.mark.parametrize(
 25 |         "name_path_pattern, symbol_name_path_parts, is_substring_match, expected",
 26 |         [
 27 |             # Exact matches, anywhere in the name (is_substring_match=False)
 28 |             pytest.param("foo", ["foo"], False, True, id="'foo' matches 'foo' exactly (simple)"),
 29 |             pytest.param("foo/", ["foo"], False, True, id="'foo/' matches 'foo' exactly (simple)"),
 30 |             pytest.param("foo", ["bar", "foo"], False, True, id="'foo' matches ['bar', 'foo'] exactly (simple, last element)"),
 31 |             pytest.param("foo", ["foobar"], False, False, id="'foo' does not match 'foobar' exactly (simple)"),
 32 |             pytest.param(
 33 |                 "foo", ["bar", "foobar"], False, False, id="'foo' does not match ['bar', 'foobar'] exactly (simple, last element)"
 34 |             ),
 35 |             pytest.param(
 36 |                 "foo", ["path", "to", "foo"], False, True, id="'foo' matches ['path', 'to', 'foo'] exactly (simple, last element)"
 37 |             ),
 38 |             # Exact matches, absolute patterns (is_substring_match=False)
 39 |             pytest.param("/foo", ["foo"], False, True, id="'/foo' matches ['foo'] exactly (absolute simple)"),
 40 |             pytest.param("/foo", ["foo", "bar"], False, False, id="'/foo' does not match ['foo', 'bar'] (absolute simple, len mismatch)"),
 41 |             pytest.param("/foo", ["bar"], False, False, id="'/foo' does not match ['bar'] (absolute simple, name mismatch)"),
 42 |             pytest.param(
 43 |                 "/foo", ["bar", "foo"], False, False, id="'/foo' does not match ['bar', 'foo'] (absolute simple, position mismatch)"
 44 |             ),
 45 |             # Substring matches, anywhere in the name (is_substring_match=True)
 46 |             pytest.param("foo", ["foobar"], True, True, id="'foo' matches 'foobar' as substring (simple)"),
 47 |             pytest.param("foo", ["bar", "foobar"], True, True, id="'foo' matches ['bar', 'foobar'] as substring (simple, last element)"),
 48 |             pytest.param(
 49 |                 "foo", ["barfoo"], True, True, id="'foo' matches 'barfoo' as substring (simple)"
 50 |             ),  # This was potentially ambiguous before
 51 |             pytest.param("foo", ["baz"], True, False, id="'foo' does not match 'baz' as substring (simple)"),
 52 |             pytest.param("foo", ["bar", "baz"], True, False, id="'foo' does not match ['bar', 'baz'] as substring (simple, last element)"),
 53 |             pytest.param("foo", ["my_foobar_func"], True, True, id="'foo' matches 'my_foobar_func' as substring (simple)"),
 54 |             pytest.param(
 55 |                 "foo",
 56 |                 ["ClassA", "my_foobar_method"],
 57 |                 True,
 58 |                 True,
 59 |                 id="'foo' matches ['ClassA', 'my_foobar_method'] as substring (simple, last element)",
 60 |             ),
 61 |             pytest.param("foo", ["my_bar_func"], True, False, id="'foo' does not match 'my_bar_func' as substring (simple)"),
 62 |             # Substring matches, absolute patterns (is_substring_match=True)
 63 |             pytest.param("/foo", ["foobar"], True, True, id="'/foo' matches ['foobar'] as substring (absolute simple)"),
 64 |             pytest.param("/foo/", ["foobar"], True, True, id="'/foo/' matches ['foobar'] as substring (absolute simple, last element)"),
 65 |             pytest.param("/foo", ["barfoobaz"], True, True, id="'/foo' matches ['barfoobaz'] as substring (absolute simple)"),
 66 |             pytest.param(
 67 |                 "/foo", ["foo", "bar"], True, False, id="'/foo' does not match ['foo', 'bar'] as substring (absolute simple, len mismatch)"
 68 |             ),
 69 |             pytest.param("/foo", ["bar"], True, False, id="'/foo' does not match ['bar'] (absolute simple, no substr)"),
 70 |             pytest.param(
 71 |                 "/foo", ["bar", "foo"], True, False, id="'/foo' does not match ['bar', 'foo'] (absolute simple, position mismatch)"
 72 |             ),
 73 |             pytest.param(
 74 |                 "/foo/", ["bar", "foo"], True, False, id="'/foo/' does not match ['bar', 'foo'] (absolute simple, position mismatch)"
 75 |             ),
 76 |         ],
 77 |     )
 78 |     def test_match_simple_name(self, name_path_pattern, symbol_name_path_parts, is_substring_match, expected):
 79 |         """Tests matching for simple names (no '/' in pattern)."""
 80 |         result = LanguageServerSymbol.match_name_path(name_path_pattern, symbol_name_path_parts, is_substring_match)
 81 |         error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, is_substring_match, expected, result)
 82 |         assert result == expected, error_msg
 83 | 
 84 |     @pytest.mark.parametrize(
 85 |         "name_path_pattern, symbol_name_path_parts, is_substring_match, expected",
 86 |         [
 87 |             # --- Relative patterns (suffix matching) ---
 88 |             # Exact matches, relative patterns (is_substring_match=False)
 89 |             pytest.param("bar/foo", ["bar", "foo"], False, True, id="R: 'bar/foo' matches ['bar', 'foo'] exactly"),
 90 |             pytest.param("bar/foo", ["mod", "bar", "foo"], False, True, id="R: 'bar/foo' matches ['mod', 'bar', 'foo'] exactly (suffix)"),
 91 |             pytest.param(
 92 |                 "bar/foo", ["bar", "foo", "baz"], False, False, id="R: 'bar/foo' does not match ['bar', 'foo', 'baz'] (pattern shorter)"
 93 |             ),
 94 |             pytest.param("bar/foo", ["bar"], False, False, id="R: 'bar/foo' does not match ['bar'] (pattern longer)"),
 95 |             pytest.param("bar/foo", ["baz", "foo"], False, False, id="R: 'bar/foo' does not match ['baz', 'foo'] (first part mismatch)"),
 96 |             pytest.param("bar/foo", ["bar", "baz"], False, False, id="R: 'bar/foo' does not match ['bar', 'baz'] (last part mismatch)"),
 97 |             pytest.param("bar/foo", ["foo"], False, False, id="R: 'bar/foo' does not match ['foo'] (pattern longer)"),
 98 |             pytest.param(
 99 |                 "bar/foo", ["other", "foo"], False, False, id="R: 'bar/foo' does not match ['other', 'foo'] (first part mismatch)"
100 |             ),
101 |             pytest.param(
102 |                 "bar/foo", ["bar", "otherfoo"], False, False, id="R: 'bar/foo' does not match ['bar', 'otherfoo'] (last part mismatch)"
103 |             ),
104 |             # Substring matches, relative patterns (is_substring_match=True)
105 |             pytest.param("bar/foo", ["bar", "foobar"], True, True, id="R: 'bar/foo' matches ['bar', 'foobar'] as substring"),
106 |             pytest.param(
107 |                 "bar/foo", ["mod", "bar", "foobar"], True, True, id="R: 'bar/foo' matches ['mod', 'bar', 'foobar'] as substring (suffix)"
108 |             ),
109 |             pytest.param("bar/foo", ["bar", "bazfoo"], True, True, id="R: 'bar/foo' matches ['bar', 'bazfoo'] as substring"),
110 |             pytest.param("bar/fo", ["bar", "foo"], True, True, id="R: 'bar/fo' matches ['bar', 'foo'] as substring"),  # codespell:ignore
111 |             pytest.param("bar/foo", ["bar", "baz"], True, False, id="R: 'bar/foo' does not match ['bar', 'baz'] (last no substr)"),
112 |             pytest.param(
113 |                 "bar/foo", ["baz", "foobar"], True, False, id="R: 'bar/foo' does not match ['baz', 'foobar'] (first part mismatch)"
114 |             ),
115 |             pytest.param(
116 |                 "bar/foo", ["bar", "my_foobar_method"], True, True, id="R: 'bar/foo' matches ['bar', 'my_foobar_method'] as substring"
117 |             ),
118 |             pytest.param(
119 |                 "bar/foo",
120 |                 ["mod", "bar", "my_foobar_method"],
121 |                 True,
122 |                 True,
123 |                 id="R: 'bar/foo' matches ['mod', 'bar', 'my_foobar_method'] as substring (suffix)",
124 |             ),
125 |             pytest.param(
126 |                 "bar/foo",
127 |                 ["bar", "another_method"],
128 |                 True,
129 |                 False,
130 |                 id="R: 'bar/foo' does not match ['bar', 'another_method'] (last no substr)",
131 |             ),
132 |             pytest.param(
133 |                 "bar/foo",
134 |                 ["other", "my_foobar_method"],
135 |                 True,
136 |                 False,
137 |                 id="R: 'bar/foo' does not match ['other', 'my_foobar_method'] (first part mismatch)",
138 |             ),
139 |             pytest.param("bar/f", ["bar", "foo"], True, True, id="R: 'bar/f' matches ['bar', 'foo'] as substring"),
140 |             # Exact matches, absolute patterns (is_substring_match=False)
141 |             pytest.param("/bar/foo", ["bar", "foo"], False, True, id="A: '/bar/foo' matches ['bar', 'foo'] exactly"),
142 |             pytest.param(
143 |                 "/bar/foo", ["bar", "foo", "baz"], False, False, id="A: '/bar/foo' does not match ['bar', 'foo', 'baz'] (pattern shorter)"
144 |             ),
145 |             pytest.param("/bar/foo", ["bar"], False, False, id="A: '/bar/foo' does not match ['bar'] (pattern longer)"),
146 |             pytest.param("/bar/foo", ["baz", "foo"], False, False, id="A: '/bar/foo' does not match ['baz', 'foo'] (first part mismatch)"),
147 |             pytest.param("/bar/foo", ["bar", "baz"], False, False, id="A: '/bar/foo' does not match ['bar', 'baz'] (last part mismatch)"),
148 |             # Substring matches (is_substring_match=True)
149 |             pytest.param("/bar/foo", ["bar", "foobar"], True, True, id="A: '/bar/foo' matches ['bar', 'foobar'] as substring"),
150 |             pytest.param("/bar/foo", ["bar", "bazfoo"], True, True, id="A: '/bar/foo' matches ['bar', 'bazfoo'] as substring"),
151 |             pytest.param("/bar/fo", ["bar", "foo"], True, True, id="A: '/bar/fo' matches ['bar', 'foo'] as substring"),  # codespell:ignore
152 |             pytest.param("/bar/foo", ["bar", "baz"], True, False, id="A: '/bar/foo' does not match ['bar', 'baz'] (last no substr)"),
153 |             pytest.param(
154 |                 "/bar/foo", ["baz", "foobar"], True, False, id="A: '/bar/foo' does not match ['baz', 'foobar'] (first part mismatch)"
155 |             ),
156 |         ],
157 |     )
158 |     def test_match_name_path_pattern_path_len_2(self, name_path_pattern, symbol_name_path_parts, is_substring_match, expected):
159 |         """Tests matching for qualified names (e.g. 'module/class/func')."""
160 |         result = LanguageServerSymbol.match_name_path(name_path_pattern, symbol_name_path_parts, is_substring_match)
161 |         error_msg = self._create_assertion_error_message(name_path_pattern, symbol_name_path_parts, is_substring_match, expected, result)
162 |         assert result == expected, error_msg
163 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/typescript_language_server.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import shutil
  9 | import threading
 10 | 
 11 | from overrides import override
 12 | from sensai.util.logging import LogTime
 13 | 
 14 | from solidlsp.ls import SolidLanguageServer
 15 | from solidlsp.ls_config import LanguageServerConfig
 16 | from solidlsp.ls_logger import LanguageServerLogger
 17 | from solidlsp.ls_utils import PlatformId, PlatformUtils
 18 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 19 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 20 | from solidlsp.settings import SolidLSPSettings
 21 | 
 22 | from .common import RuntimeDependency, RuntimeDependencyCollection
 23 | 
 24 | # Platform-specific imports
 25 | if os.name != "nt":  # Unix-like systems
 26 |     import pwd
 27 | else:
 28 |     # Dummy pwd module for Windows
 29 |     class pwd:
 30 |         @staticmethod
 31 |         def getpwuid(uid):
 32 |             return type("obj", (), {"pw_name": os.environ.get("USERNAME", "unknown")})()
 33 | 
 34 | 
 35 | # Conditionally import pwd module (Unix-only)
 36 | if not PlatformUtils.get_platform_id().value.startswith("win"):
 37 |     pass
 38 | 
 39 | 
 40 | class TypeScriptLanguageServer(SolidLanguageServer):
 41 |     """
 42 |     Provides TypeScript specific instantiation of the LanguageServer class. Contains various configurations and settings specific to TypeScript.
 43 |     """
 44 | 
 45 |     def __init__(
 46 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
 47 |     ):
 48 |         """
 49 |         Creates a TypeScriptLanguageServer instance. This class is not meant to be instantiated directly. Use LanguageServer.create() instead.
 50 |         """
 51 |         ts_lsp_executable_path = self._setup_runtime_dependencies(logger, config, solidlsp_settings)
 52 |         super().__init__(
 53 |             config,
 54 |             logger,
 55 |             repository_root_path,
 56 |             ProcessLaunchInfo(cmd=ts_lsp_executable_path, cwd=repository_root_path),
 57 |             "typescript",
 58 |             solidlsp_settings,
 59 |         )
 60 |         self.server_ready = threading.Event()
 61 |         self.initialize_searcher_command_available = threading.Event()
 62 | 
 63 |     @override
 64 |     def is_ignored_dirname(self, dirname: str) -> bool:
 65 |         return super().is_ignored_dirname(dirname) or dirname in [
 66 |             "node_modules",
 67 |             "dist",
 68 |             "build",
 69 |             "coverage",
 70 |         ]
 71 | 
 72 |     @classmethod
 73 |     def _setup_runtime_dependencies(
 74 |         cls, logger: LanguageServerLogger, config: LanguageServerConfig, solidlsp_settings: SolidLSPSettings
 75 |     ) -> list[str]:
 76 |         """
 77 |         Setup runtime dependencies for TypeScript Language Server and return the command to start the server.
 78 |         """
 79 |         platform_id = PlatformUtils.get_platform_id()
 80 | 
 81 |         valid_platforms = [
 82 |             PlatformId.LINUX_x64,
 83 |             PlatformId.LINUX_arm64,
 84 |             PlatformId.OSX,
 85 |             PlatformId.OSX_x64,
 86 |             PlatformId.OSX_arm64,
 87 |             PlatformId.WIN_x64,
 88 |             PlatformId.WIN_arm64,
 89 |         ]
 90 |         assert platform_id in valid_platforms, f"Platform {platform_id} is not supported for multilspy javascript/typescript at the moment"
 91 | 
 92 |         deps = RuntimeDependencyCollection(
 93 |             [
 94 |                 RuntimeDependency(
 95 |                     id="typescript",
 96 |                     description="typescript package",
 97 |                     command=["npm", "install", "--prefix", "./", "[email protected]"],
 98 |                     platform_id="any",
 99 |                 ),
100 |                 RuntimeDependency(
101 |                     id="typescript-language-server",
102 |                     description="typescript-language-server package",
103 |                     command=["npm", "install", "--prefix", "./", "[email protected]"],
104 |                     platform_id="any",
105 |                 ),
106 |             ]
107 |         )
108 | 
109 |         # Verify both node and npm are installed
110 |         is_node_installed = shutil.which("node") is not None
111 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
112 |         is_npm_installed = shutil.which("npm") is not None
113 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
114 | 
115 |         # Verify both node and npm are installed
116 |         is_node_installed = shutil.which("node") is not None
117 |         assert is_node_installed, "node is not installed or isn't in PATH. Please install NodeJS and try again."
118 |         is_npm_installed = shutil.which("npm") is not None
119 |         assert is_npm_installed, "npm is not installed or isn't in PATH. Please install npm and try again."
120 | 
121 |         # Install typescript and typescript-language-server if not already installed
122 |         tsserver_ls_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "ts-lsp")
123 |         tsserver_executable_path = os.path.join(tsserver_ls_dir, "node_modules", ".bin", "typescript-language-server")
124 |         if not os.path.exists(tsserver_executable_path):
125 |             logger.log(f"Typescript Language Server executable not found at {tsserver_executable_path}. Installing...", logging.INFO)
126 |             with LogTime("Installation of TypeScript language server dependencies", logger=logger.logger):
127 |                 deps.install(logger, tsserver_ls_dir)
128 | 
129 |         if not os.path.exists(tsserver_executable_path):
130 |             raise FileNotFoundError(
131 |                 f"typescript-language-server executable not found at {tsserver_executable_path}, something went wrong with the installation."
132 |             )
133 |         return [tsserver_executable_path, "--stdio"]
134 | 
135 |     @staticmethod
136 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
137 |         """
138 |         Returns the initialize params for the TypeScript Language Server.
139 |         """
140 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
141 |         initialize_params = {
142 |             "locale": "en",
143 |             "capabilities": {
144 |                 "textDocument": {
145 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
146 |                     "completion": {"dynamicRegistration": True, "completionItem": {"snippetSupport": True}},
147 |                     "definition": {"dynamicRegistration": True},
148 |                     "references": {"dynamicRegistration": True},
149 |                     "documentSymbol": {
150 |                         "dynamicRegistration": True,
151 |                         "hierarchicalDocumentSymbolSupport": True,
152 |                         "symbolKind": {"valueSet": list(range(1, 27))},
153 |                     },
154 |                     "hover": {"dynamicRegistration": True, "contentFormat": ["markdown", "plaintext"]},
155 |                     "signatureHelp": {"dynamicRegistration": True},
156 |                     "codeAction": {"dynamicRegistration": True},
157 |                 },
158 |                 "workspace": {
159 |                     "workspaceFolders": True,
160 |                     "didChangeConfiguration": {"dynamicRegistration": True},
161 |                     "symbol": {"dynamicRegistration": True},
162 |                 },
163 |             },
164 |             "processId": os.getpid(),
165 |             "rootPath": repository_absolute_path,
166 |             "rootUri": root_uri,
167 |             "workspaceFolders": [
168 |                 {
169 |                     "uri": root_uri,
170 |                     "name": os.path.basename(repository_absolute_path),
171 |                 }
172 |             ],
173 |         }
174 |         return initialize_params
175 | 
176 |     def _start_server(self):
177 |         """
178 |         Starts the TypeScript Language Server, waits for the server to be ready and yields the LanguageServer instance.
179 | 
180 |         Usage:
181 |         ```
182 |         async with lsp.start_server():
183 |             # LanguageServer has been initialized and ready to serve requests
184 |             await lsp.request_definition(...)
185 |             await lsp.request_references(...)
186 |             # Shutdown the LanguageServer on exit from scope
187 |         # LanguageServer has been shutdown
188 |         """
189 | 
190 |         def register_capability_handler(params):
191 |             assert "registrations" in params
192 |             for registration in params["registrations"]:
193 |                 if registration["method"] == "workspace/executeCommand":
194 |                     self.initialize_searcher_command_available.set()
195 |                     # TypeScript doesn't have a direct equivalent to resolve_main_method
196 |                     # You might want to set a different flag or remove this line
197 |                     # self.resolve_main_method_available.set()
198 |             return
199 | 
200 |         def execute_client_command_handler(params):
201 |             return []
202 | 
203 |         def do_nothing(params):
204 |             return
205 | 
206 |         def window_log_message(msg):
207 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
208 | 
209 |         def check_experimental_status(params):
210 |             """
211 |             Also listen for experimental/serverStatus as a backup signal
212 |             """
213 |             if params.get("quiescent") == True:
214 |                 self.server_ready.set()
215 |                 self.completions_available.set()
216 | 
217 |         self.server.on_request("client/registerCapability", register_capability_handler)
218 |         self.server.on_notification("window/logMessage", window_log_message)
219 |         self.server.on_request("workspace/executeClientCommand", execute_client_command_handler)
220 |         self.server.on_notification("$/progress", do_nothing)
221 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
222 |         self.server.on_notification("experimental/serverStatus", check_experimental_status)
223 | 
224 |         self.logger.log("Starting TypeScript server process", logging.INFO)
225 |         self.server.start()
226 |         initialize_params = self._get_initialize_params(self.repository_root_path)
227 | 
228 |         self.logger.log(
229 |             "Sending initialize request from LSP client to LSP server and awaiting response",
230 |             logging.INFO,
231 |         )
232 |         init_response = self.server.send.initialize(initialize_params)
233 | 
234 |         # TypeScript-specific capability checks
235 |         assert init_response["capabilities"]["textDocumentSync"] == 2
236 |         assert "completionProvider" in init_response["capabilities"]
237 |         assert init_response["capabilities"]["completionProvider"] == {
238 |             "triggerCharacters": [".", '"', "'", "/", "@", "<"],
239 |             "resolveProvider": True,
240 |         }
241 | 
242 |         self.server.notify.initialized({})
243 |         if self.server_ready.wait(timeout=1.0):
244 |             self.logger.log("TypeScript server is ready", logging.INFO)
245 |         else:
246 |             self.logger.log("Timeout waiting for TypeScript server to become ready, proceeding anyway", logging.INFO)
247 |             # Fallback: assume server is ready after timeout
248 |             self.server_ready.set()
249 |         self.completions_available.set()
250 | 
251 |     @override
252 |     def _get_wait_time_for_cross_file_referencing(self) -> float:
253 |         return 1
254 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/nix/test_nix_basic.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the Nix language server implementation using nixd.
  3 | 
  4 | These tests validate symbol finding and cross-file reference capabilities for Nix expressions.
  5 | """
  6 | 
  7 | import platform
  8 | 
  9 | import pytest
 10 | 
 11 | from solidlsp import SolidLanguageServer
 12 | from solidlsp.ls_config import Language
 13 | 
 14 | # Skip all Nix tests on Windows as Nix doesn't support Windows
 15 | pytestmark = pytest.mark.skipif(platform.system() == "Windows", reason="Nix and nil are not available on Windows")
 16 | 
 17 | 
 18 | @pytest.mark.nix
 19 | class TestNixLanguageServer:
 20 |     """Test Nix language server symbol finding capabilities."""
 21 | 
 22 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
 23 |     def test_find_symbols_in_default_nix(self, language_server: SolidLanguageServer) -> None:
 24 |         """Test finding specific symbols in default.nix."""
 25 |         symbols = language_server.request_document_symbols("default.nix")
 26 | 
 27 |         assert symbols is not None
 28 |         assert len(symbols) > 0
 29 | 
 30 |         # Extract symbol names from the returned structure
 31 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 32 |         symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
 33 | 
 34 |         # Verify specific function exists
 35 |         assert "makeGreeting" in symbol_names, "makeGreeting function not found"
 36 | 
 37 |         # Verify exact attribute sets are found
 38 |         expected_attrs = {"listUtils", "stringUtils"}
 39 |         found_attrs = symbol_names & expected_attrs
 40 |         assert found_attrs == expected_attrs, f"Expected exactly {expected_attrs}, found {found_attrs}"
 41 | 
 42 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
 43 |     def test_find_symbols_in_utils(self, language_server: SolidLanguageServer) -> None:
 44 |         """Test finding symbols in lib/utils.nix."""
 45 |         symbols = language_server.request_document_symbols("lib/utils.nix")
 46 | 
 47 |         assert symbols is not None
 48 |         assert len(symbols) > 0
 49 | 
 50 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 51 |         symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
 52 | 
 53 |         # Verify exact utility modules are found
 54 |         expected_modules = {"math", "strings", "lists", "attrs"}
 55 |         found_modules = symbol_names & expected_modules
 56 |         assert found_modules == expected_modules, f"Expected exactly {expected_modules}, found {found_modules}"
 57 | 
 58 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
 59 |     def test_find_symbols_in_flake(self, language_server: SolidLanguageServer) -> None:
 60 |         """Test finding symbols in flake.nix."""
 61 |         symbols = language_server.request_document_symbols("flake.nix")
 62 | 
 63 |         assert symbols is not None
 64 |         assert len(symbols) > 0
 65 | 
 66 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 67 |         symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
 68 | 
 69 |         # Flakes must have either inputs or outputs
 70 |         assert "inputs" in symbol_names or "outputs" in symbol_names, "Flake must have inputs or outputs"
 71 | 
 72 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
 73 |     def test_find_symbols_in_module(self, language_server: SolidLanguageServer) -> None:
 74 |         """Test finding symbols in a NixOS module."""
 75 |         symbols = language_server.request_document_symbols("modules/example.nix")
 76 | 
 77 |         assert symbols is not None
 78 |         assert len(symbols) > 0
 79 | 
 80 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 81 |         symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
 82 | 
 83 |         # NixOS modules must have either options or config
 84 |         assert "options" in symbol_names or "config" in symbol_names, "Module must have options or config"
 85 | 
 86 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
 87 |     def test_find_references_within_file(self, language_server: SolidLanguageServer) -> None:
 88 |         """Test finding references within the same file."""
 89 |         symbols = language_server.request_document_symbols("default.nix")
 90 | 
 91 |         assert symbols is not None
 92 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 93 | 
 94 |         # Find makeGreeting function
 95 |         greeting_symbol = None
 96 |         for sym in symbol_list:
 97 |             if sym.get("name") == "makeGreeting":
 98 |                 greeting_symbol = sym
 99 |                 break
100 | 
101 |         assert greeting_symbol is not None, "makeGreeting function not found"
102 |         assert "range" in greeting_symbol, "Symbol must have range information"
103 | 
104 |         range_start = greeting_symbol["range"]["start"]
105 |         refs = language_server.request_references("default.nix", range_start["line"], range_start["character"])
106 | 
107 |         assert refs is not None
108 |         assert isinstance(refs, list)
109 |         # nixd finds at least the inherit statement (line 67)
110 |         assert len(refs) >= 1, f"Should find at least 1 reference to makeGreeting, found {len(refs)}"
111 | 
112 |         # Verify makeGreeting is referenced at expected locations
113 |         if refs:
114 |             ref_lines = sorted([ref["range"]["start"]["line"] for ref in refs])
115 |             # Check if we found the inherit (line 67, 0-indexed: 66)
116 |             assert 66 in ref_lines, f"Should find makeGreeting inherit at line 67, found at lines {[l+1 for l in ref_lines]}"
117 | 
118 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
119 |     def test_hover_information(self, language_server: SolidLanguageServer) -> None:
120 |         """Test hover information for symbols."""
121 |         # Get hover info for makeGreeting function
122 |         hover_info = language_server.request_hover("default.nix", 9, 5)  # Position near makeGreeting
123 | 
124 |         assert hover_info is not None, "Should provide hover information"
125 | 
126 |         if isinstance(hover_info, dict) and len(hover_info) > 0:
127 |             # If hover info is provided, it should have proper structure
128 |             assert "contents" in hover_info or "value" in hover_info, "Hover should have contents or value"
129 | 
130 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
131 |     def test_cross_file_references_utils_import(self, language_server: SolidLanguageServer) -> None:
132 |         """Test finding cross-file references for imported utils."""
133 |         # Find references to 'utils' which is imported in default.nix from lib/utils.nix
134 |         # Line 10 in default.nix: utils = import ./lib/utils.nix { inherit lib; };
135 |         refs = language_server.request_references("default.nix", 9, 2)  # Position of 'utils'
136 | 
137 |         assert refs is not None
138 |         assert isinstance(refs, list)
139 | 
140 |         # Should find references within default.nix where utils is used
141 |         default_refs = [ref for ref in refs if "default.nix" in ref.get("uri", "")]
142 |         # utils is: imported (line 10), used in listUtils.unique (line 24), inherited in exports (line 69)
143 |         assert len(default_refs) >= 2, f"Should find at least 2 references to utils in default.nix, found {len(default_refs)}"
144 | 
145 |         # Verify utils is referenced at expected locations (0-indexed)
146 |         if default_refs:
147 |             ref_lines = sorted([ref["range"]["start"]["line"] for ref in default_refs])
148 |             # Check for key references - at least the import (line 10) or usage (line 24)
149 |             assert (
150 |                 9 in ref_lines or 23 in ref_lines
151 |             ), f"Should find utils import or usage, found references at lines {[l+1 for l in ref_lines]}"
152 | 
153 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
154 |     def test_verify_imports_exist(self, language_server: SolidLanguageServer) -> None:
155 |         """Verify that our test files have proper imports set up."""
156 |         # Verify that default.nix imports utils from lib/utils.nix
157 |         symbols = language_server.request_document_symbols("default.nix")
158 | 
159 |         assert symbols is not None
160 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
161 | 
162 |         # Check that makeGreeting exists (defined in default.nix)
163 |         symbol_names = {sym.get("name") for sym in symbol_list if isinstance(sym, dict)}
164 |         assert "makeGreeting" in symbol_names, "makeGreeting should be found in default.nix"
165 | 
166 |         # Verify lib/utils.nix has the expected structure
167 |         utils_symbols = language_server.request_document_symbols("lib/utils.nix")
168 |         assert utils_symbols is not None
169 |         utils_list = utils_symbols[0] if isinstance(utils_symbols, tuple) else utils_symbols
170 |         utils_names = {sym.get("name") for sym in utils_list if isinstance(sym, dict)}
171 | 
172 |         # Verify key functions exist in utils
173 |         assert "math" in utils_names, "math should be found in lib/utils.nix"
174 |         assert "strings" in utils_names, "strings should be found in lib/utils.nix"
175 | 
176 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
177 |     def test_go_to_definition_cross_file(self, language_server: SolidLanguageServer) -> None:
178 |         """Test go-to-definition from default.nix to lib/utils.nix."""
179 |         # Line 24 in default.nix: unique = utils.lists.unique;
180 |         # Test go-to-definition for 'utils'
181 |         definitions = language_server.request_definition("default.nix", 23, 14)  # Position of 'utils'
182 | 
183 |         assert definitions is not None
184 |         assert isinstance(definitions, list)
185 | 
186 |         if len(definitions) > 0:
187 |             # Should point to the import statement or utils.nix
188 |             assert any(
189 |                 "utils" in def_item.get("uri", "") or "default.nix" in def_item.get("uri", "") for def_item in definitions
190 |             ), "Definition should relate to utils import or utils.nix file"
191 | 
192 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
193 |     def test_definition_navigation_in_flake(self, language_server: SolidLanguageServer) -> None:
194 |         """Test definition navigation in flake.nix."""
195 |         # Test that we can navigate to definitions within flake.nix
196 |         # Line 69: default = hello-custom;
197 |         definitions = language_server.request_definition("flake.nix", 68, 20)  # Position of 'hello-custom'
198 | 
199 |         assert definitions is not None
200 |         assert isinstance(definitions, list)
201 |         # nixd should find the definition of hello-custom in the same file
202 |         if len(definitions) > 0:
203 |             assert any(
204 |                 "flake.nix" in def_item.get("uri", "") for def_item in definitions
205 |             ), "Should find hello-custom definition in flake.nix"
206 | 
207 |     @pytest.mark.parametrize("language_server", [Language.NIX], indirect=True)
208 |     def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
209 |         """Test that full symbol tree is not empty."""
210 |         symbols = language_server.request_full_symbol_tree()
211 | 
212 |         assert symbols is not None
213 |         assert len(symbols) > 0, "Symbol tree should not be empty"
214 | 
215 |         # The tree should have at least one root node
216 |         root = symbols[0]
217 |         assert isinstance(root, dict), "Root should be a dict"
218 |         assert "name" in root, "Root should have a name"
219 | 
```

--------------------------------------------------------------------------------
/test/serena/test_serena_agent.py:
--------------------------------------------------------------------------------

```python
  1 | import json
  2 | import logging
  3 | import os
  4 | import time
  5 | 
  6 | import pytest
  7 | 
  8 | import test.solidlsp.clojure as clj
  9 | from serena.agent import SerenaAgent
 10 | from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig
 11 | from serena.project import Project
 12 | from serena.tools import FindReferencingSymbolsTool, FindSymbolTool
 13 | from solidlsp.ls_config import Language
 14 | from test.conftest import get_repo_path
 15 | 
 16 | 
 17 | @pytest.fixture
 18 | def serena_config():
 19 |     """Create an in-memory configuration for tests with test repositories pre-registered."""
 20 |     # Create test projects for all supported languages
 21 |     test_projects = []
 22 |     for language in [
 23 |         Language.PYTHON,
 24 |         Language.GO,
 25 |         Language.JAVA,
 26 |         Language.KOTLIN,
 27 |         Language.RUST,
 28 |         Language.TYPESCRIPT,
 29 |         Language.PHP,
 30 |         Language.CSHARP,
 31 |         Language.CLOJURE,
 32 |     ]:
 33 |         repo_path = get_repo_path(language)
 34 |         if repo_path.exists():
 35 |             project_name = f"test_repo_{language}"
 36 |             project = Project(
 37 |                 project_root=str(repo_path),
 38 |                 project_config=ProjectConfig(
 39 |                     project_name=project_name,
 40 |                     language=language,
 41 |                     ignored_paths=[],
 42 |                     excluded_tools=set(),
 43 |                     read_only=False,
 44 |                     ignore_all_files_in_gitignore=True,
 45 |                     initial_prompt="",
 46 |                     encoding="utf-8",
 47 |                 ),
 48 |             )
 49 |             test_projects.append(RegisteredProject.from_project_instance(project))
 50 | 
 51 |     config = SerenaConfig(gui_log_window_enabled=False, web_dashboard=False, log_level=logging.ERROR)
 52 |     config.projects = test_projects
 53 |     return config
 54 | 
 55 | 
 56 | @pytest.fixture
 57 | def serena_agent(request: pytest.FixtureRequest, serena_config):
 58 |     language = Language(request.param)
 59 |     project_name = f"test_repo_{language}"
 60 | 
 61 |     return SerenaAgent(project=project_name, serena_config=serena_config)
 62 | 
 63 | 
 64 | class TestSerenaAgent:
 65 |     @pytest.mark.parametrize(
 66 |         "serena_agent,symbol_name,expected_kind,expected_file",
 67 |         [
 68 |             pytest.param(Language.PYTHON, "User", "Class", "models.py", marks=pytest.mark.python),
 69 |             pytest.param(Language.GO, "Helper", "Function", "main.go", marks=pytest.mark.go),
 70 |             pytest.param(Language.JAVA, "Model", "Class", "Model.java", marks=pytest.mark.java),
 71 |             pytest.param(Language.KOTLIN, "Model", "Struct", "Model.kt", marks=pytest.mark.kotlin),
 72 |             pytest.param(Language.RUST, "add", "Function", "lib.rs", marks=pytest.mark.rust),
 73 |             pytest.param(Language.TYPESCRIPT, "DemoClass", "Class", "index.ts", marks=pytest.mark.typescript),
 74 |             pytest.param(Language.PHP, "helperFunction", "Function", "helper.php", marks=pytest.mark.php),
 75 |             pytest.param(
 76 |                 Language.CLOJURE,
 77 |                 "greet",
 78 |                 "Function",
 79 |                 clj.CORE_PATH,
 80 |                 marks=[pytest.mark.clojure, pytest.mark.skipif(clj.CLI_FAIL, reason=f"Clojure CLI not available: {clj.CLI_FAIL}")],
 81 |             ),
 82 |             pytest.param(Language.CSHARP, "Calculator", "Class", "Program.cs", marks=pytest.mark.csharp),
 83 |         ],
 84 |         indirect=["serena_agent"],
 85 |     )
 86 |     def test_find_symbol(self, serena_agent, symbol_name: str, expected_kind: str, expected_file: str):
 87 |         agent = serena_agent
 88 |         find_symbol_tool = agent.get_tool(FindSymbolTool)
 89 |         result = find_symbol_tool.apply_ex(name_path=symbol_name)
 90 | 
 91 |         symbols = json.loads(result)
 92 |         assert any(
 93 |             symbol_name in s["name_path"] and expected_kind.lower() in s["kind"].lower() and expected_file in s["relative_path"]
 94 |             for s in symbols
 95 |         ), f"Expected to find {symbol_name} ({expected_kind}) in {expected_file}"
 96 | 
 97 |     @pytest.mark.parametrize(
 98 |         "serena_agent,symbol_name,def_file,ref_file",
 99 |         [
100 |             pytest.param(
101 |                 Language.PYTHON,
102 |                 "User",
103 |                 os.path.join("test_repo", "models.py"),
104 |                 os.path.join("test_repo", "services.py"),
105 |                 marks=pytest.mark.python,
106 |             ),
107 |             pytest.param(Language.GO, "Helper", "main.go", "main.go", marks=pytest.mark.go),
108 |             pytest.param(
109 |                 Language.JAVA,
110 |                 "Model",
111 |                 os.path.join("src", "main", "java", "test_repo", "Model.java"),
112 |                 os.path.join("src", "main", "java", "test_repo", "Main.java"),
113 |                 marks=pytest.mark.java,
114 |             ),
115 |             pytest.param(
116 |                 Language.KOTLIN,
117 |                 "Model",
118 |                 os.path.join("src", "main", "kotlin", "test_repo", "Model.kt"),
119 |                 os.path.join("src", "main", "kotlin", "test_repo", "Main.kt"),
120 |                 marks=pytest.mark.kotlin,
121 |             ),
122 |             pytest.param(Language.RUST, "add", os.path.join("src", "lib.rs"), os.path.join("src", "main.rs"), marks=pytest.mark.rust),
123 |             pytest.param(Language.TYPESCRIPT, "helperFunction", "index.ts", "use_helper.ts", marks=pytest.mark.typescript),
124 |             pytest.param(Language.PHP, "helperFunction", "helper.php", "index.php", marks=pytest.mark.php),
125 |             pytest.param(
126 |                 Language.CLOJURE,
127 |                 "multiply",
128 |                 clj.CORE_PATH,
129 |                 clj.UTILS_PATH,
130 |                 marks=[pytest.mark.clojure, pytest.mark.skipif(clj.CLI_FAIL, reason=f"Clojure CLI not available: {clj.CLI_FAIL}")],
131 |             ),
132 |             pytest.param(Language.CSHARP, "Calculator", "Program.cs", "Program.cs", marks=pytest.mark.csharp),
133 |         ],
134 |         indirect=["serena_agent"],
135 |     )
136 |     def test_find_symbol_references(self, serena_agent, symbol_name: str, def_file: str, ref_file: str) -> None:
137 |         agent = serena_agent
138 | 
139 |         # Find the symbol location first
140 |         find_symbol_tool = agent.get_tool(FindSymbolTool)
141 |         result = find_symbol_tool.apply_ex(name_path=symbol_name, relative_path=def_file)
142 | 
143 |         time.sleep(1)
144 |         symbols = json.loads(result)
145 |         # Find the definition
146 |         def_symbol = symbols[0]
147 | 
148 |         # Now find references
149 |         find_refs_tool = agent.get_tool(FindReferencingSymbolsTool)
150 |         result = find_refs_tool.apply_ex(name_path=def_symbol["name_path"], relative_path=def_symbol["relative_path"])
151 | 
152 |         refs = json.loads(result)
153 |         assert any(
154 |             ref["relative_path"] == ref_file for ref in refs
155 |         ), f"Expected to find reference to {symbol_name} in {ref_file}. refs={refs}"
156 | 
157 |     @pytest.mark.parametrize(
158 |         "serena_agent,name_path,substring_matching,expected_symbol_name,expected_kind,expected_file",
159 |         [
160 |             pytest.param(
161 |                 Language.PYTHON,
162 |                 "OuterClass/NestedClass",
163 |                 False,
164 |                 "NestedClass",
165 |                 "Class",
166 |                 os.path.join("test_repo", "nested.py"),
167 |                 id="exact_qualname_class",
168 |                 marks=pytest.mark.python,
169 |             ),
170 |             pytest.param(
171 |                 Language.PYTHON,
172 |                 "OuterClass/NestedClass/find_me",
173 |                 False,
174 |                 "find_me",
175 |                 "Method",
176 |                 os.path.join("test_repo", "nested.py"),
177 |                 id="exact_qualname_method",
178 |                 marks=pytest.mark.python,
179 |             ),
180 |             pytest.param(
181 |                 Language.PYTHON,
182 |                 "OuterClass/NestedCl",  # Substring for NestedClass
183 |                 True,
184 |                 "NestedClass",
185 |                 "Class",
186 |                 os.path.join("test_repo", "nested.py"),
187 |                 id="substring_qualname_class",
188 |                 marks=pytest.mark.python,
189 |             ),
190 |             pytest.param(
191 |                 Language.PYTHON,
192 |                 "OuterClass/NestedClass/find_m",  # Substring for find_me
193 |                 True,
194 |                 "find_me",
195 |                 "Method",
196 |                 os.path.join("test_repo", "nested.py"),
197 |                 id="substring_qualname_method",
198 |                 marks=pytest.mark.python,
199 |             ),
200 |             pytest.param(
201 |                 Language.PYTHON,
202 |                 "/OuterClass",  # Absolute path
203 |                 False,
204 |                 "OuterClass",
205 |                 "Class",
206 |                 os.path.join("test_repo", "nested.py"),
207 |                 id="absolute_qualname_class",
208 |                 marks=pytest.mark.python,
209 |             ),
210 |             pytest.param(
211 |                 Language.PYTHON,
212 |                 "/OuterClass/NestedClass/find_m",  # Absolute path with substring
213 |                 True,
214 |                 "find_me",
215 |                 "Method",
216 |                 os.path.join("test_repo", "nested.py"),
217 |                 id="absolute_substring_qualname_method",
218 |                 marks=pytest.mark.python,
219 |             ),
220 |         ],
221 |         indirect=["serena_agent"],
222 |     )
223 |     def test_find_symbol_name_path(
224 |         self,
225 |         serena_agent,
226 |         name_path: str,
227 |         substring_matching: bool,
228 |         expected_symbol_name: str,
229 |         expected_kind: str,
230 |         expected_file: str,
231 |     ):
232 |         agent = serena_agent
233 | 
234 |         find_symbol_tool = agent.get_tool(FindSymbolTool)
235 |         result = find_symbol_tool.apply_ex(
236 |             name_path=name_path,
237 |             depth=0,
238 |             relative_path=None,
239 |             include_body=False,
240 |             include_kinds=None,
241 |             exclude_kinds=None,
242 |             substring_matching=substring_matching,
243 |         )
244 | 
245 |         symbols = json.loads(result)
246 |         assert any(
247 |             expected_symbol_name == s["name_path"].split("/")[-1]
248 |             and expected_kind.lower() in s["kind"].lower()
249 |             and expected_file in s["relative_path"]
250 |             for s in symbols
251 |         ), f"Expected to find {name_path} ({expected_kind}) in {expected_file} for {agent._active_project.language.name}. Symbols: {symbols}"
252 | 
253 |     @pytest.mark.parametrize(
254 |         "serena_agent,name_path",
255 |         [
256 |             pytest.param(
257 |                 Language.PYTHON,
258 |                 "/NestedClass",  # Absolute path, NestedClass is not top-level
259 |                 id="absolute_path_non_top_level_no_match",
260 |                 marks=pytest.mark.python,
261 |             ),
262 |             pytest.param(
263 |                 Language.PYTHON,
264 |                 "/NoSuchParent/NestedClass",  # Absolute path with non-existent parent
265 |                 id="absolute_path_non_existent_parent_no_match",
266 |                 marks=pytest.mark.python,
267 |             ),
268 |         ],
269 |         indirect=["serena_agent"],
270 |     )
271 |     def test_find_symbol_name_path_no_match(
272 |         self,
273 |         serena_agent,
274 |         name_path: str,
275 |     ):
276 |         agent = serena_agent
277 | 
278 |         find_symbol_tool = agent.get_tool(FindSymbolTool)
279 |         result = find_symbol_tool.apply_ex(
280 |             name_path=name_path,
281 |             depth=0,
282 |             substring_matching=True,
283 |         )
284 | 
285 |         symbols = json.loads(result)
286 |         assert not symbols, f"Expected to find no symbols for {name_path}. Symbols found: {symbols}"
287 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/clojure/test_clojure_basic.py:
--------------------------------------------------------------------------------

```python
  1 | import pytest
  2 | 
  3 | from serena.project import Project
  4 | from solidlsp.ls import SolidLanguageServer
  5 | from solidlsp.ls_config import Language
  6 | from solidlsp.ls_types import UnifiedSymbolInformation
  7 | 
  8 | from . import CLI_FAIL, CORE_PATH, UTILS_PATH
  9 | 
 10 | 
 11 | @pytest.mark.clojure
 12 | @pytest.mark.skipif(CLI_FAIL, reason=f"Clojure CLI not available: {CLI_FAIL}")
 13 | class TestLanguageServerBasics:
 14 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
 15 |     def test_basic_definition(self, language_server: SolidLanguageServer):
 16 |         """
 17 |         Test finding definition of 'greet' function call in core.clj
 18 |         """
 19 |         result = language_server.request_definition(CORE_PATH, 20, 12)  # Position of 'greet' in (greet "World")
 20 | 
 21 |         assert isinstance(result, list)
 22 |         assert len(result) >= 1
 23 | 
 24 |         definition = result[0]
 25 |         assert definition["relativePath"] == CORE_PATH
 26 |         assert definition["range"]["start"]["line"] == 2, "Should find the definition of greet function at line 2"
 27 | 
 28 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
 29 |     def test_cross_file_references(self, language_server: SolidLanguageServer):
 30 |         """
 31 |         Test finding references to 'multiply' function from core.clj
 32 |         """
 33 |         result = language_server.request_references(CORE_PATH, 12, 6)
 34 | 
 35 |         assert isinstance(result, list) and len(result) >= 2, "Should find definition + usage in utils.clj"
 36 | 
 37 |         usage_found = any(
 38 |             item["relativePath"] == UTILS_PATH and item["range"]["start"]["line"] == 6  # multiply usage in calculate-area
 39 |             for item in result
 40 |         )
 41 |         assert usage_found, "Should find multiply usage in utils.clj"
 42 | 
 43 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
 44 |     def test_completions(self, language_server: SolidLanguageServer):
 45 |         with language_server.open_file(UTILS_PATH):
 46 |             # After "core/" in calculate-area
 47 |             result = language_server.request_completions(UTILS_PATH, 6, 8)
 48 | 
 49 |             assert isinstance(result, list) and len(result) > 0
 50 | 
 51 |             completion_texts = [item["completionText"] for item in result]
 52 |             assert any("multiply" in text for text in completion_texts), "Should find 'multiply' function in completions after 'core/'"
 53 | 
 54 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
 55 |     def test_document_symbols(self, language_server: SolidLanguageServer):
 56 |         symbols, _ = language_server.request_document_symbols(CORE_PATH)
 57 | 
 58 |         assert isinstance(symbols, list) and len(symbols) >= 4, "greet, add, multiply, -main functions"
 59 | 
 60 |         # Check that we find the expected function symbols
 61 |         symbol_names = [symbol["name"] for symbol in symbols]
 62 |         expected_functions = ["greet", "add", "multiply", "-main"]
 63 | 
 64 |         for func_name in expected_functions:
 65 |             assert func_name in symbol_names, f"Should find {func_name} function in symbols"
 66 | 
 67 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
 68 |     def test_hover(self, language_server: SolidLanguageServer):
 69 |         """Test hover on greet function"""
 70 |         result = language_server.request_hover(CORE_PATH, 2, 7)
 71 | 
 72 |         assert result is not None, "Hover should return information for greet function"
 73 |         assert "contents" in result
 74 |         # Should contain function signature or documentation
 75 |         contents = result["contents"]
 76 |         if isinstance(contents, str):
 77 |             assert "greet" in contents.lower()
 78 |         elif isinstance(contents, dict) and "value" in contents:
 79 |             assert "greet" in contents["value"].lower()
 80 |         else:
 81 |             assert False, f"Unexpected contents format: {type(contents)}"
 82 | 
 83 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
 84 |     def test_workspace_symbols(self, language_server: SolidLanguageServer):
 85 |         # Search for functions containing "add"
 86 |         result = language_server.request_workspace_symbol("add")
 87 | 
 88 |         assert isinstance(result, list) and len(result) > 0, "Should find at least one symbol containing 'add'"
 89 | 
 90 |         # Should find the 'add' function
 91 |         symbol_names = [symbol["name"] for symbol in result]
 92 |         assert any("add" in name.lower() for name in symbol_names), f"Should find 'add' function in symbols: {symbol_names}"
 93 | 
 94 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
 95 |     def test_namespace_functions(self, language_server: SolidLanguageServer):
 96 |         """Test definition lookup for core/greet usage in utils.clj"""
 97 |         # Position of 'greet' in core/greet call
 98 |         result = language_server.request_definition(UTILS_PATH, 11, 25)
 99 | 
100 |         assert isinstance(result, list)
101 |         assert len(result) >= 1
102 | 
103 |         definition = result[0]
104 |         assert definition["relativePath"] == CORE_PATH, "Should find the definition of greet in core.clj"
105 | 
106 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
107 |     def test_request_references_with_content(self, language_server: SolidLanguageServer):
108 |         """Test references to multiply function with content"""
109 |         references = language_server.request_references(CORE_PATH, 12, 6)
110 |         result = [
111 |             language_server.retrieve_content_around_line(ref1["relativePath"], ref1["range"]["start"]["line"], 3, 0) for ref1 in references
112 |         ]
113 | 
114 |         assert result is not None, "Should find references with content"
115 |         assert isinstance(result, list)
116 |         assert len(result) >= 2, "Should find definition + usage in utils.clj"
117 | 
118 |         for ref in result:
119 |             assert ref.source_file_path is not None, "Each reference should have a source file path"
120 |             content_str = ref.to_display_string()
121 |             assert len(content_str) > 0, "Content should not be empty"
122 | 
123 |         # Verify we find the reference in utils.clj with context
124 |         utils_refs = [ref for ref in result if ref.source_file_path and "utils.clj" in ref.source_file_path]
125 |         assert len(utils_refs) > 0, "Should find reference in utils.clj"
126 | 
127 |         # The context should contain the calculate-area function
128 |         utils_content = utils_refs[0].to_display_string()
129 |         assert "calculate-area" in utils_content
130 | 
131 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
132 |     def test_request_full_symbol_tree(self, language_server: SolidLanguageServer):
133 |         """Test retrieving the full symbol tree for project overview
134 |         We just check that we find some expected symbols.
135 |         """
136 |         result = language_server.request_full_symbol_tree()
137 | 
138 |         assert result is not None, "Should return symbol tree"
139 |         assert isinstance(result, list), "Symbol tree should be a list"
140 |         assert len(result) > 0, "Should find symbols in the project"
141 | 
142 |         def traverse_symbols(symbols, indent=0):
143 |             """Recursively traverse symbols to print their structure"""
144 |             info = []
145 |             for s in symbols:
146 |                 name = getattr(s, "name", "NO_NAME")
147 |                 kind = getattr(s, "kind", "NO_KIND")
148 |                 info.append(f"{' ' * indent}Symbol: {name}, Kind: {kind}")
149 |                 if hasattr(s, "children") and s.children:
150 |                     info.append(" " * indent + "Children:")
151 |                     info.extend(traverse_symbols(s.children, indent + 2))
152 |             return info
153 | 
154 |         def list_all_symbols(symbols: list[UnifiedSymbolInformation]):
155 |             found = []
156 |             for symbol in symbols:
157 |                 found.append(symbol["name"])
158 |                 found.extend(list_all_symbols(symbol["children"]))
159 |             return found
160 | 
161 |         all_symbol_names = list_all_symbols(result)
162 | 
163 |         expected_symbols = ["greet", "add", "multiply", "-main", "calculate-area", "format-greeting", "sum-list"]
164 |         found_expected = [name for name in expected_symbols if any(name in symbol_name for symbol_name in all_symbol_names)]
165 | 
166 |         if len(found_expected) < 7:
167 |             pytest.fail(
168 |                 f"Expected to find at least 3 symbols from {expected_symbols}, but found: {found_expected}.\n"
169 |                 f"All symbol names: {all_symbol_names}\n"
170 |                 f"Symbol tree structure:\n{traverse_symbols(result)}"
171 |             )
172 | 
173 |     @pytest.mark.parametrize("language_server", [Language.CLOJURE], indirect=True)
174 |     def test_request_referencing_symbols(self, language_server: SolidLanguageServer):
175 |         """Test finding symbols that reference a given symbol
176 |         Finds references to the 'multiply' function.
177 |         """
178 |         result = language_server.request_referencing_symbols(CORE_PATH, 12, 6)
179 |         assert isinstance(result, list) and len(result) > 0, "Should find at least one referencing symbol"
180 |         found_relevant_references = False
181 |         for ref in result:
182 |             if hasattr(ref, "symbol") and "calculate-area" in ref.symbol["name"]:
183 |                 found_relevant_references = True
184 |                 break
185 | 
186 |         assert found_relevant_references, f"Should have found calculate-area referencing multiply, but got: {result}"
187 | 
188 | 
189 | class TestProjectBasics:
190 |     @pytest.mark.parametrize("project", [Language.CLOJURE], indirect=True)
191 |     def test_retrieve_content_around_line(self, project: Project):
192 |         """Test retrieving content around specific lines"""
193 |         # Test retrieving content around the greet function definition (line 2)
194 |         result = project.retrieve_content_around_line(CORE_PATH, 2, 2)
195 | 
196 |         assert result is not None, "Should retrieve content around line 2"
197 |         content_str = result.to_display_string()
198 |         assert "greet" in content_str, "Should contain the greet function definition"
199 |         assert "defn" in content_str, "Should contain defn keyword"
200 | 
201 |         # Test retrieving content around multiply function (around line 13)
202 |         result = project.retrieve_content_around_line(CORE_PATH, 13, 1)
203 | 
204 |         assert result is not None, "Should retrieve content around line 13"
205 |         content_str = result.to_display_string()
206 |         assert "multiply" in content_str, "Should contain multiply function"
207 | 
208 |     @pytest.mark.parametrize("project", [Language.CLOJURE], indirect=True)
209 |     def test_search_files_for_pattern(self, project: Project) -> None:
210 |         result = project.search_source_files_for_pattern("defn.*greet")
211 | 
212 |         assert result is not None, "Pattern search should return results"
213 |         assert len(result) > 0, "Should find at least one match for 'defn.*greet'"
214 | 
215 |         core_matches = [match for match in result if match.source_file_path and "core.clj" in match.source_file_path]
216 |         assert len(core_matches) > 0, "Should find greet function in core.clj"
217 | 
218 |         result = project.search_source_files_for_pattern(":require")
219 | 
220 |         assert result is not None, "Should find require statements"
221 |         utils_matches = [match for match in result if match.source_file_path and "utils.clj" in match.source_file_path]
222 |         assert len(utils_matches) > 0, "Should find require statement in utils.clj"
223 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/ls_types.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Defines wrapper objects around the types returned by LSP to ensure decoupling between LSP versions and multilspy
  3 | """
  4 | 
  5 | from __future__ import annotations
  6 | 
  7 | from enum import Enum, IntEnum
  8 | from typing import NotRequired, Union
  9 | 
 10 | from typing_extensions import TypedDict
 11 | 
 12 | URI = str
 13 | DocumentUri = str
 14 | Uint = int
 15 | RegExp = str
 16 | 
 17 | 
 18 | class Position(TypedDict):
 19 |     r"""Position in a text document expressed as zero-based line and character
 20 |     offset. Prior to 3.17 the offsets were always based on a UTF-16 string
 21 |     representation. So a string of the form `a𐐀b` the character offset of the
 22 |     character `a` is 0, the character offset of `𐐀` is 1 and the character
 23 |     offset of b is 3 since `𐐀` is represented using two code units in UTF-16.
 24 |     Since 3.17 clients and servers can agree on a different string encoding
 25 |     representation (e.g. UTF-8). The client announces it's supported encoding
 26 |     via the client capability [`general.positionEncodings`](#clientCapabilities).
 27 |     The value is an array of position encodings the client supports, with
 28 |     decreasing preference (e.g. the encoding at index `0` is the most preferred
 29 |     one). To stay backwards compatible the only mandatory encoding is UTF-16
 30 |     represented via the string `utf-16`. The server can pick one of the
 31 |     encodings offered by the client and signals that encoding back to the
 32 |     client via the initialize result's property
 33 |     [`capabilities.positionEncoding`](#serverCapabilities). If the string value
 34 |     `utf-16` is missing from the client's capability `general.positionEncodings`
 35 |     servers can safely assume that the client supports UTF-16. If the server
 36 |     omits the position encoding in its initialize result the encoding defaults
 37 |     to the string value `utf-16`. Implementation considerations: since the
 38 |     conversion from one encoding into another requires the content of the
 39 |     file / line the conversion is best done where the file is read which is
 40 |     usually on the server side.
 41 | 
 42 |     Positions are line end character agnostic. So you can not specify a position
 43 |     that denotes `\r|\n` or `\n|` where `|` represents the character offset.
 44 | 
 45 |     @since 3.17.0 - support for negotiated position encoding.
 46 |     """
 47 | 
 48 |     line: Uint
 49 |     """ Line position in a document (zero-based).
 50 | 
 51 |     If a line number is greater than the number of lines in a document, it defaults back to the number of lines in the document.
 52 |     If a line number is negative, it defaults to 0. """
 53 |     character: Uint
 54 |     """ Character offset on a line in a document (zero-based).
 55 | 
 56 |     The meaning of this offset is determined by the negotiated
 57 |     `PositionEncodingKind`.
 58 | 
 59 |     If the character value is greater than the line length it defaults back to the
 60 |     line length. """
 61 | 
 62 | 
 63 | class Range(TypedDict):
 64 |     """A range in a text document expressed as (zero-based) start and end positions.
 65 | 
 66 |     If you want to specify a range that contains a line including the line ending
 67 |     character(s) then use an end position denoting the start of the next line.
 68 |     For example:
 69 |     ```ts
 70 |     {
 71 |         start: { line: 5, character: 23 }
 72 |         end : { line 6, character : 0 }
 73 |     }
 74 |     ```
 75 |     """
 76 | 
 77 |     start: Position
 78 |     """ The range's start position. """
 79 |     end: Position
 80 |     """ The range's end position. """
 81 | 
 82 | 
 83 | class Location(TypedDict):
 84 |     """Represents a location inside a resource, such as a line
 85 |     inside a text file.
 86 |     """
 87 | 
 88 |     uri: DocumentUri
 89 |     range: Range
 90 |     absolutePath: str
 91 |     relativePath: str | None
 92 | 
 93 | 
 94 | class CompletionItemKind(IntEnum):
 95 |     """The kind of a completion entry."""
 96 | 
 97 |     Text = 1
 98 |     Method = 2
 99 |     Function = 3
100 |     Constructor = 4
101 |     Field = 5
102 |     Variable = 6
103 |     Class = 7
104 |     Interface = 8
105 |     Module = 9
106 |     Property = 10
107 |     Unit = 11
108 |     Value = 12
109 |     Enum = 13
110 |     Keyword = 14
111 |     Snippet = 15
112 |     Color = 16
113 |     File = 17
114 |     Reference = 18
115 |     Folder = 19
116 |     EnumMember = 20
117 |     Constant = 21
118 |     Struct = 22
119 |     Event = 23
120 |     Operator = 24
121 |     TypeParameter = 25
122 | 
123 | 
124 | class CompletionItem(TypedDict):
125 |     """A completion item represents a text snippet that is
126 |     proposed to complete text that is being typed.
127 |     """
128 | 
129 |     completionText: str
130 |     """ The completionText of this completion item.
131 | 
132 |     The completionText property is also by default the text that
133 |     is inserted when selecting this completion."""
134 | 
135 |     kind: CompletionItemKind
136 |     """ The kind of this completion item. Based of the kind
137 |     an icon is chosen by the editor. """
138 | 
139 |     detail: NotRequired[str]
140 |     """ A human-readable string with additional information
141 |     about this item, like type or symbol information. """
142 | 
143 | 
144 | class SymbolKind(IntEnum):
145 |     """A symbol kind."""
146 | 
147 |     File = 1
148 |     Module = 2
149 |     Namespace = 3
150 |     Package = 4
151 |     Class = 5
152 |     Method = 6
153 |     Property = 7
154 |     Field = 8
155 |     Constructor = 9
156 |     Enum = 10
157 |     Interface = 11
158 |     Function = 12
159 |     Variable = 13
160 |     Constant = 14
161 |     String = 15
162 |     Number = 16
163 |     Boolean = 17
164 |     Array = 18
165 |     Object = 19
166 |     Key = 20
167 |     Null = 21
168 |     EnumMember = 22
169 |     Struct = 23
170 |     Event = 24
171 |     Operator = 25
172 |     TypeParameter = 26
173 | 
174 | 
175 | class SymbolTag(IntEnum):
176 |     """Symbol tags are extra annotations that tweak the rendering of a symbol.
177 | 
178 |     @since 3.16
179 |     """
180 | 
181 |     Deprecated = 1
182 |     """ Render a symbol as obsolete, usually using a strike-out. """
183 | 
184 | 
185 | class UnifiedSymbolInformation(TypedDict):
186 |     """Represents information about programming constructs like variables, classes,
187 |     interfaces etc.
188 |     """
189 | 
190 |     deprecated: NotRequired[bool]
191 |     """ Indicates if this symbol is deprecated.
192 | 
193 |     @deprecated Use tags instead """
194 |     location: NotRequired[Location]
195 |     """ The location of this symbol. The location's range is used by a tool
196 |     to reveal the location in the editor. If the symbol is selected in the
197 |     tool the range's start information is used to position the cursor. So
198 |     the range usually spans more than the actual symbol's name and does
199 |     normally include things like visibility modifiers.
200 | 
201 |     The range doesn't have to denote a node range in the sense of an abstract
202 |     syntax tree. It can therefore not be used to re-construct a hierarchy of
203 |     the symbols. """
204 |     name: str
205 |     """ The name of this symbol. """
206 |     kind: SymbolKind
207 |     """ The kind of this symbol. """
208 |     tags: NotRequired[list[SymbolTag]]
209 |     """ Tags for this symbol.
210 | 
211 |     @since 3.16.0 """
212 |     containerName: NotRequired[str]
213 |     """ The name of the symbol containing this symbol. This information is for
214 |     user interface purposes (e.g. to render a qualifier in the user interface
215 |     if necessary). It can't be used to re-infer a hierarchy for the document
216 |     symbols. 
217 |     
218 |     Note: within Serena, the parent attribute was added and should be used instead. 
219 |     Most LS don't provide containerName.
220 |     """
221 | 
222 |     detail: NotRequired[str]
223 |     """ More detail for this symbol, e.g the signature of a function. """
224 | 
225 |     range: NotRequired[Range]
226 |     """ The range enclosing this symbol not including leading/trailing whitespace but everything else
227 |     like comments. This information is typically used to determine if the clients cursor is
228 |     inside the symbol to reveal in the symbol in the UI. """
229 |     selectionRange: NotRequired[Range]
230 |     """ The range that should be selected and revealed when this symbol is being picked, e.g the name of a function.
231 |     Must be contained by the `range`. """
232 | 
233 |     body: NotRequired[str]
234 |     """ The body of the symbol. """
235 | 
236 |     children: list[UnifiedSymbolInformation]
237 |     """ The children of the symbol. 
238 |     Added to be compatible with `lsp_types.DocumentSymbol`, 
239 |     since it is sometimes useful to have the children of the symbol as a user-facing feature."""
240 | 
241 |     parent: NotRequired[UnifiedSymbolInformation | None]
242 |     """The parent of the symbol, if there is any. Added with Serena, not part of the LSP.
243 |     All symbols except the root packages will have a parent.
244 |     """
245 | 
246 | 
247 | class MarkupKind(Enum):
248 |     """Describes the content type that a client supports in various
249 |     result literals like `Hover`, `ParameterInfo` or `CompletionItem`.
250 | 
251 |     Please note that `MarkupKinds` must not start with a `$`. This kinds
252 |     are reserved for internal usage.
253 |     """
254 | 
255 |     PlainText = "plaintext"
256 |     """ Plain text is supported as a content format """
257 |     Markdown = "markdown"
258 |     """ Markdown is supported as a content format """
259 | 
260 | 
261 | class __MarkedString_Type_1(TypedDict):
262 |     language: str
263 |     value: str
264 | 
265 | 
266 | MarkedString = Union[str, "__MarkedString_Type_1"]
267 | """ MarkedString can be used to render human readable text. It is either a markdown string
268 | or a code-block that provides a language and a code snippet. The language identifier
269 | is semantically equal to the optional language identifier in fenced code blocks in GitHub
270 | issues. See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting
271 | 
272 | The pair of a language and a value is an equivalent to markdown:
273 | ```${language}
274 | ${value}
275 | ```
276 | 
277 | Note that markdown strings will be sanitized - that means html will be escaped.
278 | @deprecated use MarkupContent instead. """
279 | 
280 | 
281 | class MarkupContent(TypedDict):
282 |     r"""A `MarkupContent` literal represents a string value which content is interpreted base on its
283 |     kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds.
284 | 
285 |     If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues.
286 |     See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting
287 | 
288 |     Here is an example how such a string can be constructed using JavaScript / TypeScript:
289 |     ```ts
290 |     let markdown: MarkdownContent = {
291 |      kind: MarkupKind.Markdown,
292 |      value: [
293 |        '# Header',
294 |        'Some text',
295 |        '```typescript',
296 |        'someCode();',
297 |        '```'
298 |      ].join('\n')
299 |     };
300 |     ```
301 | 
302 |     *Please Note* that clients might sanitize the return markdown. A client could decide to
303 |     remove HTML from the markdown to avoid script execution.
304 |     """
305 | 
306 |     kind: MarkupKind
307 |     """ The type of the Markup """
308 |     value: str
309 |     """ The content itself """
310 | 
311 | 
312 | class Hover(TypedDict):
313 |     """The result of a hover request."""
314 | 
315 |     contents: MarkupContent | MarkedString | list[MarkedString]
316 |     """ The hover's content """
317 |     range: NotRequired[Range]
318 |     """ An optional range inside the text document that is used to
319 |     visualize the hover, e.g. by changing the background color. """
320 | 
321 | 
322 | class DiagnosticsSeverity(IntEnum):
323 |     ERROR = 1
324 |     WARNING = 2
325 |     INFORMATION = 3
326 |     HINT = 4
327 | 
328 | 
329 | class Diagnostic(TypedDict):
330 |     """Diagnostic information for a text document."""
331 | 
332 |     uri: DocumentUri
333 |     """ The URI of the text document to which the diagnostics apply. """
334 |     range: Range
335 |     """ The range of the text document to which the diagnostics apply. """
336 |     severity: NotRequired[DiagnosticsSeverity]
337 |     """ The severity of the diagnostic. """
338 |     message: str
339 |     """ The diagnostic message. """
340 |     code: str
341 |     """ The code of the diagnostic. """
342 |     source: NotRequired[str]
343 |     """ The source of the diagnostic, e.g. the name of the tool that produced it. """
344 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/ls_config.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Configuration objects for language servers
  3 | """
  4 | 
  5 | import fnmatch
  6 | from collections.abc import Iterable
  7 | from dataclasses import dataclass, field
  8 | from enum import Enum
  9 | from typing import TYPE_CHECKING, Self
 10 | 
 11 | if TYPE_CHECKING:
 12 |     from solidlsp import SolidLanguageServer
 13 | 
 14 | 
 15 | class FilenameMatcher:
 16 |     def __init__(self, *patterns: str) -> None:
 17 |         """
 18 |         :param patterns: fnmatch-compatible patterns
 19 |         """
 20 |         self.patterns = patterns
 21 | 
 22 |     def is_relevant_filename(self, fn: str) -> bool:
 23 |         for pattern in self.patterns:
 24 |             if fnmatch.fnmatch(fn, pattern):
 25 |                 return True
 26 |         return False
 27 | 
 28 | 
 29 | class Language(str, Enum):
 30 |     """
 31 |     Possible languages with Multilspy.
 32 |     """
 33 | 
 34 |     CSHARP = "csharp"
 35 |     PYTHON = "python"
 36 |     RUST = "rust"
 37 |     JAVA = "java"
 38 |     KOTLIN = "kotlin"
 39 |     TYPESCRIPT = "typescript"
 40 |     GO = "go"
 41 |     RUBY = "ruby"
 42 |     DART = "dart"
 43 |     CPP = "cpp"
 44 |     PHP = "php"
 45 |     R = "r"
 46 |     PERL = "perl"
 47 |     CLOJURE = "clojure"
 48 |     ELIXIR = "elixir"
 49 |     ELM = "elm"
 50 |     TERRAFORM = "terraform"
 51 |     SWIFT = "swift"
 52 |     BASH = "bash"
 53 |     ZIG = "zig"
 54 |     LUA = "lua"
 55 |     NIX = "nix"
 56 |     ERLANG = "erlang"
 57 |     AL = "al"
 58 |     # Experimental or deprecated Language Servers
 59 |     TYPESCRIPT_VTS = "typescript_vts"
 60 |     """Use the typescript language server through the natively bundled vscode extension via https://github.com/yioneko/vtsls"""
 61 |     PYTHON_JEDI = "python_jedi"
 62 |     """Jedi language server for Python (instead of pyright, which is the default)"""
 63 |     CSHARP_OMNISHARP = "csharp_omnisharp"
 64 |     """OmniSharp language server for C# (instead of the default csharp-ls by microsoft).
 65 |     Currently has problems with finding references, and generally seems less stable and performant.
 66 |     """
 67 |     RUBY_SOLARGRAPH = "ruby_solargraph"
 68 |     """Solargraph language server for Ruby (legacy, experimental).
 69 |     Use Language.RUBY (ruby-lsp) for better performance and modern LSP features.
 70 |     """
 71 |     MARKDOWN = "markdown"
 72 |     """Marksman language server for Markdown (experimental).
 73 |     Must be explicitly specified as the main language, not auto-detected.
 74 |     This is an edge case primarily useful when working on documentation-heavy projects.
 75 |     """
 76 | 
 77 |     @classmethod
 78 |     def iter_all(cls, include_experimental: bool = False) -> Iterable[Self]:
 79 |         for lang in cls:
 80 |             if include_experimental or not lang.is_experimental():
 81 |                 yield lang
 82 | 
 83 |     def is_experimental(self) -> bool:
 84 |         """
 85 |         Check if the language server is experimental or deprecated.
 86 |         """
 87 |         return self in {self.TYPESCRIPT_VTS, self.PYTHON_JEDI, self.CSHARP_OMNISHARP, self.RUBY_SOLARGRAPH, self.MARKDOWN}
 88 | 
 89 |     def __str__(self) -> str:
 90 |         return self.value
 91 | 
 92 |     def get_source_fn_matcher(self) -> FilenameMatcher:
 93 |         match self:
 94 |             case self.PYTHON | self.PYTHON_JEDI:
 95 |                 return FilenameMatcher("*.py", "*.pyi")
 96 |             case self.JAVA:
 97 |                 return FilenameMatcher("*.java")
 98 |             case self.TYPESCRIPT | self.TYPESCRIPT_VTS:
 99 |                 # see https://github.com/oraios/serena/issues/204
100 |                 path_patterns = []
101 |                 for prefix in ["c", "m", ""]:
102 |                     for postfix in ["x", ""]:
103 |                         for base_pattern in ["ts", "js"]:
104 |                             path_patterns.append(f"*.{prefix}{base_pattern}{postfix}")
105 |                 return FilenameMatcher(*path_patterns)
106 |             case self.CSHARP | self.CSHARP_OMNISHARP:
107 |                 return FilenameMatcher("*.cs")
108 |             case self.RUST:
109 |                 return FilenameMatcher("*.rs")
110 |             case self.GO:
111 |                 return FilenameMatcher("*.go")
112 |             case self.RUBY:
113 |                 return FilenameMatcher("*.rb", "*.erb")
114 |             case self.RUBY_SOLARGRAPH:
115 |                 return FilenameMatcher("*.rb")
116 |             case self.CPP:
117 |                 return FilenameMatcher("*.cpp", "*.h", "*.hpp", "*.c", "*.hxx", "*.cc", "*.cxx")
118 |             case self.KOTLIN:
119 |                 return FilenameMatcher("*.kt", "*.kts")
120 |             case self.DART:
121 |                 return FilenameMatcher("*.dart")
122 |             case self.PHP:
123 |                 return FilenameMatcher("*.php")
124 |             case self.R:
125 |                 return FilenameMatcher("*.R", "*.r", "*.Rmd", "*.Rnw")
126 |             case self.PERL:
127 |                 return FilenameMatcher("*.pl", "*.pm", "*.t")
128 |             case self.CLOJURE:
129 |                 return FilenameMatcher("*.clj", "*.cljs", "*.cljc", "*.edn")  # codespell:ignore edn
130 |             case self.ELIXIR:
131 |                 return FilenameMatcher("*.ex", "*.exs")
132 |             case self.ELM:
133 |                 return FilenameMatcher("*.elm")
134 |             case self.TERRAFORM:
135 |                 return FilenameMatcher("*.tf", "*.tfvars", "*.tfstate")
136 |             case self.SWIFT:
137 |                 return FilenameMatcher("*.swift")
138 |             case self.BASH:
139 |                 return FilenameMatcher("*.sh", "*.bash")
140 |             case self.ZIG:
141 |                 return FilenameMatcher("*.zig", "*.zon")
142 |             case self.LUA:
143 |                 return FilenameMatcher("*.lua")
144 |             case self.NIX:
145 |                 return FilenameMatcher("*.nix")
146 |             case self.ERLANG:
147 |                 return FilenameMatcher("*.erl", "*.hrl", "*.escript", "*.config", "*.app", "*.app.src")
148 |             case self.AL:
149 |                 return FilenameMatcher("*.al", "*.dal")
150 |             case self.MARKDOWN:
151 |                 return FilenameMatcher("*.md", "*.markdown")
152 |             case _:
153 |                 raise ValueError(f"Unhandled language: {self}")
154 | 
155 |     def get_ls_class(self) -> type["SolidLanguageServer"]:
156 |         match self:
157 |             case self.PYTHON:
158 |                 from solidlsp.language_servers.pyright_server import PyrightServer
159 | 
160 |                 return PyrightServer
161 |             case self.PYTHON_JEDI:
162 |                 from solidlsp.language_servers.jedi_server import JediServer
163 | 
164 |                 return JediServer
165 |             case self.JAVA:
166 |                 from solidlsp.language_servers.eclipse_jdtls import EclipseJDTLS
167 | 
168 |                 return EclipseJDTLS
169 |             case self.KOTLIN:
170 |                 from solidlsp.language_servers.kotlin_language_server import KotlinLanguageServer
171 | 
172 |                 return KotlinLanguageServer
173 |             case self.RUST:
174 |                 from solidlsp.language_servers.rust_analyzer import RustAnalyzer
175 | 
176 |                 return RustAnalyzer
177 |             case self.CSHARP:
178 |                 from solidlsp.language_servers.csharp_language_server import CSharpLanguageServer
179 | 
180 |                 return CSharpLanguageServer
181 |             case self.CSHARP_OMNISHARP:
182 |                 from solidlsp.language_servers.omnisharp import OmniSharp
183 | 
184 |                 return OmniSharp
185 |             case self.TYPESCRIPT:
186 |                 from solidlsp.language_servers.typescript_language_server import TypeScriptLanguageServer
187 | 
188 |                 return TypeScriptLanguageServer
189 |             case self.TYPESCRIPT_VTS:
190 |                 from solidlsp.language_servers.vts_language_server import VtsLanguageServer
191 | 
192 |                 return VtsLanguageServer
193 |             case self.GO:
194 |                 from solidlsp.language_servers.gopls import Gopls
195 | 
196 |                 return Gopls
197 |             case self.RUBY:
198 |                 from solidlsp.language_servers.ruby_lsp import RubyLsp
199 | 
200 |                 return RubyLsp
201 |             case self.RUBY_SOLARGRAPH:
202 |                 from solidlsp.language_servers.solargraph import Solargraph
203 | 
204 |                 return Solargraph
205 |             case self.DART:
206 |                 from solidlsp.language_servers.dart_language_server import DartLanguageServer
207 | 
208 |                 return DartLanguageServer
209 |             case self.CPP:
210 |                 from solidlsp.language_servers.clangd_language_server import ClangdLanguageServer
211 | 
212 |                 return ClangdLanguageServer
213 |             case self.PHP:
214 |                 from solidlsp.language_servers.intelephense import Intelephense
215 | 
216 |                 return Intelephense
217 |             case self.PERL:
218 |                 from solidlsp.language_servers.perl_language_server import PerlLanguageServer
219 | 
220 |                 return PerlLanguageServer
221 |             case self.CLOJURE:
222 |                 from solidlsp.language_servers.clojure_lsp import ClojureLSP
223 | 
224 |                 return ClojureLSP
225 |             case self.ELIXIR:
226 |                 from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
227 | 
228 |                 return ElixirTools
229 |             case self.ELM:
230 |                 from solidlsp.language_servers.elm_language_server import ElmLanguageServer
231 | 
232 |                 return ElmLanguageServer
233 |             case self.TERRAFORM:
234 |                 from solidlsp.language_servers.terraform_ls import TerraformLS
235 | 
236 |                 return TerraformLS
237 |             case self.SWIFT:
238 |                 from solidlsp.language_servers.sourcekit_lsp import SourceKitLSP
239 | 
240 |                 return SourceKitLSP
241 |             case self.BASH:
242 |                 from solidlsp.language_servers.bash_language_server import BashLanguageServer
243 | 
244 |                 return BashLanguageServer
245 |             case self.ZIG:
246 |                 from solidlsp.language_servers.zls import ZigLanguageServer
247 | 
248 |                 return ZigLanguageServer
249 |             case self.NIX:
250 |                 from solidlsp.language_servers.nixd_ls import NixLanguageServer
251 | 
252 |                 return NixLanguageServer
253 |             case self.LUA:
254 |                 from solidlsp.language_servers.lua_ls import LuaLanguageServer
255 | 
256 |                 return LuaLanguageServer
257 |             case self.ERLANG:
258 |                 from solidlsp.language_servers.erlang_language_server import ErlangLanguageServer
259 | 
260 |                 return ErlangLanguageServer
261 |             case self.AL:
262 |                 from solidlsp.language_servers.al_language_server import ALLanguageServer
263 | 
264 |                 return ALLanguageServer
265 |             case self.MARKDOWN:
266 |                 from solidlsp.language_servers.marksman import Marksman
267 | 
268 |                 return Marksman
269 |             case self.R:
270 |                 from solidlsp.language_servers.r_language_server import RLanguageServer
271 | 
272 |                 return RLanguageServer
273 |             case _:
274 |                 raise ValueError(f"Unhandled language: {self}")
275 | 
276 |     @classmethod
277 |     def from_ls_class(cls, ls_class: type["SolidLanguageServer"]) -> Self:
278 |         """
279 |         Get the Language enum value from a SolidLanguageServer class.
280 | 
281 |         :param ls_class: The SolidLanguageServer class to find the corresponding Language for
282 |         :return: The Language enum value
283 |         :raises ValueError: If the language server class is not supported
284 |         """
285 |         for enum_instance in cls:
286 |             if enum_instance.get_ls_class() == ls_class:
287 |                 return enum_instance
288 |         raise ValueError(f"Unhandled language server class: {ls_class}")
289 | 
290 | 
291 | @dataclass
292 | class LanguageServerConfig:
293 |     """
294 |     Configuration parameters
295 |     """
296 | 
297 |     code_language: Language
298 |     trace_lsp_communication: bool = False
299 |     start_independent_lsp_process: bool = True
300 |     ignored_paths: list[str] = field(default_factory=list)
301 |     """Paths, dirs or glob-like patterns. The matching will follow the same logic as for .gitignore entries"""
302 | 
303 |     @classmethod
304 |     def from_dict(cls, env: dict):
305 |         """
306 |         Create a MultilspyConfig instance from a dictionary
307 |         """
308 |         import inspect
309 | 
310 |         return cls(**{k: v for k, v in env.items() if k in inspect.signature(cls).parameters})
311 | 
```

--------------------------------------------------------------------------------
/src/solidlsp/language_servers/lua_ls.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Provides Lua specific instantiation of the LanguageServer class using lua-language-server.
  3 | """
  4 | 
  5 | import logging
  6 | import os
  7 | import pathlib
  8 | import platform
  9 | import shutil
 10 | import tarfile
 11 | import threading
 12 | import zipfile
 13 | from pathlib import Path
 14 | 
 15 | import requests
 16 | from overrides import override
 17 | 
 18 | from solidlsp.ls import SolidLanguageServer
 19 | from solidlsp.ls_config import LanguageServerConfig
 20 | from solidlsp.ls_logger import LanguageServerLogger
 21 | from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams
 22 | from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo
 23 | from solidlsp.settings import SolidLSPSettings
 24 | 
 25 | 
 26 | class LuaLanguageServer(SolidLanguageServer):
 27 |     """
 28 |     Provides Lua specific instantiation of the LanguageServer class using lua-language-server.
 29 |     """
 30 | 
 31 |     @override
 32 |     def is_ignored_dirname(self, dirname: str) -> bool:
 33 |         # For Lua projects, we should ignore:
 34 |         # - .luarocks: package manager cache
 35 |         # - lua_modules: local dependencies
 36 |         # - node_modules: if the project has JavaScript components
 37 |         return super().is_ignored_dirname(dirname) or dirname in [".luarocks", "lua_modules", "node_modules", "build", "dist", ".cache"]
 38 | 
 39 |     @staticmethod
 40 |     def _get_lua_ls_path():
 41 |         """Get the path to lua-language-server executable."""
 42 |         # First check if it's in PATH
 43 |         lua_ls = shutil.which("lua-language-server")
 44 |         if lua_ls:
 45 |             return lua_ls
 46 | 
 47 |         # Check common installation locations
 48 |         home = Path.home()
 49 |         possible_paths = [
 50 |             home / ".local" / "bin" / "lua-language-server",
 51 |             home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server",
 52 |             Path("/usr/local/bin/lua-language-server"),
 53 |             Path("/opt/lua-language-server/bin/lua-language-server"),
 54 |         ]
 55 | 
 56 |         # Add Windows-specific paths
 57 |         if platform.system() == "Windows":
 58 |             possible_paths.extend(
 59 |                 [
 60 |                     home / "AppData" / "Local" / "lua-language-server" / "bin" / "lua-language-server.exe",
 61 |                     home / ".serena" / "language_servers" / "lua" / "bin" / "lua-language-server.exe",
 62 |                 ]
 63 |             )
 64 | 
 65 |         for path in possible_paths:
 66 |             if path.exists():
 67 |                 return str(path)
 68 | 
 69 |         return None
 70 | 
 71 |     @staticmethod
 72 |     def _download_lua_ls():
 73 |         """Download and install lua-language-server if not present."""
 74 |         system = platform.system()
 75 |         machine = platform.machine().lower()
 76 |         lua_ls_version = "3.15.0"
 77 | 
 78 |         # Map platform and architecture to download URL
 79 |         if system == "Linux":
 80 |             if machine in ["x86_64", "amd64"]:
 81 |                 download_name = f"lua-language-server-{lua_ls_version}-linux-x64.tar.gz"
 82 |             elif machine in ["aarch64", "arm64"]:
 83 |                 download_name = f"lua-language-server-{lua_ls_version}-linux-arm64.tar.gz"
 84 |             else:
 85 |                 raise RuntimeError(f"Unsupported Linux architecture: {machine}")
 86 |         elif system == "Darwin":
 87 |             if machine in ["x86_64", "amd64"]:
 88 |                 download_name = f"lua-language-server-{lua_ls_version}-darwin-x64.tar.gz"
 89 |             elif machine in ["arm64", "aarch64"]:
 90 |                 download_name = f"lua-language-server-{lua_ls_version}-darwin-arm64.tar.gz"
 91 |             else:
 92 |                 raise RuntimeError(f"Unsupported macOS architecture: {machine}")
 93 |         elif system == "Windows":
 94 |             if machine in ["amd64", "x86_64"]:
 95 |                 download_name = f"lua-language-server-{lua_ls_version}-win32-x64.zip"
 96 |             else:
 97 |                 raise RuntimeError(f"Unsupported Windows architecture: {machine}")
 98 |         else:
 99 |             raise RuntimeError(f"Unsupported operating system: {system}")
100 | 
101 |         download_url = f"https://github.com/LuaLS/lua-language-server/releases/download/{lua_ls_version}/{download_name}"
102 | 
103 |         # Create installation directory
104 |         install_dir = Path.home() / ".serena" / "language_servers" / "lua"
105 |         install_dir.mkdir(parents=True, exist_ok=True)
106 | 
107 |         # Download the file
108 |         print(f"Downloading lua-language-server from {download_url}...")
109 |         response = requests.get(download_url, stream=True)
110 |         response.raise_for_status()
111 | 
112 |         # Save and extract
113 |         download_path = install_dir / download_name
114 |         with open(download_path, "wb") as f:
115 |             for chunk in response.iter_content(chunk_size=8192):
116 |                 f.write(chunk)
117 | 
118 |         print(f"Extracting lua-language-server to {install_dir}...")
119 |         if download_name.endswith(".tar.gz"):
120 |             with tarfile.open(download_path, "r:gz") as tar:
121 |                 tar.extractall(install_dir)
122 |         elif download_name.endswith(".zip"):
123 |             with zipfile.ZipFile(download_path, "r") as zip_ref:
124 |                 zip_ref.extractall(install_dir)
125 | 
126 |         # Clean up download file
127 |         download_path.unlink()
128 | 
129 |         # Make executable on Unix systems
130 |         if system != "Windows":
131 |             lua_ls_path = install_dir / "bin" / "lua-language-server"
132 |             if lua_ls_path.exists():
133 |                 lua_ls_path.chmod(0o755)
134 |                 return str(lua_ls_path)
135 |         else:
136 |             lua_ls_path = install_dir / "bin" / "lua-language-server.exe"
137 |             if lua_ls_path.exists():
138 |                 return str(lua_ls_path)
139 | 
140 |         raise RuntimeError("Failed to find lua-language-server executable after extraction")
141 | 
142 |     @staticmethod
143 |     def _setup_runtime_dependency():
144 |         """
145 |         Check if required Lua runtime dependencies are available.
146 |         Downloads lua-language-server if not present.
147 |         """
148 |         lua_ls_path = LuaLanguageServer._get_lua_ls_path()
149 | 
150 |         if not lua_ls_path:
151 |             print("lua-language-server not found. Downloading...")
152 |             lua_ls_path = LuaLanguageServer._download_lua_ls()
153 |             print(f"lua-language-server installed at: {lua_ls_path}")
154 | 
155 |         return lua_ls_path
156 | 
157 |     def __init__(
158 |         self, config: LanguageServerConfig, logger: LanguageServerLogger, repository_root_path: str, solidlsp_settings: SolidLSPSettings
159 |     ):
160 |         lua_ls_path = self._setup_runtime_dependency()
161 | 
162 |         super().__init__(
163 |             config,
164 |             logger,
165 |             repository_root_path,
166 |             ProcessLaunchInfo(cmd=lua_ls_path, cwd=repository_root_path),
167 |             "lua",
168 |             solidlsp_settings,
169 |         )
170 |         self.server_ready = threading.Event()
171 |         self.request_id = 0
172 | 
173 |     @staticmethod
174 |     def _get_initialize_params(repository_absolute_path: str) -> InitializeParams:
175 |         """
176 |         Returns the initialize params for the Lua Language Server.
177 |         """
178 |         root_uri = pathlib.Path(repository_absolute_path).as_uri()
179 |         initialize_params = {
180 |             "locale": "en",
181 |             "capabilities": {
182 |                 "textDocument": {
183 |                     "synchronization": {"didSave": True, "dynamicRegistration": True},
184 |                     "definition": {"dynamicRegistration": True},
185 |                     "references": {"dynamicRegistration": True},
186 |                     "documentSymbol": {
187 |                         "dynamicRegistration": True,
188 |                         "hierarchicalDocumentSymbolSupport": True,
189 |                         "symbolKind": {"valueSet": list(range(1, 27))},
190 |                     },
191 |                     "completion": {
192 |                         "dynamicRegistration": True,
193 |                         "completionItem": {
194 |                             "snippetSupport": True,
195 |                             "commitCharactersSupport": True,
196 |                             "documentationFormat": ["markdown", "plaintext"],
197 |                             "deprecatedSupport": True,
198 |                             "preselectSupport": True,
199 |                         },
200 |                     },
201 |                     "hover": {
202 |                         "dynamicRegistration": True,
203 |                         "contentFormat": ["markdown", "plaintext"],
204 |                     },
205 |                     "signatureHelp": {
206 |                         "dynamicRegistration": True,
207 |                         "signatureInformation": {
208 |                             "documentationFormat": ["markdown", "plaintext"],
209 |                             "parameterInformation": {"labelOffsetSupport": True},
210 |                         },
211 |                     },
212 |                 },
213 |                 "workspace": {
214 |                     "workspaceFolders": True,
215 |                     "didChangeConfiguration": {"dynamicRegistration": True},
216 |                     "configuration": True,
217 |                     "symbol": {
218 |                         "dynamicRegistration": True,
219 |                         "symbolKind": {"valueSet": list(range(1, 27))},
220 |                     },
221 |                 },
222 |             },
223 |             "processId": os.getpid(),
224 |             "rootPath": repository_absolute_path,
225 |             "rootUri": root_uri,
226 |             "workspaceFolders": [
227 |                 {
228 |                     "uri": root_uri,
229 |                     "name": os.path.basename(repository_absolute_path),
230 |                 }
231 |             ],
232 |             "initializationOptions": {
233 |                 # Lua Language Server specific options
234 |                 "runtime": {
235 |                     "version": "Lua 5.4",
236 |                     "path": ["?.lua", "?/init.lua"],
237 |                 },
238 |                 "diagnostics": {
239 |                     "enable": True,
240 |                     "globals": ["vim", "describe", "it", "before_each", "after_each"],  # Common globals
241 |                 },
242 |                 "workspace": {
243 |                     "library": [],  # Can be extended with project-specific libraries
244 |                     "checkThirdParty": False,
245 |                     "userThirdParty": [],
246 |                 },
247 |                 "telemetry": {
248 |                     "enable": False,
249 |                 },
250 |                 "completion": {
251 |                     "enable": True,
252 |                     "callSnippet": "Both",
253 |                     "keywordSnippet": "Both",
254 |                 },
255 |             },
256 |         }
257 |         return initialize_params
258 | 
259 |     def _start_server(self):
260 |         """Start Lua Language Server process"""
261 | 
262 |         def register_capability_handler(params):
263 |             return
264 | 
265 |         def window_log_message(msg):
266 |             self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
267 | 
268 |         def do_nothing(params):
269 |             return
270 | 
271 |         self.server.on_request("client/registerCapability", register_capability_handler)
272 |         self.server.on_notification("window/logMessage", window_log_message)
273 |         self.server.on_notification("$/progress", do_nothing)
274 |         self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
275 | 
276 |         self.logger.log("Starting Lua Language Server process", logging.INFO)
277 |         self.server.start()
278 |         initialize_params = self._get_initialize_params(self.repository_root_path)
279 | 
280 |         self.logger.log(
281 |             "Sending initialize request from LSP client to LSP server and awaiting response",
282 |             logging.INFO,
283 |         )
284 |         init_response = self.server.send.initialize(initialize_params)
285 | 
286 |         # Verify server capabilities
287 |         assert "textDocumentSync" in init_response["capabilities"]
288 |         assert "definitionProvider" in init_response["capabilities"]
289 |         assert "documentSymbolProvider" in init_response["capabilities"]
290 |         assert "referencesProvider" in init_response["capabilities"]
291 | 
292 |         self.server.notify.initialized({})
293 |         self.completions_available.set()
294 | 
295 |         # Lua Language Server is typically ready immediately after initialization
296 |         self.server_ready.set()
297 |         self.server_ready.wait()
298 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/erlang/test_erlang_ignored_dirs.py:
--------------------------------------------------------------------------------

```python
  1 | from collections.abc import Generator
  2 | from pathlib import Path
  3 | 
  4 | import pytest
  5 | 
  6 | from solidlsp import SolidLanguageServer
  7 | from solidlsp.ls_config import Language
  8 | from test.conftest import create_ls
  9 | 
 10 | from . import ERLANG_LS_UNAVAILABLE, ERLANG_LS_UNAVAILABLE_REASON
 11 | 
 12 | # These marks will be applied to all tests in this module
 13 | pytestmark = [
 14 |     pytest.mark.erlang,
 15 |     pytest.mark.skipif(ERLANG_LS_UNAVAILABLE, reason=f"Erlang LS not available: {ERLANG_LS_UNAVAILABLE_REASON}"),
 16 | ]
 17 | 
 18 | 
 19 | @pytest.fixture(scope="module")
 20 | def ls_with_ignored_dirs() -> Generator[SolidLanguageServer, None, None]:
 21 |     """Fixture to set up an LS for the erlang test repo with the 'ignored_dir' directory ignored."""
 22 |     ignored_paths = ["_build", "ignored_dir"]
 23 |     ls = create_ls(ignored_paths=ignored_paths, language=Language.ERLANG)
 24 |     ls.start()
 25 |     try:
 26 |         yield ls
 27 |     finally:
 28 |         try:
 29 |             ls.stop(shutdown_timeout=1.0)  # Shorter timeout for CI
 30 |         except Exception as e:
 31 |             print(f"Warning: Error stopping language server: {e}")
 32 |             # Force cleanup if needed
 33 |             if hasattr(ls, "server") and hasattr(ls.server, "process"):
 34 |                 try:
 35 |                     ls.server.process.terminate()
 36 |                 except:
 37 |                     pass
 38 | 
 39 | 
 40 | @pytest.mark.timeout(60)  # Add 60 second timeout
 41 | @pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
 42 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
 43 | def test_symbol_tree_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
 44 |     """Tests that request_full_symbol_tree ignores the configured directory."""
 45 |     root = ls_with_ignored_dirs.request_full_symbol_tree()[0]
 46 |     root_children = root["children"]
 47 |     children_names = {child["name"] for child in root_children}
 48 | 
 49 |     # Should have src, include, and test directories, but not _build or ignored_dir
 50 |     expected_dirs = {"src", "include", "test"}
 51 |     found_expected = expected_dirs.intersection(children_names)
 52 |     assert len(found_expected) > 0, f"Expected some dirs from {expected_dirs} to be in {children_names}"
 53 |     assert "_build" not in children_names, f"_build should not be in {children_names}"
 54 |     assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names}"
 55 | 
 56 | 
 57 | @pytest.mark.timeout(60)  # Add 60 second timeout
 58 | @pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
 59 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
 60 | def test_find_references_ignores_dir(ls_with_ignored_dirs: SolidLanguageServer):
 61 |     """Tests that find_references ignores the configured directory."""
 62 |     # Location of user record, which might be referenced in ignored_dir
 63 |     definition_file = "include/records.hrl"
 64 | 
 65 |     # Find the user record definition
 66 |     symbols = ls_with_ignored_dirs.request_document_symbols(definition_file)
 67 |     user_symbol = None
 68 |     for symbol_group in symbols:
 69 |         user_symbol = next((s for s in symbol_group if "user" in s.get("name", "").lower()), None)
 70 |         if user_symbol:
 71 |             break
 72 | 
 73 |     if not user_symbol or "selectionRange" not in user_symbol:
 74 |         pytest.skip("User record symbol not found for reference testing")
 75 | 
 76 |     sel_start = user_symbol["selectionRange"]["start"]
 77 |     references = ls_with_ignored_dirs.request_references(definition_file, sel_start["line"], sel_start["character"])
 78 | 
 79 |     # Assert that _build and ignored_dir do not appear in the references
 80 |     assert not any("_build" in ref["relativePath"] for ref in references), "_build should be ignored"
 81 |     assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored"
 82 | 
 83 | 
 84 | @pytest.mark.timeout(90)  # Longer timeout for this complex test
 85 | @pytest.mark.xfail(reason="Known timeout issue on Ubuntu CI with Erlang LS server startup", strict=False)
 86 | @pytest.mark.parametrize("repo_path", [Language.ERLANG], indirect=True)
 87 | def test_refs_and_symbols_with_glob_patterns(repo_path: Path) -> None:
 88 |     """Tests that refs and symbols with glob patterns are ignored."""
 89 |     ignored_paths = ["_build*", "ignored_*", "*.tmp"]
 90 |     ls = create_ls(ignored_paths=ignored_paths, repo_path=str(repo_path), language=Language.ERLANG)
 91 |     ls.start()
 92 | 
 93 |     try:
 94 |         # Same as in the above tests
 95 |         root = ls.request_full_symbol_tree()[0]
 96 |         root_children = root["children"]
 97 |         children_names = {child["name"] for child in root_children}
 98 | 
 99 |         # Should have src, include, and test directories, but not _build or ignored_dir
100 |         expected_dirs = {"src", "include", "test"}
101 |         found_expected = expected_dirs.intersection(children_names)
102 |         assert len(found_expected) > 0, f"Expected some dirs from {expected_dirs} to be in {children_names}"
103 |         assert "_build" not in children_names, f"_build should not be in {children_names} (glob pattern)"
104 |         assert "ignored_dir" not in children_names, f"ignored_dir should not be in {children_names} (glob pattern)"
105 | 
106 |         # Test that the refs and symbols with glob patterns are ignored
107 |         definition_file = "include/records.hrl"
108 | 
109 |         # Find the user record definition
110 |         symbols = ls.request_document_symbols(definition_file)
111 |         user_symbol = None
112 |         for symbol_group in symbols:
113 |             user_symbol = next((s for s in symbol_group if "user" in s.get("name", "").lower()), None)
114 |             if user_symbol:
115 |                 break
116 | 
117 |         if user_symbol and "selectionRange" in user_symbol:
118 |             sel_start = user_symbol["selectionRange"]["start"]
119 |             references = ls.request_references(definition_file, sel_start["line"], sel_start["character"])
120 | 
121 |             # Assert that _build and ignored_dir do not appear in references
122 |             assert not any("_build" in ref["relativePath"] for ref in references), "_build should be ignored (glob)"
123 |             assert not any("ignored_dir" in ref["relativePath"] for ref in references), "ignored_dir should be ignored (glob)"
124 |     finally:
125 |         try:
126 |             ls.stop(shutdown_timeout=1.0)  # Shorter timeout for CI
127 |         except Exception as e:
128 |             print(f"Warning: Error stopping glob pattern test LS: {e}")
129 |             # Force cleanup if needed
130 |             if hasattr(ls, "server") and hasattr(ls.server, "process"):
131 |                 try:
132 |                     ls.server.process.terminate()
133 |                 except:
134 |                     pass
135 | 
136 | 
137 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
138 | def test_default_ignored_directories(language_server: SolidLanguageServer):
139 |     """Test that default Erlang directories are ignored."""
140 |     # Test that Erlang-specific directories are ignored by default
141 |     assert language_server.is_ignored_dirname("_build"), "_build should be ignored"
142 |     assert language_server.is_ignored_dirname("ebin"), "ebin should be ignored"
143 |     assert language_server.is_ignored_dirname("deps"), "deps should be ignored"
144 |     assert language_server.is_ignored_dirname(".rebar3"), ".rebar3 should be ignored"
145 |     assert language_server.is_ignored_dirname("_checkouts"), "_checkouts should be ignored"
146 |     assert language_server.is_ignored_dirname("node_modules"), "node_modules should be ignored"
147 | 
148 |     # Test that important directories are not ignored
149 |     assert not language_server.is_ignored_dirname("src"), "src should not be ignored"
150 |     assert not language_server.is_ignored_dirname("include"), "include should not be ignored"
151 |     assert not language_server.is_ignored_dirname("test"), "test should not be ignored"
152 |     assert not language_server.is_ignored_dirname("priv"), "priv should not be ignored"
153 | 
154 | 
155 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
156 | def test_symbol_tree_excludes_build_dirs(language_server: SolidLanguageServer):
157 |     """Test that symbol tree excludes build and dependency directories."""
158 |     symbol_tree = language_server.request_full_symbol_tree()
159 | 
160 |     if symbol_tree:
161 |         root = symbol_tree[0]
162 |         children_names = {child["name"] for child in root.get("children", [])}
163 | 
164 |         # Build and dependency directories should not appear
165 |         ignored_dirs = {"_build", "ebin", "deps", ".rebar3", "_checkouts", "node_modules"}
166 |         found_ignored = ignored_dirs.intersection(children_names)
167 |         assert len(found_ignored) == 0, f"Found ignored directories in symbol tree: {found_ignored}"
168 | 
169 |         # Important directories should appear
170 |         important_dirs = {"src", "include", "test"}
171 |         found_important = important_dirs.intersection(children_names)
172 |         assert len(found_important) > 0, f"Expected to find important directories: {important_dirs}, got: {children_names}"
173 | 
174 | 
175 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
176 | def test_ignore_compiled_files(language_server: SolidLanguageServer):
177 |     """Test that compiled Erlang files are ignored."""
178 |     # Test that beam files are ignored
179 |     assert language_server.is_ignored_filename("module.beam"), "BEAM files should be ignored"
180 |     assert language_server.is_ignored_filename("app.beam"), "BEAM files should be ignored"
181 | 
182 |     # Test that source files are not ignored
183 |     assert not language_server.is_ignored_filename("module.erl"), "Erlang source files should not be ignored"
184 |     assert not language_server.is_ignored_filename("records.hrl"), "Header files should not be ignored"
185 | 
186 | 
187 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
188 | def test_rebar_directories_ignored(language_server: SolidLanguageServer):
189 |     """Test that rebar-specific directories are ignored."""
190 |     # Test rebar3-specific directories
191 |     assert language_server.is_ignored_dirname("_build"), "rebar3 _build should be ignored"
192 |     assert language_server.is_ignored_dirname("_checkouts"), "rebar3 _checkouts should be ignored"
193 |     assert language_server.is_ignored_dirname(".rebar3"), "rebar3 cache should be ignored"
194 | 
195 |     # Test that rebar.lock and rebar.config are not ignored (they are configuration files)
196 |     assert not language_server.is_ignored_filename("rebar.config"), "rebar.config should not be ignored"
197 |     assert not language_server.is_ignored_filename("rebar.lock"), "rebar.lock should not be ignored"
198 | 
199 | 
200 | @pytest.mark.parametrize("ls_with_ignored_dirs", [Language.ERLANG], indirect=True)
201 | def test_document_symbols_ignores_dirs(ls_with_ignored_dirs: SolidLanguageServer):
202 |     """Test that document symbols from ignored directories are not included."""
203 |     # Try to get symbols from a file in ignored directory (should not find it)
204 |     try:
205 |         ignored_file = "ignored_dir/ignored_module.erl"
206 |         symbols = ls_with_ignored_dirs.request_document_symbols(ignored_file)
207 |         # If we get here, the file was found - symbols should be empty or None
208 |         if symbols:
209 |             assert len(symbols) == 0, "Should not find symbols in ignored directory"
210 |     except Exception:
211 |         # This is expected - the file should not be accessible
212 |         pass
213 | 
214 | 
215 | @pytest.mark.parametrize("language_server", [Language.ERLANG], indirect=True)
216 | def test_erlang_specific_ignore_patterns(language_server: SolidLanguageServer):
217 |     """Test Erlang-specific ignore patterns work correctly."""
218 |     erlang_ignored_dirs = ["_build", "ebin", ".rebar3", "_checkouts", "cover"]
219 | 
220 |     # These should be ignored
221 |     for dirname in erlang_ignored_dirs:
222 |         assert language_server.is_ignored_dirname(dirname), f"{dirname} should be ignored"
223 | 
224 |     # These should not be ignored
225 |     erlang_important_dirs = ["src", "include", "test", "priv"]
226 |     for dirname in erlang_important_dirs:
227 |         assert not language_server.is_ignored_dirname(dirname), f"{dirname} should not be ignored"
228 | 
```

--------------------------------------------------------------------------------
/test/solidlsp/lua/test_lua_basic.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Tests for the Lua language server implementation.
  3 | 
  4 | These tests validate symbol finding and cross-file reference capabilities
  5 | for Lua modules and functions.
  6 | """
  7 | 
  8 | import pytest
  9 | 
 10 | from solidlsp import SolidLanguageServer
 11 | from solidlsp.ls_config import Language
 12 | from solidlsp.ls_types import SymbolKind
 13 | 
 14 | 
 15 | @pytest.mark.lua
 16 | class TestLuaLanguageServer:
 17 |     """Test Lua language server symbol finding and cross-file references."""
 18 | 
 19 |     @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
 20 |     def test_find_symbols_in_calculator(self, language_server: SolidLanguageServer) -> None:
 21 |         """Test finding specific functions in calculator.lua."""
 22 |         symbols = language_server.request_document_symbols("src/calculator.lua")
 23 | 
 24 |         assert symbols is not None
 25 |         assert len(symbols) > 0
 26 | 
 27 |         # Extract function names from the returned structure
 28 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 29 |         function_names = set()
 30 |         for symbol in symbol_list:
 31 |             if isinstance(symbol, dict):
 32 |                 name = symbol.get("name", "")
 33 |                 # Handle both plain names and module-prefixed names
 34 |                 if "." in name:
 35 |                     name = name.split(".")[-1]
 36 |                 if symbol.get("kind") == SymbolKind.Function:
 37 |                     function_names.add(name)
 38 | 
 39 |         # Verify exact calculator functions exist
 40 |         expected_functions = {"add", "subtract", "multiply", "divide", "factorial"}
 41 |         found_functions = function_names & expected_functions
 42 |         assert found_functions == expected_functions, f"Expected exactly {expected_functions}, found {found_functions}"
 43 | 
 44 |         # Verify specific functions
 45 |         assert "add" in function_names, "add function not found"
 46 |         assert "multiply" in function_names, "multiply function not found"
 47 |         assert "factorial" in function_names, "factorial function not found"
 48 | 
 49 |     @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
 50 |     def test_find_symbols_in_utils(self, language_server: SolidLanguageServer) -> None:
 51 |         """Test finding specific functions in utils.lua."""
 52 |         symbols = language_server.request_document_symbols("src/utils.lua")
 53 | 
 54 |         assert symbols is not None
 55 |         assert len(symbols) > 0
 56 | 
 57 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 58 |         function_names = set()
 59 |         all_symbols = set()
 60 | 
 61 |         for symbol in symbol_list:
 62 |             if isinstance(symbol, dict):
 63 |                 name = symbol.get("name", "")
 64 |                 all_symbols.add(name)
 65 |                 # Handle both plain names and module-prefixed names
 66 |                 if "." in name:
 67 |                     name = name.split(".")[-1]
 68 |                 if symbol.get("kind") == SymbolKind.Function:
 69 |                     function_names.add(name)
 70 | 
 71 |         # Verify exact string utility functions
 72 |         expected_utils = {"trim", "split", "starts_with", "ends_with"}
 73 |         found_utils = function_names & expected_utils
 74 |         assert found_utils == expected_utils, f"Expected exactly {expected_utils}, found {found_utils}"
 75 | 
 76 |         # Verify exact table utility functions
 77 |         table_utils = {"deep_copy", "table_contains", "table_merge"}
 78 |         found_table_utils = function_names & table_utils
 79 |         assert found_table_utils == table_utils, f"Expected exactly {table_utils}, found {found_table_utils}"
 80 | 
 81 |         # Check for Logger class/table
 82 |         assert "Logger" in all_symbols or any("Logger" in s for s in all_symbols), "Logger not found in symbols"
 83 | 
 84 |     @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
 85 |     def test_find_symbols_in_main(self, language_server: SolidLanguageServer) -> None:
 86 |         """Test finding functions in main.lua."""
 87 |         symbols = language_server.request_document_symbols("main.lua")
 88 | 
 89 |         assert symbols is not None
 90 |         assert len(symbols) > 0
 91 | 
 92 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
 93 |         function_names = set()
 94 | 
 95 |         for symbol in symbol_list:
 96 |             if isinstance(symbol, dict) and symbol.get("kind") == SymbolKind.Function:
 97 |                 function_names.add(symbol.get("name", ""))
 98 | 
 99 |         # Verify exact main functions exist
100 |         expected_funcs = {"print_banner", "test_calculator", "test_utils"}
101 |         found_funcs = function_names & expected_funcs
102 |         assert found_funcs == expected_funcs, f"Expected exactly {expected_funcs}, found {found_funcs}"
103 | 
104 |         assert "test_calculator" in function_names, "test_calculator function not found"
105 |         assert "test_utils" in function_names, "test_utils function not found"
106 | 
107 |     @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
108 |     def test_cross_file_references_calculator_add(self, language_server: SolidLanguageServer) -> None:
109 |         """Test finding cross-file references to calculator.add function."""
110 |         symbols = language_server.request_document_symbols("src/calculator.lua")
111 | 
112 |         assert symbols is not None
113 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
114 | 
115 |         # Find the add function
116 |         add_symbol = None
117 |         for sym in symbol_list:
118 |             if isinstance(sym, dict):
119 |                 name = sym.get("name", "")
120 |                 if "add" in name or name == "add":
121 |                     add_symbol = sym
122 |                     break
123 | 
124 |         assert add_symbol is not None, "add function not found in calculator.lua"
125 | 
126 |         # Get references to the add function
127 |         range_info = add_symbol.get("selectionRange", add_symbol.get("range"))
128 |         assert range_info is not None, "add function has no range information"
129 | 
130 |         range_start = range_info["start"]
131 |         refs = language_server.request_references("src/calculator.lua", range_start["line"], range_start["character"])
132 | 
133 |         assert refs is not None
134 |         assert isinstance(refs, list)
135 |         # add function appears in: main.lua (lines 16, 71), test_calculator.lua (lines 22, 23, 24)
136 |         # Note: The declaration itself may or may not be included as a reference
137 |         assert len(refs) >= 5, f"Should find at least 5 references to calculator.add, found {len(refs)}"
138 | 
139 |         # Verify exact reference locations
140 |         ref_files = {}
141 |         for ref in refs:
142 |             filename = ref.get("uri", "").split("/")[-1]
143 |             if filename not in ref_files:
144 |                 ref_files[filename] = []
145 |             ref_files[filename].append(ref["range"]["start"]["line"])
146 | 
147 |         # The declaration may or may not be included
148 |         if "calculator.lua" in ref_files:
149 |             assert (
150 |                 5 in ref_files["calculator.lua"]
151 |             ), f"If declaration is included, it should be at line 6 (0-indexed: 5), found at {ref_files['calculator.lua']}"
152 | 
153 |         # Check main.lua has usages
154 |         assert "main.lua" in ref_files, "Should find add usages in main.lua"
155 |         assert (
156 |             15 in ref_files["main.lua"] or 70 in ref_files["main.lua"]
157 |         ), f"Should find add usage in main.lua, found at lines {ref_files.get('main.lua', [])}"
158 | 
159 |         # Check for cross-file references from main.lua
160 |         main_refs = [ref for ref in refs if "main.lua" in ref.get("uri", "")]
161 |         assert len(main_refs) > 0, "calculator.add should be called in main.lua"
162 | 
163 |     @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
164 |     def test_cross_file_references_utils_trim(self, language_server: SolidLanguageServer) -> None:
165 |         """Test finding cross-file references to utils.trim function."""
166 |         symbols = language_server.request_document_symbols("src/utils.lua")
167 | 
168 |         assert symbols is not None
169 |         symbol_list = symbols[0] if isinstance(symbols, tuple) else symbols
170 | 
171 |         # Find the trim function
172 |         trim_symbol = None
173 |         for sym in symbol_list:
174 |             if isinstance(sym, dict):
175 |                 name = sym.get("name", "")
176 |                 if "trim" in name or name == "trim":
177 |                     trim_symbol = sym
178 |                     break
179 | 
180 |         assert trim_symbol is not None, "trim function not found in utils.lua"
181 | 
182 |         # Get references to the trim function
183 |         range_info = trim_symbol.get("selectionRange", trim_symbol.get("range"))
184 |         assert range_info is not None, "trim function has no range information"
185 | 
186 |         range_start = range_info["start"]
187 |         refs = language_server.request_references("src/utils.lua", range_start["line"], range_start["character"])
188 | 
189 |         assert refs is not None
190 |         assert isinstance(refs, list)
191 |         # trim function appears in: usage (line 32 in main.lua)
192 |         # Note: The declaration itself may or may not be included as a reference
193 |         assert len(refs) >= 1, f"Should find at least 1 reference to utils.trim, found {len(refs)}"
194 | 
195 |         # Verify exact reference locations
196 |         ref_files = {}
197 |         for ref in refs:
198 |             filename = ref.get("uri", "").split("/")[-1]
199 |             if filename not in ref_files:
200 |                 ref_files[filename] = []
201 |             ref_files[filename].append(ref["range"]["start"]["line"])
202 | 
203 |         # The declaration may or may not be included
204 |         if "utils.lua" in ref_files:
205 |             assert (
206 |                 5 in ref_files["utils.lua"]
207 |             ), f"If declaration is included, it should be at line 6 (0-indexed: 5), found at {ref_files['utils.lua']}"
208 | 
209 |         # Check main.lua has usage
210 |         assert "main.lua" in ref_files, "Should find trim usage in main.lua"
211 |         assert (
212 |             31 in ref_files["main.lua"]
213 |         ), f"Should find trim usage at line 32 (0-indexed: 31) in main.lua, found at lines {ref_files.get('main.lua', [])}"
214 | 
215 |         # Check for cross-file references from main.lua
216 |         main_refs = [ref for ref in refs if "main.lua" in ref.get("uri", "")]
217 |         assert len(main_refs) > 0, "utils.trim should be called in main.lua"
218 | 
219 |     @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
220 |     def test_hover_information(self, language_server: SolidLanguageServer) -> None:
221 |         """Test hover information for symbols."""
222 |         # Get hover info for a function
223 |         hover_info = language_server.request_hover("src/calculator.lua", 5, 10)  # Position near add function
224 | 
225 |         assert hover_info is not None, "Should provide hover information"
226 | 
227 |         # Hover info could be a dict with 'contents' or a string
228 |         if isinstance(hover_info, dict):
229 |             assert "contents" in hover_info or "value" in hover_info, "Hover should have contents"
230 | 
231 |     @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
232 |     def test_full_symbol_tree(self, language_server: SolidLanguageServer) -> None:
233 |         """Test that full symbol tree is not empty."""
234 |         symbols = language_server.request_full_symbol_tree()
235 | 
236 |         assert symbols is not None
237 |         assert len(symbols) > 0, "Symbol tree should not be empty"
238 | 
239 |         # The tree should have at least one root node
240 |         root = symbols[0]
241 |         assert isinstance(root, dict), "Root should be a dict"
242 |         assert "name" in root, "Root should have a name"
243 | 
244 |     @pytest.mark.parametrize("language_server", [Language.LUA], indirect=True)
245 |     def test_references_between_test_and_source(self, language_server: SolidLanguageServer) -> None:
246 |         """Test finding references from test files to source files."""
247 |         # Check if test_calculator.lua references calculator module
248 |         test_symbols = language_server.request_document_symbols("tests/test_calculator.lua")
249 | 
250 |         assert test_symbols is not None
251 |         assert len(test_symbols) > 0
252 | 
253 |         # The test file should have some content that references calculator
254 |         symbol_list = test_symbols[0] if isinstance(test_symbols, tuple) else test_symbols
255 |         assert len(symbol_list) > 0, "test_calculator.lua should have symbols"
256 | 
```
Page 5/14FirstPrevNextLast