diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/LanguageServerConfiguration.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/LanguageServerConfiguration.java index 12826196162..63844d7226c 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/LanguageServerConfiguration.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/LanguageServerConfiguration.java @@ -33,6 +33,7 @@ import com.github._1c_syntax.bsl.languageserver.configuration.formating.FormattingOptions; import com.github._1c_syntax.bsl.languageserver.configuration.inlayhints.InlayHintOptions; import com.github._1c_syntax.bsl.languageserver.configuration.references.ReferencesOptions; +import com.github._1c_syntax.bsl.languageserver.configuration.semantictokens.SemanticTokensOptions; import com.github._1c_syntax.utils.Absolute; import jakarta.annotation.PostConstruct; import lombok.AccessLevel; @@ -102,6 +103,10 @@ public class LanguageServerConfiguration { @Setter(value = AccessLevel.NONE) private ReferencesOptions referencesOptions = new ReferencesOptions(); + @JsonProperty("semanticTokens") + @Setter(value = AccessLevel.NONE) + private SemanticTokensOptions semanticTokensOptions = new SemanticTokensOptions(); + private String siteRoot = "https://1c-syntax.github.io/bsl-language-server"; private boolean useDevSite; @@ -217,5 +222,6 @@ private void copyPropertiesFrom(LanguageServerConfiguration configuration) { PropertyUtils.copyProperties(this.documentLinkOptions, configuration.documentLinkOptions); PropertyUtils.copyProperties(this.formattingOptions, configuration.formattingOptions); PropertyUtils.copyProperties(this.referencesOptions, configuration.referencesOptions); + PropertyUtils.copyProperties(this.semanticTokensOptions, configuration.semanticTokensOptions); } } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/ParsedStrTemplateMethods.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/ParsedStrTemplateMethods.java new file mode 100644 index 00000000000..f2fc2f4ab82 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/ParsedStrTemplateMethods.java @@ -0,0 +1,43 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2025 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.configuration.semantictokens; + +import java.util.Map; +import java.util.Set; + +/** + * Предварительно разобранные паттерны функций-шаблонизаторов. + *

+ * Структура: + *

+ * + * @param localMethods Методы для локального вызова (без указания модуля) + * @param moduleMethodPairs Методы с указанием модуля (модуль -> набор методов) + */ +public record ParsedStrTemplateMethods( + Set localMethods, + Map> moduleMethodPairs +) { +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/SemanticTokensOptions.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/SemanticTokensOptions.java new file mode 100644 index 00000000000..48507252dd4 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/SemanticTokensOptions.java @@ -0,0 +1,128 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2025 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.configuration.semantictokens; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Настройки для семантических токенов. + *

+ * Позволяет указать дополнительные функции-шаблонизаторы строк, + * аналогичные СтрШаблон/StrTemplate, для подсветки плейсхолдеров (%1, %2 и т.д.). + */ +@Getter +@AllArgsConstructor(onConstructor = @__({@JsonCreator(mode = JsonCreator.Mode.DISABLED)})) +@NoArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class SemanticTokensOptions { + + private static final List DEFAULT_STR_TEMPLATE_METHODS = List.of( + // Локальный вызов + "ПодставитьПараметрыВСтроку", + "SubstituteParametersToString", + // Стандартный модуль БСП + "СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку", + // Английский вариант + "StringFunctionsClientServer.SubstituteParametersToString" + ); + + /** + * Список паттернов "Модуль.Метод" для функций-шаблонизаторов строк. + *

+ * Строки внутри вызовов этих функций будут подсвечиваться так же, + * как строки в СтрШаблон/StrTemplate (с выделением плейсхолдеров %1, %2 и т.д.). + *

+ * Формат: "ИмяМодуля.ИмяМетода", например: + *

    + *
  • "СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку"
  • + *
  • "StringFunctionsClientServer.SubstituteParametersToString"
  • + *
  • "ПодставитьПараметрыВСтроку" - для локального вызова без указания модуля
  • + *
+ *

+ * По умолчанию включает стандартные варианты из БСП. + */ + private List strTemplateMethods = new ArrayList<>(DEFAULT_STR_TEMPLATE_METHODS); + + /** + * Кэшированные разобранные паттерны функций-шаблонизаторов. + */ + @JsonIgnore + private ParsedStrTemplateMethods parsedStrTemplateMethods = parseStrTemplateMethods(DEFAULT_STR_TEMPLATE_METHODS); + + /** + * Устанавливает список паттернов функций-шаблонизаторов и пересчитывает кэш. + * + * @param strTemplateMethods Список паттернов + */ + public void setStrTemplateMethods(List strTemplateMethods) { + this.strTemplateMethods = strTemplateMethods; + this.parsedStrTemplateMethods = parseStrTemplateMethods(strTemplateMethods); + } + + /** + * Возвращает предварительно разобранные паттерны функций-шаблонизаторов. + * + * @return Разобранные паттерны для быстрого поиска + */ + @JsonIgnore + public ParsedStrTemplateMethods getParsedStrTemplateMethods() { + return parsedStrTemplateMethods; + } + + private static ParsedStrTemplateMethods parseStrTemplateMethods(List methods) { + var localMethods = new HashSet(); + var moduleMethodPairs = new HashMap>(); + + for (var pattern : methods) { + if (pattern.isBlank()) { + continue; + } + var patternLower = pattern.toLowerCase(Locale.ENGLISH); + + if (patternLower.contains(".")) { + var parts = patternLower.split("\\.", 2); + if (parts.length == 2 && !parts[0].isEmpty() && !parts[1].isEmpty()) { + moduleMethodPairs + .computeIfAbsent(parts[0], k -> new HashSet<>()) + .add(parts[1]); + } + } else { + localMethods.add(patternLower); + } + } + + return new ParsedStrTemplateMethods(localMethods, moduleMethodPairs); + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/package-info.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/package-info.java new file mode 100644 index 00000000000..a8aad39ca28 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/package-info.java @@ -0,0 +1,28 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2025 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +/** + * Пакет содержит настройки для семантических токенов. + */ +@NullMarked +package com.github._1c_syntax.bsl.languageserver.configuration.semantictokens; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplier.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplier.java index 69cc5155548..20d98356269 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplier.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplier.java @@ -21,6 +21,9 @@ */ package com.github._1c_syntax.bsl.languageserver.semantictokens; +import com.github._1c_syntax.bsl.languageserver.configuration.LanguageServerConfiguration; +import com.github._1c_syntax.bsl.languageserver.configuration.events.LanguageServerConfigurationChangedEvent; +import com.github._1c_syntax.bsl.languageserver.configuration.semantictokens.ParsedStrTemplateMethods; import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; import com.github._1c_syntax.bsl.languageserver.semantictokens.strings.AstTokenInfo; import com.github._1c_syntax.bsl.languageserver.semantictokens.strings.QueryContext; @@ -33,11 +36,13 @@ import com.github._1c_syntax.bsl.languageserver.utils.MultilingualStringAnalyser; import com.github._1c_syntax.bsl.languageserver.utils.Ranges; import com.github._1c_syntax.bsl.parser.BSLLexer; +import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.antlr.v4.runtime.Token; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SemanticTokenTypes; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import java.util.ArrayList; @@ -57,6 +62,7 @@ *

  • Запросы SDBL: разбивает строки на части вокруг токенов запроса и добавляет токены SDBL
  • *
  • НСтр/NStr: подсвечивает языковые ключи (ru=, en=)
  • *
  • СтрШаблон/StrTemplate: подсвечивает плейсхолдеры (%1, %2)
  • + *
  • Конфигурируемые функции-шаблонизаторы: подсвечивает плейсхолдеры (%1, %2)
  • *
  • Обычные строки: выдаёт токен для всей строки
  • * */ @@ -72,6 +78,30 @@ public class StringSemanticTokensSupplier implements SemanticTokensSupplier { ); private final SemanticTokensHelper helper; + private final LanguageServerConfiguration configuration; + + private volatile ParsedStrTemplateMethods parsedStrTemplateMethods; + + @PostConstruct + private void init() { + updateParsedStrTemplateMethods(); + } + + /** + * Обработчик события {@link LanguageServerConfigurationChangedEvent}. + *

    + * Обновляет кэшированные паттерны функций-шаблонизаторов при изменении конфигурации. + * + * @param event Событие + */ + @EventListener + public void handleEvent(LanguageServerConfigurationChangedEvent event) { + updateParsedStrTemplateMethods(); + } + + private void updateParsedStrTemplateMethods() { + parsedStrTemplateMethods = configuration.getSemanticTokensOptions().getParsedStrTemplateMethods(); + } @Override public List getSemanticTokens(DocumentContext documentContext) { @@ -277,7 +307,7 @@ private void processSpecialContext( private Map collectSpecialStringContexts(DocumentContext documentContext) { Map contexts = new HashMap<>(); - var visitor = new SpecialContextVisitor(contexts); + var visitor = new SpecialContextVisitor(contexts, parsedStrTemplateMethods); visitor.visit(documentContext.getAst()); return contexts; } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SpecialContextVisitor.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SpecialContextVisitor.java index de314e65eee..9f88edb99e0 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SpecialContextVisitor.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SpecialContextVisitor.java @@ -21,6 +21,7 @@ */ package com.github._1c_syntax.bsl.languageserver.semantictokens.strings; +import com.github._1c_syntax.bsl.languageserver.configuration.semantictokens.ParsedStrTemplateMethods; import com.github._1c_syntax.bsl.languageserver.utils.MultilingualStringAnalyser; import com.github._1c_syntax.bsl.languageserver.utils.Trees; import com.github._1c_syntax.bsl.parser.BSLLexer; @@ -32,6 +33,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -44,6 +46,9 @@ *

    * Также поддерживает поиск строк-шаблонов, которые присвоены переменным, * а затем используются в вызове СтрШаблон. + *

    + * Дополнительно поддерживает конфигурируемые функции-шаблонизаторы, + * аналогичные СтрШаблон (например, СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку). */ public class SpecialContextVisitor extends BSLParserBaseVisitor { @@ -55,14 +60,17 @@ public class SpecialContextVisitor extends BSLParserBaseVisitor { ); private final Map contexts; + private final ParsedStrTemplateMethods parsedMethods; /** - * Создаёт visitor для сбора контекстов строк. + * Создаёт visitor для сбора контекстов строк с конфигурируемыми функциями-шаблонизаторами. * - * @param contexts Map для заполнения контекстами строк + * @param contexts Map для заполнения контекстами строк + * @param parsedMethods Предварительно разобранные паттерны функций-шаблонизаторов */ - public SpecialContextVisitor(Map contexts) { + public SpecialContextVisitor(Map contexts, ParsedStrTemplateMethods parsedMethods) { this.contexts = contexts; + this.parsedMethods = parsedMethods; } @Override @@ -73,27 +81,106 @@ public Void visitGlobalMethodCall(BSLParser.GlobalMethodCallContext ctx) { context = StringContext.NSTR; } else if (MultilingualStringAnalyser.isStrTemplateCall(ctx)) { context = StringContext.STR_TEMPLATE; + } else if (isConfiguredStrTemplateCall(ctx)) { + context = StringContext.STR_TEMPLATE; } if (context != null) { - var callParams = ctx.doCall().callParamList().callParam(); - if (!callParams.isEmpty()) { - var firstParam = callParams.get(0); - var stringTokens = getStringTokensFromParam(firstParam); + processMethodCallParams(ctx.doCall(), context, ctx); + } - if (stringTokens.isEmpty() && context == StringContext.STR_TEMPLATE) { - // Первый параметр не строковый литерал - возможно, это переменная - // Пытаемся найти присвоение этой переменной - stringTokens = findStringTokensFromVariable(firstParam, ctx); - } + return super.visitGlobalMethodCall(ctx); + } - for (Token token : stringTokens) { - contexts.merge(token, context, StringContext::combine); + @Override + public Void visitCallStatement(BSLParser.CallStatementContext ctx) { + // Обрабатываем вызовы вида Модуль.Метод(...) в отдельных statements (не в выражениях) + if (ctx.IDENTIFIER() != null && ctx.accessCall() != null) { + processModuleMethodCall(ctx.IDENTIFIER().getText(), ctx.accessCall(), ctx); + } + + return super.visitCallStatement(ctx); + } + + @Override + public Void visitComplexIdentifier(BSLParser.ComplexIdentifierContext ctx) { + // Обрабатываем вызовы вида Модуль.Метод(...) в выражениях (присвоениях и т.п.) + var identifier = ctx.IDENTIFIER(); + if (identifier != null && ctx.modifier() != null && !ctx.modifier().isEmpty()) { + for (var modifier : ctx.modifier()) { + var accessCall = modifier.accessCall(); + if (accessCall != null) { + processModuleMethodCall(identifier.getText(), accessCall, ctx); } } } - return super.visitGlobalMethodCall(ctx); + return super.visitComplexIdentifier(ctx); + } + + /** + * Обрабатывает вызов метода модуля вида Модуль.Метод(...). + */ + private void processModuleMethodCall( + String moduleName, + BSLParser.AccessCallContext accessCall, + org.antlr.v4.runtime.ParserRuleContext ctx + ) { + var methodCall = accessCall.methodCall(); + if (methodCall == null || methodCall.methodName() == null) { + return; + } + + var methodName = methodCall.methodName().getText().toLowerCase(Locale.ENGLISH); + var moduleNameLower = moduleName.toLowerCase(Locale.ENGLISH); + + if (isModuleMethodMatch(moduleNameLower, methodName)) { + var doCall = methodCall.doCall(); + if (doCall != null) { + processMethodCallParams(doCall, StringContext.STR_TEMPLATE, ctx); + } + } + } + + /** + * Проверяет, является ли вызов глобального метода конфигурируемым шаблонизатором. + */ + private boolean isConfiguredStrTemplateCall(BSLParser.GlobalMethodCallContext ctx) { + var methodName = ctx.methodName().getText().toLowerCase(Locale.ENGLISH); + return parsedMethods.localMethods().contains(methodName); + } + + /** + * Проверяет, соответствует ли пара "модуль.метод" конфигурируемым паттернам. + */ + private boolean isModuleMethodMatch(String moduleName, String methodName) { + var moduleMethods = parsedMethods.moduleMethodPairs().get(moduleName); + return moduleMethods != null && moduleMethods.contains(methodName); + } + + /** + * Обрабатывает параметры вызова метода. + */ + private void processMethodCallParams( + BSLParser.DoCallContext doCall, + StringContext context, + ParserRuleContext callContext + ) { + var callParams = doCall.callParamList().callParam(); + if (!callParams.isEmpty()) { + var firstParam = callParams.get(0); + var stringTokens = getStringTokensFromParam(firstParam); + + if (stringTokens.isEmpty() && context == StringContext.STR_TEMPLATE) { + // Первый параметр не строковый литерал - возможно, это переменная + // Пытаемся найти присвоение этой переменной + stringTokens = findStringTokensFromVariable(firstParam, callContext); + } + + for (Token token : stringTokens) { + contexts.merge(token, context, StringContext::combine); + } + } } private List getStringTokensFromParam(BSLParser.CallParamContext callParam) { @@ -128,7 +215,7 @@ private List getStringTokensFromParam(BSLParser.CallParamContext callPara */ private List findStringTokensFromVariable( BSLParser.CallParamContext callParam, - BSLParser.GlobalMethodCallContext callContext + ParserRuleContext callContext ) { // Получаем имя переменной из первого параметра var varName = extractVariableName(callParam); diff --git a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json index 37bc7f9ce94..b941252210f 100644 --- a/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json +++ b/src/main/resources/com/github/_1c_syntax/bsl/languageserver/configuration/schema.json @@ -1155,6 +1155,27 @@ ] } } + }, + "semanticTokens": { + "$id": "#/properties/semanticTokens", + "type": "object", + "title": "Semantic tokens configuration.", + "properties": { + "strTemplateMethods": { + "$id": "#/properties/semanticTokens/strTemplateMethods", + "type": "array", + "title": "List of 'Module.Method' patterns for string template functions similar to StrTemplate/СтрШаблон. Strings in these functions will be highlighted with placeholder detection (%1, %2, etc.).", + "items": { + "type": "string" + }, + "default": [ + "ПодставитьПараметрыВСтроку", + "SubstituteParametersToString", + "СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку", + "StringFunctionsClientServer.SubstituteParametersToString" + ] + } + } } } } \ No newline at end of file diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/configuration/LanguageServerConfigurationTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/configuration/LanguageServerConfigurationTest.java index 40e03ba2f5d..16178676ece 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/configuration/LanguageServerConfigurationTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/configuration/LanguageServerConfigurationTest.java @@ -26,6 +26,7 @@ import com.github._1c_syntax.bsl.languageserver.configuration.diagnostics.Mode; import com.github._1c_syntax.bsl.languageserver.configuration.diagnostics.SkipSupport; import com.github._1c_syntax.bsl.languageserver.configuration.inlayhints.InlayHintOptions; +import com.github._1c_syntax.bsl.languageserver.configuration.semantictokens.SemanticTokensOptions; import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterEachTestMethod; import com.github._1c_syntax.utils.Absolute; import org.eclipse.lsp4j.TextDocumentSyncKind; @@ -162,6 +163,7 @@ void testPartialInitialization() { CodeLensOptions codeLensOptions = configuration.getCodeLensOptions(); DiagnosticsOptions diagnosticsOptions = configuration.getDiagnosticsOptions(); InlayHintOptions inlayHintOptions = configuration.getInlayHintOptions(); + SemanticTokensOptions semanticTokensOptions = configuration.getSemanticTokensOptions(); // then assertThat(codeLensOptions.getParameters().get("cognitiveComplexity")).isNull(); @@ -176,6 +178,14 @@ void testPartialInitialization() { assertThat(inlayHintOptions.getParameters()) .containsEntry("sourceDefinedMethodCall", Either.forRight(Map.of("showParametersWithTheSameName", true))); + assertThat(semanticTokensOptions.getStrTemplateMethods()) + .hasSize(2) + .contains("CustomModule.CustomMethod", "CustomLocalMethod"); + assertThat(semanticTokensOptions.getParsedStrTemplateMethods().localMethods()) + .contains("customlocalmethod"); + assertThat(semanticTokensOptions.getParsedStrTemplateMethods().moduleMethodPairs()) + .containsKey("custommodule"); + } } \ No newline at end of file diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/SemanticTokensOptionsTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/SemanticTokensOptionsTest.java new file mode 100644 index 00000000000..1b3cbf29771 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/configuration/semantictokens/SemanticTokensOptionsTest.java @@ -0,0 +1,202 @@ +/* + * This file is a part of BSL Language Server. + * + * Copyright (c) 2018-2025 + * Alexey Sosnoviy , Nikita Fedkin and contributors + * + * SPDX-License-Identifier: LGPL-3.0-or-later + * + * BSL Language Server is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * BSL Language Server is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with BSL Language Server. + */ +package com.github._1c_syntax.bsl.languageserver.configuration.semantictokens; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class SemanticTokensOptionsTest { + + @Test + void testDefaultValues() { + // when + var options = new SemanticTokensOptions(); + + // then + assertThat(options.getStrTemplateMethods()) + .hasSize(4) + .contains( + "ПодставитьПараметрыВСтроку", + "SubstituteParametersToString", + "СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку", + "StringFunctionsClientServer.SubstituteParametersToString" + ); + } + + @Test + void testParseLocalMethods() { + // given + var options = new SemanticTokensOptions(); + options.setStrTemplateMethods(List.of( + "ПодставитьПараметрыВСтроку", + "SubstituteParametersToString", + "CustomMethod" + )); + + // when + var parsed = options.getParsedStrTemplateMethods(); + + // then + assertThat(parsed.localMethods()) + .hasSize(3) + .contains("подставитьпараметрывстроку", "substituteparameterstostring", "custommethod"); + assertThat(parsed.moduleMethodPairs()).isEmpty(); + } + + @Test + void testParseModuleMethodPairs() { + // given + var options = new SemanticTokensOptions(); + options.setStrTemplateMethods(List.of( + "СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку", + "StringFunctionsClientServer.SubstituteParametersToString", + "MyModule.MyMethod" + )); + + // when + var parsed = options.getParsedStrTemplateMethods(); + + // then + assertThat(parsed.localMethods()).isEmpty(); + assertThat(parsed.moduleMethodPairs()) + .hasSize(3) + .containsKeys("строковыефункцииклиентсервер", "stringfunctionsclientserver", "mymodule"); + assertThat(parsed.moduleMethodPairs().get("строковыефункцииклиентсервер")) + .contains("подставитьпараметрывстроку"); + assertThat(parsed.moduleMethodPairs().get("stringfunctionsclientserver")) + .contains("substituteparameterstostring"); + assertThat(parsed.moduleMethodPairs().get("mymodule")) + .contains("mymethod"); + } + + @Test + void testCaseInsensitiveMatching() { + // given + var options = new SemanticTokensOptions(); + options.setStrTemplateMethods(List.of( + "ПОДСТАВИТЬПАРАМЕТРЫВСТРОКУ", + "СтроковыеФункцииКлиентСервер.ПОДСТАВИТЬПАРАМЕТРЫВСТРОКУ" + )); + + // when + var parsed = options.getParsedStrTemplateMethods(); + + // then + assertThat(parsed.localMethods()) + .hasSize(1) + .contains("подставитьпараметрывстроку"); + assertThat(parsed.moduleMethodPairs().get("строковыефункцииклиентсервер")) + .contains("подставитьпараметрывстроку"); + } + + @Test + void testEmptyStrings() { + // given + var options = new SemanticTokensOptions(); + options.setStrTemplateMethods(List.of( + "", + " ", + "ValidMethod" + )); + + // when + var parsed = options.getParsedStrTemplateMethods(); + + // then + assertThat(parsed.localMethods()) + .hasSize(1) + .contains("validmethod"); + assertThat(parsed.moduleMethodPairs()).isEmpty(); + } + + @Test + void testInvalidPatterns() { + // given + var options = new SemanticTokensOptions(); + options.setStrTemplateMethods(List.of( + ".InvalidPattern", + "InvalidPattern.", + "..DoubleDot", + "Valid.Pattern" + )); + + // when + var parsed = options.getParsedStrTemplateMethods(); + + // then + // Invalid patterns should not create entries + assertThat(parsed.localMethods()).isEmpty(); + assertThat(parsed.moduleMethodPairs()) + .hasSize(1) + .containsKey("valid"); + assertThat(parsed.moduleMethodPairs().get("valid")) + .contains("pattern"); + } + + @Test + void testCachingBehavior() { + // given + var options = new SemanticTokensOptions(); + options.setStrTemplateMethods(List.of("Method1")); + + // when + var parsed1 = options.getParsedStrTemplateMethods(); + var parsed2 = options.getParsedStrTemplateMethods(); + + // then + assertThat(parsed1).isSameAs(parsed2); + + // Update methods + options.setStrTemplateMethods(List.of("Method2")); + var parsed3 = options.getParsedStrTemplateMethods(); + + // Cache should be updated + assertThat(parsed3).isNotSameAs(parsed1); + assertThat(parsed3.localMethods()) + .hasSize(1) + .contains("method2"); + } + + @Test + void testPatternsWithMultipleDots() { + // given + var options = new SemanticTokensOptions(); + options.setStrTemplateMethods(List.of( + "Module.SubModule.Method" + )); + + // when + var parsed = options.getParsedStrTemplateMethods(); + + // then + // Should split only on first dot + assertThat(parsed.localMethods()).isEmpty(); + assertThat(parsed.moduleMethodPairs()) + .hasSize(1) + .containsKey("module"); + assertThat(parsed.moduleMethodPairs().get("module")) + .contains("submodule.method"); + } +} diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplierTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplierTest.java index 247caa77e39..37b4e9cd23c 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplierTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplierTest.java @@ -22,12 +22,14 @@ package com.github._1c_syntax.bsl.languageserver.semantictokens; import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterEachTestMethod; -import com.github._1c_syntax.bsl.languageserver.util.TestUtils; +import com.github._1c_syntax.bsl.languageserver.util.SemanticTokensTestHelper; +import com.github._1c_syntax.bsl.languageserver.util.SemanticTokensTestHelper.ExpectedToken; +import org.eclipse.lsp4j.SemanticTokenModifiers; import org.eclipse.lsp4j.SemanticTokenTypes; -import org.eclipse.lsp4j.SemanticTokensLegend; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; import java.util.List; @@ -35,29 +37,14 @@ @SpringBootTest @CleanupContextBeforeClassAndAfterEachTestMethod +@Import(SemanticTokensTestHelper.class) class StringSemanticTokensSupplierTest { @Autowired private StringSemanticTokensSupplier supplier; @Autowired - private SemanticTokensLegend legend; - - private List tokens(String bsl) { - var documentContext = TestUtils.getDocumentContext(bsl); - return supplier.getSemanticTokens(documentContext); - } - - private int typeIndex(String semanticTokenType) { - return legend.getTokenTypes().indexOf(semanticTokenType); - } - - private List tokensOfType(List tokens, String semanticTokenType) { - int typeIdx = typeIndex(semanticTokenType); - return tokens.stream() - .filter(t -> t.type() == typeIdx) - .toList(); - } + private SemanticTokensTestHelper helper; // ==================== Regular String Tests ==================== @@ -71,11 +58,13 @@ void testSimpleString() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); - // then - var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); - assertThat(stringTokens).hasSize(1); + // then - one string token + assertThat(decoded).hasSize(1); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 10, 12, SemanticTokenTypes.String, "\"Привет мир\"") + )); } @Test @@ -90,12 +79,15 @@ void testMultilineString() { """; // when - var tokens = tokens(bsl); - - // then - var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); - // STRINGSTART, 2x STRINGPART, or STRINGTAIL - assertThat(stringTokens).hasSizeGreaterThanOrEqualTo(3); + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then - multiple string parts + assertThat(decoded).hasSize(3); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 10, 14, SemanticTokenTypes.String, "\"Первая строка"), + new ExpectedToken(2, 2, 14, SemanticTokenTypes.String, "|Вторая строка"), + new ExpectedToken(3, 2, 15, SemanticTokenTypes.String, "|Третья строка\"") + )); } // ==================== NStr Tests ==================== @@ -110,16 +102,13 @@ void testNStrLanguageKeys() { """; // when - var tokens = tokens(bsl); - - // then - var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); - // ru, en - assertThat(propertyTokens).hasSize(2); + var decoded = helper.getDecodedTokens(bsl, supplier); - // Check that string parts are also present - var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); - assertThat(stringTokens).isNotEmpty(); + // then - language keys (ru, en) are highlighted as Property + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 16, 2, SemanticTokenTypes.Property, "ru"), + new ExpectedToken(1, 29, 2, SemanticTokenTypes.Property, "en") + )); } @Test @@ -132,12 +121,13 @@ void testNStrEnglishName() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); - // then - var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); - // ru, en - assertThat(propertyTokens).hasSize(2); + // then - NStr works same as НСтр + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 16, 2, SemanticTokenTypes.Property, "ru"), + new ExpectedToken(1, 29, 2, SemanticTokenTypes.Property, "en") + )); } // ==================== StrTemplate Tests ==================== @@ -152,16 +142,13 @@ void testStrTemplatePlaceholders() { """; // when - var tokens = tokens(bsl); - - // then - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %1, %2 - assertThat(parameterTokens).hasSize(2); + var decoded = helper.getDecodedTokens(bsl, supplier); - // Check that string parts are also present - var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); - assertThat(stringTokens).isNotEmpty(); + // then - placeholders %1, %2 are highlighted as Parameter + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 35, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 47, 2, SemanticTokenTypes.Parameter, "%2") + )); } @Test @@ -174,12 +161,13 @@ void testStrTemplateEnglishName() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %1, %2 - assertThat(parameterTokens).hasSize(2); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 29, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 42, 2, SemanticTokenTypes.Parameter, "%2") + )); } @Test @@ -192,12 +180,13 @@ void testStrTemplatePlaceholdersWithParentheses() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); - // then - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %(1), %(2) - assertThat(parameterTokens).hasSize(2); + // then - %(1) and %(2) syntax + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 21, 4, SemanticTokenTypes.Parameter, "%(1)"), + new ExpectedToken(1, 25, 4, SemanticTokenTypes.Parameter, "%(2)") + )); } @Test @@ -211,12 +200,13 @@ void testStrTemplateWithVariable() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - плейсхолдеры в строке-присвоении должны подсвечиваться - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %1, %2 из строки НовыйШаблон = "%1 %2" - assertThat(parameterTokens).hasSize(2); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 17, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 20, 2, SemanticTokenTypes.Parameter, "%2") + )); } @Test @@ -230,12 +220,13 @@ void testStrTemplateWithVariableAndParentheses() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %(1), %(2) - assertThat(parameterTokens).hasSize(2); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 12, 4, SemanticTokenTypes.Parameter, "%(1)"), + new ExpectedToken(1, 16, 4, SemanticTokenTypes.Parameter, "%(2)") + )); } // ==================== Combined NStr + StrTemplate Tests ==================== @@ -250,16 +241,13 @@ void testNStrInsideStrTemplate() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - должны быть и языковые ключи (ru), и плейсхолдеры (%1) - var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); - // ru - assertThat(propertyTokens).hasSize(1); - - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %1 - assertThat(parameterTokens).hasSize(1); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 27, 2, SemanticTokenTypes.Property, "ru"), + new ExpectedToken(1, 42, 2, SemanticTokenTypes.Parameter, "%1") + )); } @Test @@ -272,16 +260,15 @@ void testNStrInsideStrTemplateMultiple() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); - // ru, en - assertThat(propertyTokens).hasSize(2); - - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %1, %2 - assertThat(parameterTokens).hasSize(2); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 30, 2, SemanticTokenTypes.Property, "ru"), + new ExpectedToken(1, 43, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 48, 2, SemanticTokenTypes.Property, "en"), + new ExpectedToken(1, 60, 2, SemanticTokenTypes.Parameter, "%2") + )); } @Test @@ -295,16 +282,13 @@ void testNStrVariableInStrTemplate() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - должны быть и языковые ключи (ru), и плейсхолдеры (%1) - var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); - // ru - assertThat(propertyTokens).hasSize(1); - - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %1 - assertThat(parameterTokens).hasSize(1); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 17, 2, SemanticTokenTypes.Property, "ru"), + new ExpectedToken(1, 32, 2, SemanticTokenTypes.Parameter, "%1") + )); } @Test @@ -318,17 +302,17 @@ void testNStrVariableInStrTemplateMultiple() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); - // ru, en - assertThat(propertyTokens).hasSize(2); - - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - // %1, %2 (по одному разу в каждой подстроке, но токен один - значит 4 плейсхолдера) - // Нет, здесь один строковый токен, внутри которого 4 вхождения %N - assertThat(parameterTokens).hasSize(4); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 17, 2, SemanticTokenTypes.Property, "ru"), + new ExpectedToken(1, 30, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 35, 2, SemanticTokenTypes.Parameter, "%2"), + new ExpectedToken(1, 40, 2, SemanticTokenTypes.Property, "en"), + new ExpectedToken(1, 52, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 59, 2, SemanticTokenTypes.Parameter, "%2") + )); } // ==================== Query String Tests ==================== @@ -343,14 +327,15 @@ void testQueryStringSplit() { """; // when - var tokens = tokens(bsl); - - // then - // String parts should be split around query tokens - var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); - - // Should have multiple string parts (quotes and spaces around keywords) - assertThat(stringTokens).hasSizeGreaterThan(1); + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then - query keywords are highlighted + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 18, 7, SemanticTokenTypes.Keyword, "ВЫБРАТЬ"), + new ExpectedToken(1, 33, 2, SemanticTokenTypes.Keyword, "ИЗ"), + new ExpectedToken(1, 36, 10, SemanticTokenTypes.Namespace, "Справочник"), + new ExpectedToken(1, 47, 12, SemanticTokenTypes.Class, "Номенклатура") + )); } @Test @@ -366,13 +351,15 @@ void testMultilineQueryString() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); - - // Should have string parts on each line - assertThat(stringTokens).isNotEmpty(); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 18, 7, SemanticTokenTypes.Keyword, "ВЫБРАТЬ"), + new ExpectedToken(3, 3, 2, SemanticTokenTypes.Keyword, "ИЗ"), + new ExpectedToken(4, 5, 10, SemanticTokenTypes.Namespace, "Справочник"), + new ExpectedToken(4, 16, 12, SemanticTokenTypes.Class, "Номенклатура") + )); } // ==================== Mixed Context Tests ==================== @@ -388,16 +375,16 @@ void testNStrAndQueryInSameMethod() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); - // ru from NStr - assertThat(propertyTokens).hasSize(1); - - var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); - // Should have string parts from both NStr and query - assertThat(stringTokens).hasSizeGreaterThan(2); + helper.assertContainsTokens(decoded, List.of( + // NStr tokens + new ExpectedToken(1, 20, 2, SemanticTokenTypes.Property, "ru"), + // Query tokens + new ExpectedToken(2, 18, 7, SemanticTokenTypes.Keyword, "ВЫБРАТЬ"), + new ExpectedToken(2, 36, 10, SemanticTokenTypes.Namespace, "Справочник") + )); } @Test @@ -410,19 +397,13 @@ void testRegularStringNotAffectedByOtherContexts() { """; // when - var tokens = tokens(bsl); - - // then - var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); - // Single string token for the whole string - assertThat(stringTokens).hasSize(1); - - // No Property or Parameter tokens - var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); - assertThat(propertyTokens).isEmpty(); + var decoded = helper.getDecodedTokens(bsl, supplier); - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - assertThat(parameterTokens).isEmpty(); + // then - just one string token + assertThat(decoded).hasSize(1); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 18, 38, SemanticTokenTypes.String, "\"Это просто строка без НСтр и запроса\"") + )); } // ==================== SDBL Query Token Tests ==================== @@ -437,19 +418,15 @@ void testSimpleSelect() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var keywordTokens = tokensOfType(tokens, SemanticTokenTypes.Keyword); - var namespaceTokens = tokensOfType(tokens, SemanticTokenTypes.Namespace); - var classTokens = tokensOfType(tokens, SemanticTokenTypes.Class); - - // Выбрать, из - assertThat(keywordTokens).hasSizeGreaterThanOrEqualTo(2); - // Справочник - assertThat(namespaceTokens).hasSize(1); - // Контрагенты - assertThat(classTokens).hasSize(1); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 12, 7, SemanticTokenTypes.Keyword, "Выбрать"), + new ExpectedToken(1, 22, 2, SemanticTokenTypes.Keyword, "из"), + new ExpectedToken(1, 25, 10, SemanticTokenTypes.Namespace, "Справочник"), + new ExpectedToken(1, 36, 11, SemanticTokenTypes.Class, "Контрагенты") + )); } @Test @@ -462,14 +439,12 @@ void testQueryWithParameter() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); - // then - var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); - - // &Параметр - один объединённый токен - assertThat(parameterTokens).hasSize(1); - assertThat(parameterTokens.get(0).line()).isEqualTo(1); + // then - Query parameter has readonly modifier + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 32, 9, SemanticTokenTypes.Parameter, SemanticTokenModifiers.Readonly, "&Параметр") + )); } @Test @@ -482,13 +457,12 @@ void testQueryWithVirtualTable() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var methodTokens = tokensOfType(tokens, SemanticTokenTypes.Method); - - // СрезПоследних - assertThat(methodTokens).hasSize(1); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 52, 13, SemanticTokenTypes.Method, "СрезПоследних") + )); } @Test @@ -501,19 +475,14 @@ void testQueryWithValueFunction() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var namespaceTokens = tokensOfType(tokens, SemanticTokenTypes.Namespace); - var classTokens = tokensOfType(tokens, SemanticTokenTypes.Class); - var enumMemberTokens = tokensOfType(tokens, SemanticTokenTypes.EnumMember); - - // Справочник - assertThat(namespaceTokens).hasSize(1); - // Валюты - assertThat(classTokens).hasSize(1); - // Рубль - assertThat(enumMemberTokens).hasSize(1); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 44, 10, SemanticTokenTypes.Namespace, "Справочник"), + new ExpectedToken(1, 55, 6, SemanticTokenTypes.Class, "Валюты"), + new ExpectedToken(1, 62, 5, SemanticTokenTypes.EnumMember, "Рубль") + )); } @Test @@ -526,16 +495,14 @@ void testQueryWithEnumValue() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var enumTokens = tokensOfType(tokens, SemanticTokenTypes.Enum); - var enumMemberTokens = tokensOfType(tokens, SemanticTokenTypes.EnumMember); - - // Пол (enum) - assertThat(enumTokens).hasSize(1); - // Мужской (enum member) - assertThat(enumMemberTokens).hasSize(1); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 41, 12, SemanticTokenTypes.Namespace, "Перечисление"), + new ExpectedToken(1, 54, 3, SemanticTokenTypes.Enum, "Пол"), + new ExpectedToken(1, 58, 7, SemanticTokenTypes.EnumMember, "Мужской") + )); } @Test @@ -548,13 +515,265 @@ void testQueryWithAggregateFunction() { """; // when - var tokens = tokens(bsl); + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then - Aggregate function has defaultLibrary modifier + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 20, 10, SemanticTokenTypes.Function, SemanticTokenModifiers.DefaultLibrary, "Количество") + )); + } + + // ==================== Configurable Template Function Tests ==================== + + @Test + void testSubstituteParametersToStringPlaceholders() { + // given - СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку like СтрШаблон + String bsl = """ + Процедура Тест() + Текст = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 81, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 93, 2, SemanticTokenTypes.Parameter, "%2") + )); + } + + @Test + void testSubstituteParametersToStringEnglish() { + // given - English variant of the function + String bsl = """ + Процедура Тест() + Text = StringFunctionsClientServer.SubstituteParametersToString("Name: %1, version: %2", Name, Version); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 73, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 86, 2, SemanticTokenTypes.Parameter, "%2") + )); + } + + @Test + void testSubstituteParametersToStringLocal() { + // given - Local call without module prefix (configured in defaults) + String bsl = """ + Процедура Тест() + Текст = ПодставитьПараметрыВСтроку("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); // then - var functionTokens = tokensOfType(tokens, SemanticTokenTypes.Function); + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 52, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 64, 2, SemanticTokenTypes.Parameter, "%2") + )); + } - // Количество - assertThat(functionTokens).hasSize(1); + @Test + void testSubstituteParametersToStringWithVariable() { + // given - template stored in variable, then used in module function call + String bsl = """ + Процедура Тест() + Шаблон = "%1 + %2 = %3"; + Текст = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(Шаблон, А, Б, В); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then - placeholders in the assigned string should be highlighted + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 12, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 17, 2, SemanticTokenTypes.Parameter, "%2"), + new ExpectedToken(1, 22, 2, SemanticTokenTypes.Parameter, "%3") + )); } -} + @Test + void testNStrWithSubstituteParametersToString() { + // given - НСтр combined with ПодставитьПараметрыВСтроку + String bsl = """ + Процедура Тест() + Шаблон = НСтр("ru = 'Привет %1'"); + Текст = СтроковыеФункцииКлиентСервер.ПодставитьПараметрыВСтроку(Шаблон, Имя); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then - should have both language keys (ru) and placeholders (%1) + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 17, 2, SemanticTokenTypes.Property, "ru"), + new ExpectedToken(1, 30, 2, SemanticTokenTypes.Parameter, "%1") + )); + } + + // ==================== Case-Insensitive Method Name Tests ==================== + + @Test + void testStrTemplateUpperCase() { + // given - СтрШаблон in uppercase + String bsl = """ + Процедура Тест() + Текст = СТРШАБЛОН("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then - placeholders should still be highlighted + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 35, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 47, 2, SemanticTokenTypes.Parameter, "%2") + )); + } + + @Test + void testStrTemplateMixedCase() { + // given - СтрШаблон in mixed case + String bsl = """ + Процедура Тест() + Текст = стрШаблон("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 35, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 47, 2, SemanticTokenTypes.Parameter, "%2") + )); + } + + @Test + void testNStrUpperCase() { + // given - НСтр in uppercase + String bsl = """ + Процедура Тест() + Текст = НСТР("ru='Привет'; en='Hello'"); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then - language keys should still be highlighted + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 16, 2, SemanticTokenTypes.Property, "ru"), + new ExpectedToken(1, 29, 2, SemanticTokenTypes.Property, "en") + )); + } + + @Test + void testSubstituteParametersToStringUpperCase() { + // given - local method call in uppercase + String bsl = """ + Процедура Тест() + Текст = ПОДСТАВИТЬПАРАМЕТРЫВСТРОКУ("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then - placeholders should be highlighted + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 52, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 64, 2, SemanticTokenTypes.Parameter, "%2") + )); + } + + @Test + void testSubstituteParametersToStringMixedCase() { + // given - local method call in mixed case + String bsl = """ + Процедура Тест() + Текст = подставитьПараметрыВСтроку("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 52, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 64, 2, SemanticTokenTypes.Parameter, "%2") + )); + } + + @Test + void testModuleMethodCallUpperCase() { + // given - module.method call in uppercase + String bsl = """ + Процедура Тест() + Текст = СТРОКОВЫЕФУНКЦИИКЛИЕНТСЕРВЕР.ПОДСТАВИТЬПАРАМЕТРЫВСТРОКУ("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 81, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 93, 2, SemanticTokenTypes.Parameter, "%2") + )); + } + + @Test + void testModuleMethodCallMixedCase() { + // given - module.method call in mixed case + String bsl = """ + Процедура Тест() + Текст = СтроковыеФункцииКлиентсервер.подставитьПараметрыВстроку("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 81, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 93, 2, SemanticTokenTypes.Parameter, "%2") + )); + } + + @Test + void testEnglishSubstituteParametersToStringUpperCase() { + // given - English variant in uppercase + String bsl = """ + Процедура Тест() + Text = STRINGFUNCTIONSCLIENTSERVER.SUBSTITUTEPARAMETERSTOSTRING("Name: %1, version: %2", Name, Version); + КонецПроцедуры + """; + + // when + var decoded = helper.getDecodedTokens(bsl, supplier); + + // then + helper.assertContainsTokens(decoded, List.of( + new ExpectedToken(1, 73, 2, SemanticTokenTypes.Parameter, "%1"), + new ExpectedToken(1, 86, 2, SemanticTokenTypes.Parameter, "%2") + )); + } +} \ No newline at end of file diff --git a/src/test/resources/.partial-bsl-language-server.json b/src/test/resources/.partial-bsl-language-server.json index 070725801a3..110edf68a84 100644 --- a/src/test/resources/.partial-bsl-language-server.json +++ b/src/test/resources/.partial-bsl-language-server.json @@ -13,5 +13,11 @@ }, "diagnostics": { "mode": "on" + }, + "semanticTokens": { + "strTemplateMethods": [ + "CustomModule.CustomMethod", + "CustomLocalMethod" + ] } }