diff --git a/pyproject.toml b/pyproject.toml index e09bf4696..ec0141281 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -330,6 +330,7 @@ markers = [ "ocaml: language server running for OCaml and Reason", "scala: language server running for Scala", "al: language server running for AL (Microsoft Dynamics 365 Business Central)", + "bsl: language server running for BSL (1C:Enterprise)", "fsharp: language server running for F#", "rego: language server running for Rego", "markdown: language server running for Markdown", diff --git a/src/serena/resources/config/contexts/bsl.yml b/src/serena/resources/config/contexts/bsl.yml new file mode 100644 index 000000000..8c63bb5bf --- /dev/null +++ b/src/serena/resources/config/contexts/bsl.yml @@ -0,0 +1,21 @@ +description: Context for 1C Enterprise BSL development +prompt: | + You are working with a 1C:Enterprise codebase using BSL (Built-in Scripting Language). + Guidelines: + - Use Cyrillic-aware identifier regex: [A-Za-zА-Яа-я_][A-Za-zА-Яа-я0-9_]* + - Prefer semantic tools (find_symbol, find_referencing_symbols) for rename operations + - For cross-file operations, check forms (.xml) in addition to modules (.bsl) + - Manager modules, forms, info registers, and common modules have different + visibility semantics — consult SymbolKind before renaming + - Export methods (marked with keyword Export) are visible across modules; + module-local methods are not + - EDT project structure: src/Catalogs/*/Ext/{ManagerModule,ObjectModule,Forms/*/Ext/Form/Module}.bsl +allowed_tools: + - find_symbol + - find_referencing_symbols + - replace_symbol_body + - insert_after_symbol + - insert_before_symbol + - search_for_pattern +excluded_tools: + - execute_shell_command diff --git a/src/solidlsp/language_servers/bsl_language_server.py b/src/solidlsp/language_servers/bsl_language_server.py new file mode 100644 index 000000000..f1aebc73a --- /dev/null +++ b/src/solidlsp/language_servers/bsl_language_server.py @@ -0,0 +1,549 @@ +""" +BSL (1C:Enterprise) Language Server implementation for the SolidLSP framework. +Provides support for 1C:Enterprise and OneScript languages via BSL Language Server. +""" + +import dataclasses +import logging +import os +import pathlib + +from overrides import override + +from solidlsp.ls import SolidLanguageServer +from solidlsp.ls_config import LanguageServerConfig +from solidlsp.ls_logger import LanguageServerLogger +from solidlsp.ls_utils import FileUtils, PlatformUtils +from solidlsp.lsp_protocol_handler.lsp_types import InitializeParams +from solidlsp.lsp_protocol_handler.server import ProcessLaunchInfo +from solidlsp.settings import SolidLSPSettings + + +@dataclasses.dataclass +class BslRuntimeDependencyPaths: + """ + Stores the paths to the runtime dependencies of BSL Language Server + """ + + java_path: str + java_home_path: str + bsl_jar_path: str + + +class BslLanguageServer(SolidLanguageServer): + """ + BSL Language Server for 1C:Enterprise and OneScript. + Provides comprehensive language support including diagnostics, navigation, + code completion, and refactoring for BSL (1C:Enterprise script language). + """ + + def __init__( + self, + config: LanguageServerConfig, + logger: LanguageServerLogger, + repository_root_path: str, + solidlsp_settings: SolidLSPSettings, + ): + """ + Creates a BSL Language Server instance. + This class is not meant to be instantiated directly. + Use LanguageServer.create() instead. + """ + runtime_dependency_paths = self._setup_runtime_dependencies(logger, config, solidlsp_settings) + self.runtime_dependency_paths = runtime_dependency_paths + + # Create command to execute the BSL Language Server JAR + cmd = [ + self.runtime_dependency_paths.java_path, + "-jar", + self.runtime_dependency_paths.bsl_jar_path, + # BSL Language Server uses stdio by default, no additional parameters needed + ] + + # Set environment variables including JAVA_HOME + proc_env = {"JAVA_HOME": self.runtime_dependency_paths.java_home_path} + + super().__init__( + config, + logger, + repository_root_path, + ProcessLaunchInfo(cmd=cmd, env=proc_env, cwd=repository_root_path), + "bsl", + solidlsp_settings, + ) + + @override + def is_ignored_dirname(self, dirname: str) -> bool: + """ + Ignore directories that are typically not part of 1C source code. + """ + return super().is_ignored_dirname(dirname) or dirname in [ + "out", # 1C output directory + "bin", # Binary files + "ConfigDumpInfo", # Configuration dump + "DT-INF", # 1C Designer info + ".git", # Git repository + ".vscode", # VSCode settings + ".idea", # IntelliJ IDEA settings + ] + + @classmethod + def _setup_runtime_dependencies( + cls, + logger: LanguageServerLogger, + config: LanguageServerConfig, + solidlsp_settings: SolidLSPSettings, + ) -> BslRuntimeDependencyPaths: + """ + Setup runtime dependencies for BSL Language Server and return the paths. + Downloads Java runtime and BSL Language Server JAR if not present. + """ + platform_id = PlatformUtils.get_platform_id() + + # Verify platform support + assert platform_id.value.startswith("win-") or platform_id.value.startswith("linux-") or platform_id.value.startswith("osx-"), ( + "Only Windows, Linux and macOS platforms are supported for BSL" + ) + + # Runtime dependency information + # Using latest stable version from https://github.com/1c-syntax/bsl-language-server/releases + runtime_dependencies = { + "bsl_server": { + "id": "BslLanguageServer", + "description": "BSL Language Server for 1C:Enterprise", + "url": "https://github.com/1c-syntax/bsl-language-server/releases/download/v0.24.0-rc.3/bsl-language-server-0.24.0-rc.3-exec.jar", + "archiveType": "jar", # Direct JAR download, no extraction needed + }, + "java": { + "win-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-win32-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-win32-x86_64", + "java_path": "extension/jre/21.0.7-win32-x86_64/bin/java.exe", + }, + "linux-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-linux-x86_64", + "java_path": "extension/jre/21.0.7-linux-x86_64/bin/java", + }, + "linux-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-linux-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-linux-aarch64", + "java_path": "extension/jre/21.0.7-linux-aarch64/bin/java", + }, + "osx-x64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-x64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-macosx-x86_64", + "java_path": "extension/jre/21.0.7-macosx-x86_64/bin/java", + }, + "osx-arm64": { + "url": "https://github.com/redhat-developer/vscode-java/releases/download/v1.42.0/java-darwin-arm64-1.42.0-561.vsix", + "archiveType": "zip", + "java_home_path": "extension/jre/21.0.7-macosx-aarch64", + "java_path": "extension/jre/21.0.7-macosx-aarch64/bin/java", + }, + }, + } + + bsl_dependency = runtime_dependencies["bsl_server"] + java_dependency = runtime_dependencies["java"][platform_id.value] + + # Setup paths for dependencies + static_dir = os.path.join(cls.ls_resources_dir(solidlsp_settings), "bsl_language_server") + os.makedirs(static_dir, exist_ok=True) + + # Setup Java paths + java_dir = os.path.join(static_dir, "java") + os.makedirs(java_dir, exist_ok=True) + + java_home_path = os.path.join(java_dir, java_dependency["java_home_path"]) + java_path = os.path.join(java_dir, java_dependency["java_path"]) + + # Download and extract Java if not exists + if not os.path.exists(java_path): + logger.log(f"Downloading Java for {platform_id.value}...", logging.INFO) + FileUtils.download_and_extract_archive( + logger, + java_dependency["url"], + java_dir, + java_dependency["archiveType"], + ) + # Make Java executable on Unix platforms + if not platform_id.value.startswith("win-"): + os.chmod(java_path, 0o755) + + assert os.path.exists(java_path), f"Java executable not found at {java_path}" + + # Setup BSL Language Server JAR path + bsl_jar_path = os.path.join(static_dir, "bsl-language-server.jar") + + # Download BSL Language Server JAR if not exists + if not os.path.exists(bsl_jar_path): + logger.log("Downloading BSL Language Server JAR...", logging.INFO) + # Direct download of JAR file + import urllib.request + + urllib.request.urlretrieve(bsl_dependency["url"], bsl_jar_path) + logger.log(f"BSL Language Server downloaded to {bsl_jar_path}", logging.INFO) + + assert os.path.exists(bsl_jar_path), f"BSL Language Server JAR not found at {bsl_jar_path}" + + return BslRuntimeDependencyPaths( + java_path=java_path, + java_home_path=java_home_path, + bsl_jar_path=bsl_jar_path, + ) + + @staticmethod + def _get_initialize_params(repository_absolute_path: str) -> InitializeParams: + """ + Returns the initialize params for the BSL Language Server. + """ + if not os.path.isabs(repository_absolute_path): + repository_absolute_path = os.path.abspath(repository_absolute_path) + + root_uri = pathlib.Path(repository_absolute_path).as_uri() + + initialize_params: InitializeParams = { + "processId": os.getpid(), + "rootPath": repository_absolute_path, + "rootUri": root_uri, + "initializationOptions": { + # BSL Language Server specific options + "diagnosticLanguage": "ru", # Use Russian for diagnostics + "showCognitiveComplexityCodeLens": True, + "showCyclomaticComplexityCodeLens": True, + "computeDiagnostics": "onType", # or "onSave" + "traceLog": False, + "configurationPath": "", # Path to .bsl-language-server.json if needed + }, + "capabilities": { + "workspace": { + "applyEdit": True, + "workspaceEdit": { + "documentChanges": True, + "resourceOperations": ["create", "rename", "delete"], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": True, + "changeAnnotationSupport": {"groupsOnLabel": True}, + }, + "didChangeConfiguration": {"dynamicRegistration": True}, + "didChangeWatchedFiles": { + "dynamicRegistration": True, + "relativePatternSupport": True, + }, + "symbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "tagSupport": {"valueSet": [1]}, + "resolveSupport": {"properties": ["location.range"]}, + }, + "codeLens": {"refreshSupport": True}, + "executeCommand": {"dynamicRegistration": True}, + "configuration": True, + "workspaceFolders": True, + "semanticTokens": {"refreshSupport": True}, + "fileOperations": { + "dynamicRegistration": True, + "didCreate": True, + "didRename": True, + "didDelete": True, + "willCreate": True, + "willRename": True, + "willDelete": True, + }, + "inlineValue": {"refreshSupport": True}, + "inlayHint": {"refreshSupport": True}, + "diagnostics": {"refreshSupport": True}, + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": True, + "versionSupport": False, + "tagSupport": {"valueSet": [1, 2]}, + "codeDescriptionSupport": True, + "dataSupport": True, + }, + "synchronization": { + "dynamicRegistration": True, + "willSave": True, + "willSaveWaitUntil": True, + "didSave": True, + }, + "completion": { + "dynamicRegistration": True, + "contextSupport": True, + "completionItem": { + "snippetSupport": True, + "commitCharactersSupport": True, + "documentationFormat": ["markdown", "plaintext"], + "deprecatedSupport": True, + "preselectSupport": True, + "tagSupport": {"valueSet": [1]}, + "insertReplaceSupport": False, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits", + ] + }, + "insertTextModeSupport": {"valueSet": [1, 2]}, + "labelDetailsSupport": True, + }, + "insertTextMode": 2, + "completionItemKind": {"valueSet": list(range(1, 26))}, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode", + ] + }, + }, + "hover": { + "dynamicRegistration": True, + "contentFormat": ["markdown", "plaintext"], + }, + "signatureHelp": { + "dynamicRegistration": True, + "signatureInformation": { + "documentationFormat": ["markdown", "plaintext"], + "parameterInformation": {"labelOffsetSupport": True}, + "activeParameterSupport": True, + }, + "contextSupport": True, + }, + "definition": {"dynamicRegistration": True, "linkSupport": True}, + "references": {"dynamicRegistration": True}, + "documentHighlight": {"dynamicRegistration": True}, + "documentSymbol": { + "dynamicRegistration": True, + "symbolKind": {"valueSet": list(range(1, 27))}, + "hierarchicalDocumentSymbolSupport": True, + "tagSupport": {"valueSet": [1]}, + "labelSupport": True, + }, + "codeAction": { + "dynamicRegistration": True, + "isPreferredSupport": True, + "disabledSupport": True, + "dataSupport": True, + "resolveSupport": {"properties": ["edit"]}, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports", + ] + } + }, + "honorsChangeAnnotations": False, + }, + "codeLens": {"dynamicRegistration": True}, + "formatting": {"dynamicRegistration": True}, + "rangeFormatting": {"dynamicRegistration": True}, + "onTypeFormatting": {"dynamicRegistration": True}, + "rename": { + "dynamicRegistration": True, + "prepareSupport": True, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": True, + }, + "documentLink": { + "dynamicRegistration": True, + "tooltipSupport": True, + }, + "typeDefinition": { + "dynamicRegistration": True, + "linkSupport": True, + }, + "implementation": { + "dynamicRegistration": True, + "linkSupport": True, + }, + "colorProvider": {"dynamicRegistration": True}, + "foldingRange": { + "dynamicRegistration": True, + "rangeLimit": 5000, + "lineFoldingOnly": True, + "foldingRangeKind": {"valueSet": ["comment", "imports", "region"]}, + "foldingRange": {"collapsedText": False}, + }, + "declaration": { + "dynamicRegistration": True, + "linkSupport": True, + }, + "selectionRange": {"dynamicRegistration": True}, + "callHierarchy": {"dynamicRegistration": True}, + "semanticTokens": { + "dynamicRegistration": True, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator", + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary", + ], + "formats": ["relative"], + "requests": {"range": True, "full": {"delta": True}}, + "multilineTokenSupport": False, + "overlappingTokenSupport": False, + "serverCancelSupport": True, + "augmentsSyntaxTokens": True, + }, + "linkedEditingRange": {"dynamicRegistration": True}, + "typeHierarchy": {"dynamicRegistration": True}, + "inlineValue": {"dynamicRegistration": True}, + "inlayHint": { + "dynamicRegistration": True, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command", + ] + }, + }, + "diagnostic": { + "dynamicRegistration": True, + "relatedDocumentSupport": False, + }, + }, + "window": { + "showMessage": {"messageActionItem": {"additionalPropertiesSupport": True}}, + "showDocument": {"support": True}, + "workDoneProgress": True, + }, + "general": { + "staleRequestSupport": { + "cancel": True, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta", + ], + }, + "regularExpressions": {"engine": "ECMAScript", "version": "ES2020"}, + "markdown": {"parser": "marked", "version": "1.1.0"}, + "positionEncodings": ["utf-16"], + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": True, + "executionSummarySupport": True, + } + }, + }, + "trace": "verbose", + "workspaceFolders": [ + { + "uri": root_uri, + "name": os.path.basename(repository_absolute_path), + } + ], + } + + return initialize_params + + def _start_server(self): + """ + Starts the BSL Language Server + """ + + def do_nothing(params): + return + + def window_log_message(msg): + self.logger.log(f"LSP: window/logMessage: {msg}", logging.INFO) + + # Register common notification handlers + self.server.on_request("client/registerCapability", do_nothing) + self.server.on_notification("language/status", do_nothing) + self.server.on_notification("window/logMessage", window_log_message) + self.server.on_notification("$/progress", do_nothing) + self.server.on_notification("textDocument/publishDiagnostics", do_nothing) + + self.logger.log("Starting BSL Language Server process", logging.INFO) + self.server.start() + + initialize_params = self._get_initialize_params(self.repository_root_path) + + self.logger.log( + "Sending initialize request from LSP client to BSL Language Server and awaiting response", + logging.INFO, + ) + + init_response = self.server.send.initialize(initialize_params) + + # Verify required capabilities + capabilities = init_response.get("capabilities", {}) + + # BSL Language Server should support these basic features + assert "textDocumentSync" in capabilities, "Server must support textDocumentSync" + assert "definitionProvider" in capabilities, "Server must support go to definition" + assert "referencesProvider" in capabilities, "Server must support find references" + assert "documentSymbolProvider" in capabilities, "Server must support document symbols" + + # Optional but useful capabilities + if "hoverProvider" in capabilities: + self.logger.log("Hover support available", logging.INFO) + + if "completionProvider" in capabilities: + self.logger.log("Code completion support available", logging.INFO) + + if "signatureHelpProvider" in capabilities: + self.logger.log("Signature help support available", logging.INFO) + + if "workspaceSymbolProvider" in capabilities: + self.logger.log("Workspace symbols support available", logging.INFO) + + # Notify server that initialization is complete + self.server.notify.initialized({}) + + # BSL Language Server is ready + self.completions_available.set() + + self.logger.log("BSL Language Server initialized successfully", logging.INFO) diff --git a/src/solidlsp/ls_config.py b/src/solidlsp/ls_config.py index ff1378944..8f0bccfdb 100644 --- a/src/solidlsp/ls_config.py +++ b/src/solidlsp/ls_config.py @@ -63,6 +63,7 @@ class Language(str, Enum): ERLANG = "erlang" OCAML = "ocaml" AL = "al" + BSL = "bsl" # 1C:Enterprise and OneScript language FSHARP = "fsharp" REGO = "rego" SCALA = "scala" @@ -268,6 +269,8 @@ def get_source_fn_matcher(self) -> FilenameMatcher: return FilenameMatcher("*.ml", "*.mli", "*.re", "*.rei") case self.AL: return FilenameMatcher("*.al", "*.dal") + case self.BSL: + return FilenameMatcher("*.bsl", "*.os", "*.bsl") case self.FSHARP: return FilenameMatcher("*.fs", "*.fsx", "*.fsi") case self.REGO: @@ -492,6 +495,10 @@ def get_ls_class(self) -> type["SolidLanguageServer"]: from solidlsp.language_servers.r_language_server import RLanguageServer return RLanguageServer + case self.BSL: + from solidlsp.language_servers.bsl_language_server import BslLanguageServer + + return BslLanguageServer case self.SCALA: from solidlsp.language_servers.scala_language_server import ScalaLanguageServer diff --git "a/test/resources/repos/bsl/test_repo/\320\236\320\261\321\211\320\270\320\271\320\234\320\276\320\264\321\203\320\273\321\214.bsl" "b/test/resources/repos/bsl/test_repo/\320\236\320\261\321\211\320\270\320\271\320\234\320\276\320\264\321\203\320\273\321\214.bsl" new file mode 100644 index 000000000..c8b854042 --- /dev/null +++ "b/test/resources/repos/bsl/test_repo/\320\236\320\261\321\211\320\270\320\271\320\234\320\276\320\264\321\203\320\273\321\214.bsl" @@ -0,0 +1,161 @@ +// Общий модуль с утилитами + +#Область СтроковыеФункции + +// Функция проверки заполненности строки +Функция СтрокаЗаполнена(Значение) Экспорт + Возврат ЗначениеЗаполнено(Значение) И НЕ ПустаяСтрока(СокрЛП(Значение)); +КонецФункции + +// Замена подстроки в строке +Функция ЗаменитьПодстроку(ИсходнаяСтрока, Подстрока, НоваяПодстрока) Экспорт + Возврат СтрЗаменить(ИсходнаяСтрока, Подстрока, НоваяПодстрока); +КонецФункции + +// Разделение строки на массив +Функция РазделитьСтроку(Строка, Разделитель = ",") Экспорт + МассивСтрок = Новый Массив; + + Если НЕ СтрокаЗаполнена(Строка) Тогда + Возврат МассивСтрок; + КонецЕсли; + + НачальнаяПозиция = 1; + ДлинаСтроки = СтрДлина(Строка); + ДлинаРазделителя = СтрДлина(Разделитель); + + Пока НачальнаяПозиция <= ДлинаСтроки Цикл + ПозицияРазделителя = СтрНайти(Строка, Разделитель, НачальнаяПозиция); + + Если ПозицияРазделителя = 0 Тогда + ПодСтрока = Сред(Строка, НачальнаяПозиция); + МассивСтрок.Добавить(СокрЛП(ПодСтрока)); + Прервать; + Иначе + ПодСтрока = Сред(Строка, НачальнаяПозиция, ПозицияРазделителя - НачальнаяПозиция); + МассивСтрок.Добавить(СокрЛП(ПодСтрока)); + НачальнаяПозиция = ПозицияРазделителя + ДлинаРазделителя; + КонецЕсли; + КонецЦикла; + + Возврат МассивСтрок; +КонецФункции + +#КонецОбласти + +#Область РаботаСМассивами + +// Функция поиска в массиве +Функция НайтиВМассиве(Массив, ИскомоеЗначение) Экспорт + Для Индекс = 0 По Массив.ВГраница() Цикл + Если Массив[Индекс] = ИскомоеЗначение Тогда + Возврат Индекс; + КонецЕсли; + КонецЦикла; + + Возврат -1; +КонецФункции + +// Удаление дублей из массива +Функция УдалитьДубли(Массив) Экспорт + УникальныйМассив = Новый Массив; + + Для Каждого Элемент Из Массив Цикл + Если УникальныйМассив.Найти(Элемент) = Неопределено Тогда + УникальныйМассив.Добавить(Элемент); + КонецЕсли; + КонецЦикла; + + Возврат УникальныйМассив; +КонецФункции + +// Объединение массивов +Функция ОбъединитьМассивы(Массив1, Массив2) Экспорт + РезультирующийМассив = Новый Массив; + + Для Каждого Элемент Из Массив1 Цикл + РезультирующийМассив.Добавить(Элемент); + КонецЦикла; + + Для Каждого Элемент Из Массив2 Цикл + РезультирующийМассив.Добавить(Элемент); + КонецЦикла; + + Возврат РезультирующийМассив; +КонецФункции + +#КонецОбласти + +#Область РаботаСДатами + +// Получение начала дня +Функция НачалоДня(Дата) Экспорт + Возврат НачалоДня(Дата); +КонецФункции + +// Получение конца дня +Функция КонецДня(Дата) Экспорт + Возврат КонецДня(Дата); +КонецФункции + +// Рабочие дни между датами +Функция КоличествоРабочихДней(ДатаНачала, ДатаОкончания) Экспорт + КоличествоДней = 0; + ТекущаяДата = ДатаНачала; + + Пока ТекущаяДата <= ДатаОкончания Цикл + ДеньНедели = ДеньНедели(ТекущаяДата); + Если ДеньНедели >= 1 И ДеньНедели <= 5 Тогда // Понедельник - Пятница + КоличествоДней = КоличествоДней + 1; + КонецЕсли; + ТекущаяДата = ТекущаяДата + 86400; // +1 день в секундах + КонецЦикла; + + Возврат КоличествоДней; +КонецФункции + +#КонецОбласти + +#Область Валидация + +// Проверка ИНН +Функция ПроверитьИНН(ИНН) Экспорт + ИНН = СокрЛП(ИНН); + + Если НЕ СтрокаЗаполнена(ИНН) Тогда + Возврат Ложь; + КонецЕсли; + + ДлинаИНН = СтрДлина(ИНН); + Если ДлинаИНН <> 10 И ДлинаИНН <> 12 Тогда + Возврат Ложь; + КонецЕсли; + + // Проверка, что все символы - цифры + Для Позиция = 1 По ДлинаИНН Цикл + Символ = Сред(ИНН, Позиция, 1); + Если НЕ (Символ >= "0" И Символ <= "9") Тогда + Возврат Ложь; + КонецЕсли; + КонецЦикла; + + // Здесь может быть реализована проверка контрольных сумм + + Возврат Истина; +КонецФункции + +// Проверка email +Функция ПроверитьEmail(Email) Экспорт + Email = СокрЛП(Email); + + Если НЕ СтрокаЗаполнена(Email) Тогда + Возврат Ложь; + КонецЕсли; + + ПозицияСобаки = СтрНайти(Email, "@"); + ПозицияТочки = СтрНайти(Email, ".", ПозицияСобаки); + + Возврат ПозицияСобаки > 1 И ПозицияТочки > ПозицияСобаки + 1 И ПозицияТочки < СтрДлина(Email); +КонецФункции + +#КонецОбласти \ No newline at end of file diff --git "a/test/resources/repos/bsl/test_repo/\320\236\321\201\320\275\320\276\320\262\320\275\320\276\320\271\320\234\320\276\320\264\321\203\320\273\321\214.bsl" "b/test/resources/repos/bsl/test_repo/\320\236\321\201\320\275\320\276\320\262\320\275\320\276\320\271\320\234\320\276\320\264\321\203\320\273\321\214.bsl" new file mode 100644 index 000000000..3c873eff1 --- /dev/null +++ "b/test/resources/repos/bsl/test_repo/\320\236\321\201\320\275\320\276\320\262\320\275\320\276\320\271\320\234\320\276\320\264\321\203\320\273\321\214.bsl" @@ -0,0 +1,174 @@ +// Главный модуль с примерами кода 1С:Предприятие + +#Область ОписаниеПеременных + +Перем глСчетчик Экспорт; +Перем глМассивДанных; +Перем глСоединение; + +#КонецОбласти + +#Область ПроцедурыИФункции + +// Основная процедура инициализации +Процедура ИнициализацияСистемы() Экспорт + глСчетчик = 0; + глМассивДанных = Новый Массив; + + // Заполнение массива тестовыми данными + Для Индекс = 1 По 10 Цикл + глМассивДанных.Добавить("Элемент_" + Строка(Индекс)); + КонецЦикла; + + Сообщить("Система инициализирована"); +КонецПроцедуры + +// Функция расчета суммы +Функция РассчитатьСумму(Число1, Число2) Экспорт + Перем Результат; + + Результат = Число1 + Число2; + + Возврат Результат; +КонецФункции + +// Процедура обработки документов +Процедура ОбработатьДокумент(Документ, ТипОбработки = "Стандартная") Экспорт + + Если ТипОбработки = "Стандартная" Тогда + ВыполнитьСтандартнуюОбработку(Документ); + ИначеЕсли ТипОбработки = "Специальная" Тогда + ВыполнитьСпециальнуюОбработку(Документ); + Иначе + ВызватьИсключение "Неизвестный тип обработки: " + ТипОбработки; + КонецЕсли; + +КонецПроцедуры + +// Стандартная обработка документа +Процедура ВыполнитьСтандартнуюОбработку(Документ) + Документ.Дата = ТекущаяДата(); + Документ.Статус = "Обработан"; + + Попытка + Документ.Записать(); + глСчетчик = глСчетчик + 1; + Исключение + ЗаписатьОшибку(ОписаниеОшибки()); + КонецПопытки; +КонецПроцедуры + +// Специальная обработка документа +Процедура ВыполнитьСпециальнуюОбработку(Документ) + // Проверка реквизитов + Если НЕ ЗначениеЗаполнено(Документ.Контрагент) Тогда + ВызватьИсключение "Не заполнен контрагент"; + КонецЕсли; + + // Формирование движений + НаборДвижений = Документ.Движения.ТоварыНаСкладах; + НаборДвижений.Очистить(); + + Для Каждого СтрокаТЧ Из Документ.Товары Цикл + Движение = НаборДвижений.Добавить(); + Движение.Период = Документ.Дата; + Движение.Номенклатура = СтрокаТЧ.Номенклатура; + Движение.Количество = СтрокаТЧ.Количество; + КонецЦикла; + + НаборДвижений.Записать(); +КонецПроцедуры + +// Запись ошибки в журнал +Процедура ЗаписатьОшибку(ТекстОшибки) + ЗаписьЖурналаРегистрации("Обработка.Ошибка", + УровеньЖурналаРегистрации.Ошибка,,, + ТекстОшибки); +КонецПроцедуры + +#КонецОбласти + +#Область РаботаСДанными + +// Получение данных из базы +Функция ПолучитьДанныеИзБазы(ПараметрыОтбора) Экспорт + Запрос = Новый Запрос; + Запрос.Текст = " + |ВЫБРАТЬ + | Товары.Ссылка КАК Ссылка, + | Товары.Наименование КАК Наименование, + | Товары.Артикул КАК Артикул, + | ВЫБОР + | КОГДА Товары.ВидНоменклатуры = &ВидТовар + | ТОГДА ""Товар"" + | КОГДА Товары.ВидНоменклатуры = &ВидУслуга + | ТОГДА ""Услуга"" + | ИНАЧЕ ""Неопределено"" + | КОНЕЦ КАК ТипНоменклатуры + |ИЗ + | Справочник.Номенклатура КАК Товары + |ГДЕ + | Товары.ПометкаУдаления = ЛОЖЬ + | И Товары.Родитель = &Родитель + |УПОРЯДОЧИТЬ ПО + | Наименование"; + + Запрос.УстановитьПараметр("ВидТовар", Перечисления.ВидыНоменклатуры.Товар); + Запрос.УстановитьПараметр("ВидУслуга", Перечисления.ВидыНоменклатуры.Услуга); + Запрос.УстановитьПараметр("Родитель", ПараметрыОтбора.Родитель); + + Результат = Запрос.Выполнить(); + Возврат Результат.Выгрузить(); +КонецФункции + +// Обработка табличных данных +Функция ОбработатьТаблицу(ТаблицаДанных) Экспорт + НоваяТаблица = ТаблицаДанных.Скопировать(); + + // Добавление новой колонки + НоваяТаблица.Колонки.Добавить("СуммаСНДС", Новый ОписаниеТипов("Число")); + + // Расчет суммы с НДС + Для Каждого Строка Из НоваяТаблица Цикл + Строка.СуммаСНДС = Строка.Сумма * 1.2; // 20% НДС + КонецЦикла; + + Возврат НоваяТаблица; +КонецФункции + +#КонецОбласти + +#Область СлужебныеПроцедурыИФункции + +// Проверка прав доступа +Функция ПроверитьПраваДоступа(Пользователь, Операция) + + Если Пользователь.Роль = "Администратор" Тогда + Возврат Истина; + КонецЕсли; + + Если Операция = "Чтение" И Пользователь.Роль = "Пользователь" Тогда + Возврат Истина; + КонецЕсли; + + Возврат Ложь; + +КонецФункции + +// Асинхронная обработка +&НаКлиенте +Процедура ВыполнитьАсинхронно(Команда) + ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВыполнения", ЭтотОбъект); + НачатьВыполнениеОперации(ОписаниеОповещения); +КонецПроцедуры + +&НаКлиенте +Процедура ПослеВыполнения(Результат, ДополнительныеПараметры) Экспорт + Если Результат.Статус = "Успешно" Тогда + Сообщить("Операция выполнена успешно"); + Иначе + Сообщить("Ошибка: " + Результат.ОписаниеОшибки); + КонецЕсли; +КонецПроцедуры + +#КонецОбласти \ No newline at end of file diff --git "a/test/resources/repos/bsl/test_repo/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\276\320\241\320\277\321\200\320\260\320\262\320\276\321\207\320\275\320\270\320\272\320\260\320\274\320\270.bsl" "b/test/resources/repos/bsl/test_repo/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\276\320\241\320\277\321\200\320\260\320\262\320\276\321\207\320\275\320\270\320\272\320\260\320\274\320\270.bsl" new file mode 100644 index 000000000..b37dd9f89 --- /dev/null +++ "b/test/resources/repos/bsl/test_repo/\320\240\320\260\320\261\320\276\321\202\320\260\320\241\320\276\320\241\320\277\321\200\320\260\320\262\320\276\321\207\320\275\320\270\320\272\320\260\320\274\320\270.bsl" @@ -0,0 +1,97 @@ +// Модуль для работы со справочниками + +#Область ПубличныеМетоды + +// Создание нового элемента справочника +Функция СоздатьНовыйЭлемент(ИмяСправочника, РеквизитыЭлемента) Экспорт + НовыйЭлемент = Справочники[ИмяСправочника].СоздатьЭлемент(); + + Для Каждого КлючИЗначение Из РеквизитыЭлемента Цикл + НовыйЭлемент[КлючИЗначение.Ключ] = КлючИЗначение.Значение; + КонецЦикла; + + Попытка + НовыйЭлемент.Записать(); + Возврат НовыйЭлемент.Ссылка; + Исключение + ВызватьИсключение "Не удалось создать элемент: " + ОписаниеОшибки(); + КонецПопытки; +КонецФункции + +// Поиск элемента по наименованию +Функция НайтиПоНаименованию(ИмяСправочника, Наименование) Экспорт + Запрос = Новый Запрос; + Запрос.Текст = СтрШаблон(" + |ВЫБРАТЬ ПЕРВЫЕ 1 + | Элемент.Ссылка КАК Ссылка + |ИЗ + | Справочник.%1 КАК Элемент + |ГДЕ + | Элемент.Наименование = &Наименование", + ИмяСправочника); + + Запрос.УстановитьПараметр("Наименование", Наименование); + Выборка = Запрос.Выполнить().Выбрать(); + + Если Выборка.Следующий() Тогда + Возврат Выборка.Ссылка; + Иначе + Возврат Неопределено; + КонецЕсли; +КонецФункции + +// Получение иерархии элементов +Функция ПолучитьИерархию(СправочникСсылка) Экспорт + МассивРодителей = Новый Массив; + ТекущийЭлемент = СправочникСсылка; + + Пока ЗначениеЗаполнено(ТекущийЭлемент.Родитель) Цикл + МассивРодителей.Вставить(0, ТекущийЭлемент.Родитель); + ТекущийЭлемент = ТекущийЭлемент.Родитель; + КонецЦикла; + + Возврат МассивРодителей; +КонецФункции + +#КонецОбласти + +#Область СлужебныеМетоды + +// Проверка уникальности кода +Функция КодУникален(ИмяСправочника, Код, ИсключаяСсылку = Неопределено) + Запрос = Новый Запрос; + Запрос.Текст = СтрШаблон(" + |ВЫБРАТЬ ПЕРВЫЕ 1 + | 1 КАК Есть + |ИЗ + | Справочник.%1 КАК Справочник + |ГДЕ + | Справочник.Код = &Код + | И Справочник.Ссылка <> &ИсключаяСсылку", + ИмяСправочника); + + Запрос.УстановитьПараметр("Код", Код); + Запрос.УстановитьПараметр("ИсключаяСсылку", ИсключаяСсылку); + + Возврат Запрос.Выполнить().Пустой(); +КонецФункции + +// Получение следующего кода +Функция ПолучитьСледующийКод(ИмяСправочника) + Запрос = Новый Запрос; + Запрос.Текст = СтрШаблон(" + |ВЫБРАТЬ + | МАКСИМУМ(Справочник.Код) КАК МаксимальныйКод + |ИЗ + | Справочник.%1 КАК Справочник", + ИмяСправочника); + + Выборка = Запрос.Выполнить().Выбрать(); + Если Выборка.Следующий() И ЗначениеЗаполнено(Выборка.МаксимальныйКод) Тогда + Возврат Формат(Число(Выборка.МаксимальныйКод) + 1, "ЧЦ=9; ЧВН=; ЧГ=0"); + Иначе + Возврат "000000001"; + КонецЕсли; +КонецФункции + +#КонецОбласти \ No newline at end of file diff --git a/test/solidlsp/bsl/test_bsl_basic.py b/test/solidlsp/bsl/test_bsl_basic.py new file mode 100644 index 000000000..609452dad --- /dev/null +++ b/test/solidlsp/bsl/test_bsl_basic.py @@ -0,0 +1,142 @@ +""" +Basic integration tests for the BSL Language Server functionality. + +These tests validate the functionality of the BSL language server APIs +like request_references using the test repository for 1C:Enterprise code. +""" + +import os + +import pytest + +from solidlsp import SolidLanguageServer +from solidlsp.ls_config import Language + + +@pytest.mark.bsl +class TestBslLanguageServerBasics: + """Test basic functionality of the BSL language server.""" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_language_server_initialization(self, language_server: SolidLanguageServer) -> None: + """Test that BSL language server initializes correctly.""" + assert language_server is not None + assert language_server.language == "bsl" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None: + """Test request_document_symbols on BSL files.""" + file_path = os.path.join("test_repo", "ОсновнойМодуль.bsl") + symbols = language_server.request_document_symbols(file_path) + + # Verify that we get symbols from the BSL file + assert len(symbols) > 0, "Should find symbols in BSL file" + + # Look for main functions that should be present + symbol_names = [] + for symbol_group in symbols: + for symbol in symbol_group: + symbol_names.append(symbol.get("name", "")) + + # Check for some expected function names from our test file + expected_functions = ["ИнициализацияСистемы", "РассчитатьСумму", "ОбработатьДокумент"] + found_functions = [name for name in expected_functions if any(name in sym_name for sym_name in symbol_names)] + + assert len(found_functions) > 0, f"Should find at least one of expected functions: {expected_functions}" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_request_references_function(self, language_server: SolidLanguageServer) -> None: + """Test request_references on a BSL function.""" + file_path = os.path.join("test_repo", "ОсновнойМодуль.bsl") + + # Try to get document symbols first + symbols = language_server.request_document_symbols(file_path) + + if len(symbols) > 0: + # Look for a function symbol + function_symbol = None + for symbol_group in symbols: + for symbol in symbol_group: + if symbol.get("kind") == 12: # Function kind in LSP + function_symbol = symbol + break + if function_symbol: + break + + if function_symbol and "selectionRange" in function_symbol: + sel_start = function_symbol["selectionRange"]["start"] + # This might not find references if the function isn't called elsewhere + # but at least we test that the request doesn't crash + references = language_server.request_references(file_path, sel_start["line"], sel_start["character"]) + assert isinstance(references, list), "References should be returned as a list" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_request_hover(self, language_server: SolidLanguageServer) -> None: + """Test hover functionality on BSL code.""" + file_path = os.path.join("test_repo", "ОсновнойМодуль.bsl") + + # Try hover on the first line that likely contains a function or variable + try: + hover_info = language_server.request_hover(file_path, 10, 10) # Arbitrary position + # Hover might return None if no info available, which is fine + assert hover_info is None or isinstance(hover_info, dict) + except Exception: + # Some language servers might not support hover, which is acceptable + pytest.skip("Hover not supported by BSL language server") + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_multiple_bsl_files(self, language_server: SolidLanguageServer) -> None: + """Test that language server can handle multiple BSL files.""" + # Test our main module + file_path1 = os.path.join("test_repo", "ОсновнойМодуль.bsl") + symbols1 = language_server.request_document_symbols(file_path1) + + # Test our helper module + file_path2 = os.path.join("test_repo", "ОбщийМодуль.bsl") + symbols2 = language_server.request_document_symbols(file_path2) + + # Both files should return symbols + assert len(symbols1) > 0, "Main module should have symbols" + assert len(symbols2) > 0, "Common module should have symbols" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_bsl_file_extensions(self, language_server: SolidLanguageServer) -> None: + """Test that BSL language server works with different BSL file extensions.""" + # The language server should recognize .bsl files + + # Verify the file is recognized as a BSL file + language_config = Language.BSL + matcher = language_config.get_source_fn_matcher() + + assert matcher.is_relevant_filename("test.bsl"), "Should recognize .bsl files" + assert matcher.is_relevant_filename("test.os"), "Should recognize .os files" + assert not matcher.is_relevant_filename("test.py"), "Should not recognize .py files" + + +@pytest.mark.bsl +class TestBslLanguageServerAdvanced: + """Test advanced functionality of the BSL language server.""" + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_workspace_symbols(self, language_server: SolidLanguageServer) -> None: + """Test workspace symbol search across BSL files.""" + try: + # Search for functions across the workspace + symbols = language_server.request_workspace_symbols("Функция") + assert isinstance(symbols, list), "Workspace symbols should return a list" + except Exception: + # Workspace symbols might not be implemented + pytest.skip("Workspace symbols not supported by BSL language server") + + @pytest.mark.parametrize("language_server", [Language.BSL], indirect=True) + def test_definition_lookup(self, language_server: SolidLanguageServer) -> None: + """Test go-to-definition functionality.""" + file_path = os.path.join("test_repo", "ОсновнойМодуль.bsl") + + try: + # Try to get definition for a symbol (this might not work without proper references) + definition = language_server.request_definition(file_path, 20, 10) + assert definition is None or isinstance(definition, list) + except Exception: + # Definition lookup might not be fully implemented + pytest.skip("Definition lookup not supported by BSL language server")