diff --git a/.gitignore b/.gitignore
index ac62ed2..f284172 100644
--- a/.gitignore
+++ b/.gitignore
@@ -410,4 +410,5 @@ src/multilspy/language_servers/clangd_language_server/static/
venv/
src/multilspy/language_servers/intelephense/static/
-src/multilspy/language_servers/elixir_language_server/static/
\ No newline at end of file
+src/multilspy/language_servers/elixir_language_server/static/
+src/multilspy/language_servers/bsl_language_server/static/
\ No newline at end of file
diff --git a/README.md b/README.md
index e3acb87..8a8929c 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ pip install multilspy
| kotlin | KotlinLanguageServer |
| php | Intelephense |
| cpp | clangd |
+| bsl | [bsl-language-server](https://github.com/1c-syntax/bsl-language-server) (1C:Enterprise / OneScript — requires Java 17+) |
## Usage
@@ -60,7 +61,7 @@ from multilspy import SyncLanguageServer
from multilspy.multilspy_config import MultilspyConfig
from multilspy.multilspy_logger import MultilspyLogger
...
-config = MultilspyConfig.from_dict({"code_language": "java"}) # Also supports "python", "rust", "csharp", "typescript", "javascript", "go", "dart", "ruby", "kotlin", "php"
+config = MultilspyConfig.from_dict({"code_language": "java"}) # Also supports "python", "rust", "csharp", "typescript", "javascript", "go", "dart", "ruby", "kotlin", "php", "bsl"
logger = MultilspyLogger()
lsp = SyncLanguageServer.create(config, logger, "/abs/path/to/project/root/")
with lsp.start_server():
diff --git a/src/multilspy/language_server.py b/src/multilspy/language_server.py
index 3af094b..5656c14 100644
--- a/src/multilspy/language_server.py
+++ b/src/multilspy/language_server.py
@@ -131,6 +131,10 @@ def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_roo
from multilspy.language_servers.elixir_language_server.elixir_language_server import ElixirLanguageServer
return ElixirLanguageServer(config, logger, repository_root_path)
+ elif config.code_language == Language.BSL:
+ from multilspy.language_servers.bsl_language_server.bsl_language_server import BSLLanguageServer
+
+ return BSLLanguageServer(config, logger, repository_root_path)
else:
logger.log(f"Language {config.code_language} is not supported", logging.ERROR)
diff --git a/src/multilspy/language_servers/bsl_language_server/__init__.py b/src/multilspy/language_servers/bsl_language_server/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/multilspy/language_servers/bsl_language_server/bsl_language_server.py b/src/multilspy/language_servers/bsl_language_server/bsl_language_server.py
new file mode 100644
index 0000000..97e03d0
--- /dev/null
+++ b/src/multilspy/language_servers/bsl_language_server/bsl_language_server.py
@@ -0,0 +1,177 @@
+"""
+Provides BSL-specific instantiation of the LanguageServer class.
+
+BSL (Business Scripting Language) is the programming language of the
+1C:Enterprise platform and OneScript. This adapter wraps the bsl-language-server
+JAR from https://github.com/1c-syntax/bsl-language-server.
+
+Prerequisites:
+ - Java 17+ must be available in PATH.
+ - The BSL Language Server JAR is downloaded on first use from GitHub releases.
+"""
+
+import asyncio
+import hashlib
+import json
+import logging
+import os
+import pathlib
+import shutil
+from contextlib import asynccontextmanager
+from typing import AsyncIterator
+
+from multilspy.language_server import LanguageServer
+from multilspy.lsp_protocol_handler.server import ProcessLaunchInfo
+from multilspy.multilspy_config import MultilspyConfig
+from multilspy.multilspy_logger import MultilspyLogger
+from multilspy.multilspy_utils import FileUtils
+
+
+class BSLLanguageServer(LanguageServer):
+ """
+ Provides BSL-specific instantiation of the LanguageServer class.
+ """
+
+ def __init__(
+ self,
+ config: MultilspyConfig,
+ logger: MultilspyLogger,
+ repository_root_path: str,
+ ):
+ """
+ Creates a BSL Language Server instance. Do not instantiate directly;
+ use `LanguageServer.create` with `Language.BSL`.
+ """
+ jar_path = self._ensure_jar(logger)
+ super().__init__(
+ config,
+ logger,
+ repository_root_path,
+ ProcessLaunchInfo(
+ cmd=["java", "-jar", str(jar_path), "--lsp"],
+ cwd=repository_root_path,
+ ),
+ "bsl",
+ )
+
+ def _ensure_jar(self, logger: MultilspyLogger) -> str:
+ """Verify Java and the BSL LS JAR are available; download JAR if missing."""
+ if shutil.which("java") is None:
+ raise RuntimeError(
+ "Java 17+ is required to run BSL Language Server, "
+ "but `java` was not found in PATH. "
+ "Install a JDK 17+ from https://adoptium.net/ "
+ "and ensure `java` is on PATH."
+ )
+
+ with open(
+ os.path.join(os.path.dirname(__file__), "runtime_dependencies.json"),
+ "r",
+ encoding="utf-8",
+ ) as f:
+ deps = json.load(f)
+ dep = deps["runtimeDependency"]
+
+ server_dir = os.path.join(os.path.dirname(__file__), "static", dep["version"])
+ os.makedirs(server_dir, exist_ok=True)
+ jar_path = os.path.join(server_dir, dep["binaryName"])
+
+ if not os.path.exists(jar_path):
+ logger.log(
+ f"Downloading BSL Language Server {dep['version']} "
+ f"from {dep['url']}",
+ logging.INFO,
+ )
+ FileUtils.download_file(logger, dep["url"], jar_path)
+
+ expected_sha = dep.get("sha256") or ""
+ if expected_sha:
+ with open(jar_path, "rb") as jar_f:
+ actual_sha = hashlib.sha256(jar_f.read()).hexdigest()
+ if actual_sha != expected_sha:
+ os.remove(jar_path)
+ raise RuntimeError(
+ f"BSL Language Server JAR SHA256 mismatch: "
+ f"expected {expected_sha}, got {actual_sha}. "
+ f"Corrupted or tampered download; retry required."
+ )
+
+ return jar_path
+
+ def _get_initialize_params(self, repository_absolute_path: str) -> dict:
+ with open(
+ os.path.join(os.path.dirname(__file__), "initialize_params.json"),
+ "r",
+ encoding="utf-8",
+ ) as f:
+ d = json.load(f)
+
+ del d["_description"]
+
+ root = pathlib.Path(repository_absolute_path)
+ d["processId"] = os.getpid()
+ d["rootPath"] = repository_absolute_path
+ d["rootUri"] = root.as_uri()
+ d["workspaceFolders"][0]["uri"] = root.as_uri()
+ d["workspaceFolders"][0]["name"] = root.name or "workspace"
+ return d
+
+ @asynccontextmanager
+ async def start_server(self) -> AsyncIterator["BSLLanguageServer"]:
+ async def do_nothing(params): # noqa: ARG001
+ return
+
+ async def log_message(msg):
+ self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO)
+
+ self.server.on_request("client/registerCapability", do_nothing)
+ self.server.on_notification("language/status", do_nothing)
+ self.server.on_notification("window/logMessage", log_message)
+ self.server.on_notification("window/showMessage", do_nothing)
+ self.server.on_notification("textDocument/publishDiagnostics", do_nothing)
+ self.server.on_notification("$/progress", do_nothing)
+
+ async with super().start_server():
+ self.logger.log(
+ f"Starting BSL Language Server: {self.server.process_launch_info.cmd}",
+ logging.INFO,
+ )
+ await self.server.start()
+
+ if self.server.process.returncode is not None:
+ raise RuntimeError(
+ f"BSL Language Server failed to start "
+ f"(exit code {self.server.process.returncode}). "
+ f"Check Java version (17+ required)."
+ )
+
+ initialize_params = self._get_initialize_params(self.repository_root_path)
+
+ init_response = await asyncio.wait_for(
+ self.server.send.initialize(initialize_params),
+ timeout=120,
+ )
+ self.logger.log(
+ f"BSL LS capabilities: "
+ f"{list(init_response.get('capabilities', {}).keys())}",
+ logging.INFO,
+ )
+
+ self.server.notify.initialized({})
+ self.completions_available.set()
+
+ try:
+ yield self
+ finally:
+ try:
+ await self.server.shutdown()
+ except Exception as e:
+ self.logger.log(
+ f"BSL LS shutdown() raised: {e!r}", logging.WARNING
+ )
+ try:
+ await self.server.stop()
+ except Exception as e:
+ self.logger.log(
+ f"BSL LS stop() raised: {e!r}", logging.WARNING
+ )
diff --git a/src/multilspy/language_servers/bsl_language_server/initialize_params.json b/src/multilspy/language_servers/bsl_language_server/initialize_params.json
new file mode 100644
index 0000000..9b19063
--- /dev/null
+++ b/src/multilspy/language_servers/bsl_language_server/initialize_params.json
@@ -0,0 +1,51 @@
+{
+ "_description": "Initialization parameters for the BSL Language Server (1C Enterprise / OneScript). See https://github.com/1c-syntax/bsl-language-server.",
+ "processId": "$processId",
+ "rootPath": "$rootPath",
+ "rootUri": "$rootUri",
+ "workspaceFolders": [
+ {
+ "uri": "$rootUri",
+ "name": "$rootName"
+ }
+ ],
+ "capabilities": {
+ "workspace": {
+ "workspaceFolders": true,
+ "configuration": true,
+ "didChangeWatchedFiles": {
+ "dynamicRegistration": false
+ },
+ "workspaceSymbol": {}
+ },
+ "textDocument": {
+ "synchronization": {
+ "didSave": true
+ },
+ "documentSymbol": {
+ "hierarchicalDocumentSymbolSupport": true
+ },
+ "definition": {
+ "linkSupport": true
+ },
+ "references": {},
+ "rename": {
+ "prepareSupport": true
+ },
+ "hover": {
+ "contentFormat": ["markdown", "plaintext"]
+ },
+ "completion": {
+ "completionItem": {
+ "snippetSupport": true,
+ "documentationFormat": ["markdown", "plaintext"]
+ }
+ },
+ "signatureHelp": {},
+ "formatting": {},
+ "codeAction": {}
+ }
+ },
+ "initializationOptions": {},
+ "trace": "off"
+}
diff --git a/src/multilspy/language_servers/bsl_language_server/runtime_dependencies.json b/src/multilspy/language_servers/bsl_language_server/runtime_dependencies.json
new file mode 100644
index 0000000..86d28f1
--- /dev/null
+++ b/src/multilspy/language_servers/bsl_language_server/runtime_dependencies.json
@@ -0,0 +1,12 @@
+{
+ "_description": "Used to download the runtime dependencies for BSL Language Server from https://github.com/1c-syntax/bsl-language-server. Java 17+ must be available in PATH.",
+ "runtimeDependency": {
+ "id": "BSLLanguageServer",
+ "description": "BSL Language Server (1C Enterprise / OneScript)",
+ "version": "0.22.0",
+ "url": "https://github.com/1c-syntax/bsl-language-server/releases/download/0.22.0/bsl-language-server-0.22.0.jar",
+ "archiveType": "jar",
+ "binaryName": "bsl-language-server.jar",
+ "sha256": "10835036c3b39eba9d1bbef65debc36ea99939a651165cd55ef86bf41c7e28ea"
+ }
+}
diff --git a/src/multilspy/multilspy_config.py b/src/multilspy/multilspy_config.py
index b12a8ca..d1a7e33 100644
--- a/src/multilspy/multilspy_config.py
+++ b/src/multilspy/multilspy_config.py
@@ -24,6 +24,7 @@ class Language(str, Enum):
CPP = "cpp"
PHP = "php"
ELIXIR = "elixir"
+ BSL = "bsl"
def __str__(self) -> str:
return self.value
diff --git a/tests/multilspy/test_multilspy_bsl.py b/tests/multilspy/test_multilspy_bsl.py
new file mode 100644
index 0000000..87cff49
--- /dev/null
+++ b/tests/multilspy/test_multilspy_bsl.py
@@ -0,0 +1,155 @@
+"""
+Tests for the BSL Language Server (1C:Enterprise / OneScript).
+
+BSL LS requires Java 17+ in PATH and downloads the JAR (~94 MB) on first run
+from https://github.com/1c-syntax/bsl-language-server/releases.
+
+Skipped if `java` is not available, so CI without Java will not flake.
+"""
+
+import contextlib
+import shutil
+import tempfile
+from pathlib import Path, PurePath
+from typing import Iterator
+
+import pytest
+
+from multilspy import LanguageServer
+from multilspy.multilspy_config import Language, MultilspyConfig
+from multilspy.multilspy_logger import MultilspyLogger
+
+pytest_plugins = ("pytest_asyncio",)
+
+pytestmark = pytest.mark.skipif(
+ shutil.which("java") is None,
+ reason="BSL LS requires Java 17+ in PATH",
+)
+
+
+@contextlib.contextmanager
+def create_bsl_test_project() -> Iterator[str]:
+ """Create a minimal self-contained 1C:Enterprise configuration.
+
+ Two common modules: one declares an exported function, the other calls it.
+ Configuration.xml provides the workspace root descriptor required by BSL LS.
+ """
+ with tempfile.TemporaryDirectory() as root:
+ project_dir = Path(root) / "bsl_test_project"
+ project_dir.mkdir()
+
+ # Minimal Configuration.xml root (BSL LS reads this for project layout).
+ (project_dir / "Configuration.xml").write_text(
+ '\n'
+ '\n'
+ ' \n'
+ ' \n'
+ ' TestConfig\n'
+ ' \n'
+ ' \n'
+ '\n',
+ encoding="utf-8",
+ )
+
+ util_dir = project_dir / "CommonModules" / "TestUtil" / "Ext"
+ util_dir.mkdir(parents=True)
+ (util_dir.parent / "TestUtil.xml").write_text(
+ '\n'
+ '\n'
+ ' TestUtil'
+ 'true\n'
+ '\n',
+ encoding="utf-8",
+ )
+ (util_dir / "Module.bsl").write_text(
+ "Функция ПолучитьЗначение() Экспорт\n"
+ " Возврат \"default\";\n"
+ "КонецФункции\n",
+ encoding="utf-8",
+ )
+
+ caller_dir = project_dir / "CommonModules" / "TestCaller" / "Ext"
+ caller_dir.mkdir(parents=True)
+ (caller_dir.parent / "TestCaller.xml").write_text(
+ '\n'
+ '\n'
+ ' TestCaller'
+ 'true\n'
+ '\n',
+ encoding="utf-8",
+ )
+ (caller_dir / "Module.bsl").write_text(
+ "Процедура Вызвать() Экспорт\n"
+ " Сообщить(TestUtil.ПолучитьЗначение());\n"
+ "КонецПроцедуры\n",
+ encoding="utf-8",
+ )
+
+ yield str(project_dir)
+
+
+@pytest.mark.asyncio
+async def test_multilspy_bsl_start_and_document_symbols() -> None:
+ """Server starts, initializes, and returns symbols for a .bsl file."""
+ config = MultilspyConfig(code_language=Language.BSL)
+ logger = MultilspyLogger()
+
+ with create_bsl_test_project() as project_dir:
+ lsp = LanguageServer.create(config, logger, project_dir)
+ test_file = str(
+ PurePath("CommonModules/TestUtil/Ext/Module.bsl")
+ )
+
+ async with lsp.start_server():
+ symbols, _tree = await lsp.request_document_symbols(test_file)
+ assert isinstance(symbols, list)
+ # BSL LS exposes functions/procedures as symbols of kind 12 (Function)
+ # or 6 (Method). We just assert the wiring returns structural data.
+ if symbols:
+ names = {s.get("name") for s in symbols}
+ assert "ПолучитьЗначение" in names
+
+
+@pytest.mark.asyncio
+async def test_multilspy_bsl_references_in_same_module() -> None:
+ """In-file references for a declaration work without workspace preload."""
+ config = MultilspyConfig(code_language=Language.BSL)
+ logger = MultilspyLogger()
+
+ with create_bsl_test_project() as project_dir:
+ lsp = LanguageServer.create(config, logger, project_dir)
+ util_rel = str(PurePath("CommonModules/TestUtil/Ext/Module.bsl"))
+
+ async with lsp.start_server():
+ with lsp.open_file(util_rel):
+ refs = await lsp.request_references(util_rel, 0, 10)
+ assert isinstance(refs, list)
+
+
+@pytest.mark.asyncio
+async def test_multilspy_bsl_rename_returns_workspace_edit() -> None:
+ """Raw rename RPC returns either {changes}, {documentChanges}, or None.
+
+ This is the critical integration contract for downstream refactoring tools.
+ """
+ config = MultilspyConfig(code_language=Language.BSL)
+ logger = MultilspyLogger()
+
+ with create_bsl_test_project() as project_dir:
+ lsp = LanguageServer.create(config, logger, project_dir)
+ util_rel = str(PurePath("CommonModules/TestUtil/Ext/Module.bsl"))
+ util_uri = (Path(project_dir) / util_rel).resolve().as_uri()
+
+ async with lsp.start_server():
+ with lsp.open_file(util_rel):
+ result = await lsp.server.send.rename(
+ {
+ "textDocument": {"uri": util_uri},
+ "position": {"line": 0, "character": 10},
+ "newName": "ПолучитьЗначениеНовое",
+ }
+ )
+ # Either a WorkspaceEdit dict or None (LS chose not to rename).
+ assert result is None or isinstance(result, dict)
+ if isinstance(result, dict):
+ assert "changes" in result or "documentChanges" in result