This guide explains how to add support for a new programming language to Serena.
Adding a new language involves:
- Language Server Implementation - Creating a language-specific server class
- Language Registration - Adding the language to enums and configurations
- Test Repository - Creating a minimal test project
- Test Suite - Writing comprehensive tests
- Documentation - Updating the user-facing language lists and the changelog
Create a new file in src/solidlsp/language_servers/ (e.g., new_language_server.py).
All language servers use the DependencyProvider pattern to handle
- runtime dependency installation/discovery
- launch command creation (and, optionally, environment setup)
To implement a new language server using the DependencyProvider pattern:
- Pass
Noneforprocess_launch_infoinsuper().__init__()- the base class creates it via_create_dependency_provider() - Implement
_create_dependency_provider()to return an innerDependencyProviderclass instance. In simple cases, it can be instantiated with only two parameters:The resource dir that is passed is the directory in which installed dependencies should be stored!def _create_dependency_provider(self) -> LanguageServerDependencyProvider: return self.DependencyProvider(self._custom_settings, self._ls_resources_dir)
Base Classes (choose the most specific one that fits):
-
LanguageServerDependencyProviderUvx- For language servers distributed as a PyPI package, run on demand viauvx/uv x(no installation step to implement)- Simply instantiate it with the package name, pinned default version, entrypoint (console script) and optional
extra_args; the version can be overridden by the user via the configuredversion_setting_keycustom setting - Reference implementation:
PyrightServer
- Simply instantiate it with the package name, pinned default version, entrypoint (console script) and optional
-
LanguageServerDependencyProviderBaseCommand- For the common case where the launch command is constructed from a base command (which the user can override via custom settings; handled generically)- Implement
_create_default_base_command()to return the default base command (executable + args), downloading/installing dependencies beforehand if necessary - Implement
_create_launch_command_from_base_command(base_command)to add any further arguments, producing the final launch command
- Implement
-
LanguageServerDependencyProviderSinglePath- Alternative to inheriting from...BaseCommanddirectly for the case of a single core dependency (e.g., an executable or JAR file); use when the single path is not used directly as a base command otherwise inherit from...BaseCommanddirectly- Implement
_get_or_install_core_dependency()to return the path to the core dependency, downloading/installing it automatically if necessary - Implement
_create_launch_command(core_path)to build the full command from the core path - Reference implementations:
TypeScriptLanguageServer,Intelephense,ClojureLSP,ClangdLanguageServer
- Implement
-
LanguageServerDependencyProvider- The root base class, for complex cases with multiple dependencies or custom setup- Implement
create_launch_command()directly (note: no automatic support for user-level launch command overrides in this case) - Reference implementations:
EclipseJDTLS,CSharpLanguageServer,MatlabLanguageServer
- Implement
Implementation Pointers:
- Override
create_launch_command_envif the launch command needs environment variables to be set (defaults to{}in the base implementation) - When calling subprocesses, e.g. to install dependencies, do not use
subprocess.rundirectly; instead, use thesubprocess_runhelper function fromsolidlsp.util.subprocess_util
You should look at at least one existing implementation of each base class to understand how they work.
Use DownloadedDependency (in solidlsp.dependency_provider), which bundles the URL,
archive type, allowed hosts and checksum verification behind a single download_to() call:
dep = DownloadedDependency(
url=f"https://example.org/foo-{version}-{platform}.zip",
archive_type="zip", # optional FileUtils.ArchiveType for extraction
allowed_hosts=FOO_ALLOWED_HOSTS, # optional list of allowed hosts
)
dep.download_to(target_dir)Checksums for downloads live in a URL-keyed database, src/solidlsp/resources/downloaded_dependency_hashes.json,
managed by DownloadedDependencyHashDatabase.
Consequences for your implementation:
- Build each dependency in a factory classmethod (
_create_dep_*) that takes an optional version and falls back to the pinnedDEFAULT_*constant. - Add an
update_dep_hashes()classmethod that constructs every dependency and updates the hashes:Hook a call to this method into@classmethod def update_dep_hashes(cls) -> None: deps = [cls._create_dep_foo(), cls_._create_dep_bar(), ...] with DownloadedDependencyHashDatabase.get_instance().update_context() as db: for dep in deps: db.update(dep)
scripts/update_downloaded_dependency_hashes.py, run that script, and commit the resulting JSON changes. - After bumping any pinned version, re-run the script. Add a NOTE comment next to the version constants saying so; a stale database means unverified downloads locally and a CI failure.
- Pass
verified=Falseonly for dependencies whose hash cannot be pinned by design (e.g. a user-supplied version override).
Reference implementation: EclipseJDTLS.DependencyProvider
Note that several older language servers still define hashes locally in constants and
call FileUtils.download_and_extract_archive_verifieddirectly. Do not apply this legacy approach.
Override _create_base_initialize_params to provide server-specific initialization
parameters. The common keys — processId, rootPath, rootUri, clientInfo and
workspaceFolders — are set centrally by the InitializeParamsBuilder (see
src/solidlsp/initialize_params.py), so your override MUST NOT set them. Just return
the server-specific settings (typically capabilities and initializationOptions):
def _create_base_initialize_params(self) -> dict:
"""Return language-specific initialization parameters (server-specific keys only)."""
return {
"capabilities": {
# Language-specific capabilities
},
# "initializationOptions": {...}, # if the server needs them
}
def _start_server(self):
"""Start the language server with custom handlers."""
# Set up notification handlers
self.server.on_notification("window/logMessage", self._handle_log_message)
# Start server and initialize. Do NOT call _create_base_initialize_params directly;
# _create_initialize_params() wraps it with the builder to add the common keys.
self.server.start()
init_response = self.server.send.initialize(self._create_initialize_params())
self.server.notify.initialized({})Notes:
- The builder resolves
workspaceFoldersfrom the language server config (indexed folders +ls_additional_workspace_folders); don't build the folder list yourself. - To send a folder list nested inside
initializationOptions(some servers, e.g.EclipseJDTLS/KotlinLanguageServer, need this), set it explicitly there — only the top-levelworkspaceFoldersis builder-managed. - To suppress the top-level
workspaceFoldersentirely, override_create_initialize_params_builderand constructDefaultInitializeParamsBuilderwithset_workspace_folders=False.
After _start_server returns, the language server should be fully operational.
If the server requires that one waits for certain notifications or responses before being ready, implement that logic here.
For an example, see EclipseJDTLS._start_server.
In src/solidlsp/ls_config.py, add your language to the enum:
class LanguageServerId(str, Enum):
# Existing languages...
NEW_LANGUAGE = "new_language"
def get_source_fn_matcher(self) -> FilenameMatcher:
match self:
# Existing cases...
case self.NEW_LANGUAGE:
return FilenameMatcher(".newlang", ".nl") # File extensions
...
def get_ls_class(self) -> type["SolidLanguageServer"]:
match self:
# Existing cases...
case self.NEW_LANGUAGE:
from solidlsp.language_servers.new_language_server import NewLanguageServer
return NewLanguageServerCreate a minimal project in test/resources/repos/new_language/test_repo/:
test/resources/repos/new_language/test_repo/
├── main.newlang # Main source file
├── lib/
│ └── helper.newlang # Additional source for testing
├── project.toml # Project configuration (if applicable)
└── .gitignore # Ignore build artifacts
Create meaningful source files that demonstrate:
- Classes/Types - For symbol testing
- Functions/Methods - For reference finding
- Imports/Dependencies - For cross-file operations
- Nested Structures - For hierarchical symbol testing
Example main.newlang:
import lib.helper
class Calculator {
func add(a: Int, b: Int) -> Int {
return a + b
}
func subtract(a: Int, b: Int) -> Int {
return helper.subtract(a, b) // Reference to imported function
}
}
class Program {
func main() {
let calc = Calculator()
let result = calc.add(5, 3) // Reference to add method
print(result)
}
}
Testing the language server implementation is of crucial importance, and the tests will form the main part of the review process. Make sure that the tests are up to the standard of Serena to make the review go smoother.
General rules for tests:
- Tests for symbols and references should always check that the expected symbol names and references were actually found. Just testing that a list came back or that the result is not None is insufficient.
- Tests should never be skipped, the only exception is skipping based on some package being available or on an unsupported OS.
- Tests should run in CI, check if there is a suitable GitHub action for installing the dependencies.
Create test/solidlsp/new_language/test_new_language_basic.py.
Have a look at the structure of existing tests, for example, in test/solidlsp/php/test_php_basic.py
You should at least test:
- Finding symbols
- Finding within-file references
- Finding cross-file references
Have a look at test/solidlsp/php/test_php_basic.py as an example for what should be tested.
Declare the new language marker under [tool.pytest.ini_options].markers in pyproject.toml.
Update:
- README.md - Add language to the list of languages
- docs/01-about/020_programming-languages.md - Add language to the list and mention any special notes, compatibility or requirements (e.g. installations the user is required to do)
- src/serena/resources/project.template.yml - Refresh the commented language-server list: run
uv run python scripts/print_language_list.pyand paste its output over the existing list, stripping the trailing spaces the script pads each line with - CHANGELOG.md - Document the new language support