Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* This file is a part of BSL Language Server.
*
* Copyright (c) 2018-2025
* Alexey Sosnoviy <[email protected]>, Nikita Fedkin <[email protected]> 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.semantictokens;

import com.github._1c_syntax.bsl.languageserver.context.DocumentContext;
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.BSLParser;
import com.github._1c_syntax.bsl.parser.BSLParserBaseVisitor;
import lombok.RequiredArgsConstructor;
import org.antlr.v4.runtime.Token;
import org.eclipse.lsp4j.SemanticTokenTypes;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

/**
* Сапплаер семантических токенов для функций НСтр (NStr) и СтрШаблон (StrTemplate).
* <p>
* Для НСтр: подсвечивает языковые ключи (ru=, en=) в строковых параметрах.
* <p>
* Для СтрШаблон: подсвечивает плейсхолдеры (%1, %2, %(1)) в строковых параметрах.
*/
@Component
@RequiredArgsConstructor
public class NStrAndStrTemplateSemanticTokensSupplier implements SemanticTokensSupplier {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot используй MultilingualStringAnalyser. Расширь его при необходимости

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to use MultilingualStringAnalyser. Extended it with:

  • isNStrCall() and isStrTemplateCall() static methods
  • findLanguageKeyPositions() and findPlaceholderPositions() static methods
  • MatchPosition record for position info
  • STR_TEMPLATE_PLACEHOLDER_PATTERN (same pattern as in IncorrectUseOfStrTemplateDiagnostic)

Commit: 5c618ee


private static final Set<Integer> STRING_TOKEN_TYPES = Set.of(
BSLParser.STRING,
BSLParser.STRINGPART,
BSLParser.STRINGSTART,
BSLParser.STRINGTAIL
);

private final SemanticTokensHelper helper;

@Override
public List<SemanticTokenEntry> getSemanticTokens(DocumentContext documentContext) {
List<SemanticTokenEntry> entries = new ArrayList<>();

var visitor = new NStrAndStrTemplateVisitor(entries, helper);
visitor.visit(documentContext.getAst());

return entries;
}

/**
* Visitor for finding NStr and StrTemplate method calls.
*/
private static class NStrAndStrTemplateVisitor extends BSLParserBaseVisitor<Void> {
private final List<SemanticTokenEntry> entries;
private final SemanticTokensHelper helper;

public NStrAndStrTemplateVisitor(List<SemanticTokenEntry> entries, SemanticTokensHelper helper) {
this.entries = entries;
this.helper = helper;
}

@Override
public Void visitGlobalMethodCall(BSLParser.GlobalMethodCallContext ctx) {
if (MultilingualStringAnalyser.isNStrCall(ctx)) {
processNStrCall(ctx);
} else if (MultilingualStringAnalyser.isStrTemplateCall(ctx)) {
processStrTemplateCall(ctx);
}

return super.visitGlobalMethodCall(ctx);
}

private void processNStrCall(BSLParser.GlobalMethodCallContext ctx) {
var callParams = ctx.doCall().callParamList().callParam();
if (callParams.isEmpty()) {
return;
}

// Get the first parameter (the multilingual string)
var firstParam = callParams.get(0);
var stringTokens = getStringTokens(firstParam);

for (Token token : stringTokens) {
String tokenText = token.getText();
int tokenLine = token.getLine() - 1; // Convert to 0-indexed
int tokenStart = token.getCharPositionInLine();

// Find language keys in the string using MultilingualStringAnalyser
var positions = MultilingualStringAnalyser.findLanguageKeyPositions(tokenText);
for (var position : positions) {
helper.addEntry(
entries,
tokenLine,
tokenStart + position.start(),
position.length(),
SemanticTokenTypes.Property
);
}
}
}

private void processStrTemplateCall(BSLParser.GlobalMethodCallContext ctx) {
var callParams = ctx.doCall().callParamList().callParam();
if (callParams.isEmpty()) {
return;
}

// Get the first parameter (the template string)
var firstParam = callParams.get(0);
var stringTokens = getStringTokens(firstParam);

for (Token token : stringTokens) {
String tokenText = token.getText();
int tokenLine = token.getLine() - 1; // Convert to 0-indexed
int tokenStart = token.getCharPositionInLine();

// Find placeholders in the string using MultilingualStringAnalyser
var positions = MultilingualStringAnalyser.findPlaceholderPositions(tokenText);
for (var position : positions) {
helper.addEntry(
entries,
tokenLine,
tokenStart + position.start(),
position.length(),
SemanticTokenTypes.Parameter
);
}
}
}

private List<Token> getStringTokens(BSLParser.CallParamContext callParam) {
List<Token> stringTokens = new ArrayList<>();
var tokens = Trees.getTokens(callParam);

for (Token token : tokens) {
if (STRING_TOKEN_TYPES.contains(token.getType())) {
stringTokens.add(token);
}
}

return stringTokens;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,133 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Анализатор многоязычных строк НСтр (NStr).
* Анализатор многоязычных строк НСтр (NStr) и строковых шаблонов СтрШаблон (StrTemplate).
* <p>
* Проверяет наличие всех объявленных языков в многоязычных строках
* и анализирует использование в шаблонах.
* <p>
* Также предоставляет статические методы для поиска языковых ключей и плейсхолдеров
* в строках для целей семантической подсветки.
*/
public final class MultilingualStringAnalyser {

private static final String NSTR_METHOD_NAME = "^(НСтр|NStr)";
private static final String TEMPLATE_METHOD_NAME = "^(СтрШаблон|StrTemplate)";
private static final String NSTR_METHOD_NAME = "^(НСтр|NStr)$";
private static final String TEMPLATE_METHOD_NAME = "^(СтрШаблон|StrTemplate)$";
private static final String NSTR_LANG_REGEX = "\\w+\\s*=\\s*['|\"{2}]";
private static final String NSTR_LANG_CUT_REGEX = "\\s*=\\s*['|\"{2}]";
private static final String WHITE_SPACE_REGEX = "\\s";
private static final Pattern NSTR_METHOD_NAME_PATTERN = CaseInsensitivePattern.compile(

/**
* Паттерн для распознавания вызова метода НСтр/NStr.
*/
public static final Pattern NSTR_METHOD_NAME_PATTERN = CaseInsensitivePattern.compile(
NSTR_METHOD_NAME
);
private static final Pattern TEMPLATE_METHOD_NAME_PATTERN = CaseInsensitivePattern.compile(

/**
* Паттерн для распознавания вызова метода СтрШаблон/StrTemplate.
*/
public static final Pattern TEMPLATE_METHOD_NAME_PATTERN = CaseInsensitivePattern.compile(
TEMPLATE_METHOD_NAME
);
private static final Pattern NSTR_LANG_PATTERN = CaseInsensitivePattern.compile(

/**
* Паттерн для поиска языковых ключей в строках НСтр (например, ru=', en=").
*/
public static final Pattern NSTR_LANG_PATTERN = CaseInsensitivePattern.compile(
NSTR_LANG_REGEX
);

/**
* Паттерн для поиска плейсхолдеров в строках СтрШаблон (%1-%10 или %(1)-%(10)).
* Учитывает экранирование %%.
*/
public static final Pattern STR_TEMPLATE_PLACEHOLDER_PATTERN = Pattern.compile(
"(?<!%)%(?:(10|[1-9])(?!\\d)|\\((10|[1-9])\\))"
);

private static final Pattern NSTR_LANG_CUT_PATTERN = CaseInsensitivePattern.compile(
NSTR_LANG_CUT_REGEX
);
private static final Pattern WHITE_SPACE_PATTERN = CaseInsensitivePattern.compile(
WHITE_SPACE_REGEX
);

/**
* Запись для хранения позиции совпадения в строке.
*
* @param start Начальная позиция совпадения
* @param length Длина совпадения
* @param value Значение совпадения (языковой ключ или плейсхолдер)
*/
public record MatchPosition(int start, int length, String value) {
}

/**
* Проверить, является ли вызов метода вызовом НСтр/NStr.
*
* @param ctx Контекст вызова глобального метода
* @return true, если это вызов НСтр/NStr
*/
public static boolean isNStrCall(BSLParser.GlobalMethodCallContext ctx) {
return NSTR_METHOD_NAME_PATTERN.matcher(ctx.methodName().getText()).matches();
}

/**
* Проверить, является ли вызов метода вызовом СтрШаблон/StrTemplate.
*
* @param ctx Контекст вызова глобального метода
* @return true, если это вызов СтрШаблон/StrTemplate
*/
public static boolean isStrTemplateCall(BSLParser.GlobalMethodCallContext ctx) {
return TEMPLATE_METHOD_NAME_PATTERN.matcher(ctx.methodName().getText()).matches();
}

/**
* Найти позиции всех языковых ключей в строке НСтр.
*
* @param text Текст строки
* @return Список позиций языковых ключей
*/
public static List<MatchPosition> findLanguageKeyPositions(String text) {
List<MatchPosition> positions = new ArrayList<>();
Matcher matcher = NSTR_LANG_PATTERN.matcher(text);

while (matcher.find()) {
Matcher cutMatcher = NSTR_LANG_CUT_PATTERN.matcher(matcher.group());
String langKey = cutMatcher.replaceAll("");
int keyStart = matcher.start();
positions.add(new MatchPosition(keyStart, langKey.length(), langKey));
}

return positions;
}

/**
* Найти позиции всех плейсхолдеров в строке СтрШаблон.
*
* @param text Текст строки
* @return Список позиций плейсхолдеров
*/
public static List<MatchPosition> findPlaceholderPositions(String text) {
List<MatchPosition> positions = new ArrayList<>();
Matcher matcher = STR_TEMPLATE_PLACEHOLDER_PATTERN.matcher(text);

while (matcher.find()) {
String placeholder = matcher.group();
positions.add(new MatchPosition(matcher.start(), placeholder.length(), placeholder));
}

return positions;
}

@SuppressWarnings("NullAway.Init")
private BSLParser.GlobalMethodCallContext globalMethodCallContext;
private boolean isParentTemplate;
Expand All @@ -85,7 +178,7 @@ private static boolean isNotMultilingualString(BSLParser.GlobalMethodCallContext
String firstParameterMultilingualString = getMultilingualString(globalMethodCallContext);

return !(firstParameterMultilingualString.isEmpty() || firstParameterMultilingualString.startsWith("\""))
|| !NSTR_METHOD_NAME_PATTERN.matcher(globalMethodCallContext.methodName().getText()).find();
|| !NSTR_METHOD_NAME_PATTERN.matcher(globalMethodCallContext.methodName().getText()).matches();
}

private static boolean hasTemplateInParents(ParserRuleContext globalMethodCallContext) {
Expand All @@ -103,7 +196,7 @@ private static boolean hasTemplateInParents(ParserRuleContext globalMethodCallCo
}

private static boolean isTemplate(BSLParser.GlobalMethodCallContext parent) {
return TEMPLATE_METHOD_NAME_PATTERN.matcher(parent.methodName().getText()).find();
return TEMPLATE_METHOD_NAME_PATTERN.matcher(parent.methodName().getText()).matches();
}

private static @Nullable String getVariableName(BSLParser.GlobalMethodCallContext ctx) {
Expand Down
Loading