diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/symbol/SymbolTree.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/symbol/SymbolTree.java index 2b8da59b4f4..1a8d54e76eb 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/symbol/SymbolTree.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/symbol/SymbolTree.java @@ -27,7 +27,9 @@ import lombok.Getter; import lombok.Value; import org.antlr.v4.runtime.ParserRuleContext; +import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolKind; import java.util.ArrayList; import java.util.Collections; @@ -198,6 +200,35 @@ public Optional getVariableSymbol(String variableName, SourceDef ); } + /** + * Поиск самого вложенного символа, содержащего указанную позицию. + *

+ * Использует иерархический спуск по дереву символов вместо линейного поиска. + * + * @param position Позиция в документе. + * @return Символ, содержащий позицию, или символ модуля, если позиция вне всех символов. + */ + public SourceDefinedSymbol getSymbolAtPosition(Position position) { + return findSymbolAtPosition(module, position); + } + + private SourceDefinedSymbol findSymbolAtPosition(SourceDefinedSymbol parent, Position position) { + // Ищем среди детей символ, содержащий позицию + for (var child : parent.getChildren()) { + if (Ranges.containsPosition(child.getRange(), position)) { + // Рекурсивно ищем более вложенный символ + var found = findSymbolAtPosition(child, position); + // Пропускаем Namespace (Region) - ищем дальше или возвращаем parent + if (found.getSymbolKind() == SymbolKind.Namespace) { + continue; + } + return found; + } + } + // Если среди детей не нашли — возвращаем текущий символ + return parent; + } + private List createChildrenFlat() { List symbols = new ArrayList<>(); getChildren().forEach(child -> flatten(child, symbols)); diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java index 521c5707b76..2f060473426 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/references/ReferenceIndex.java @@ -43,7 +43,6 @@ import org.springframework.stereotype.Component; import java.net.URI; -import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -276,7 +275,7 @@ private Optional buildReference( return getSourceDefinedSymbol(symbolOccurrence.symbol()) .map((SourceDefinedSymbol symbol) -> { - SourceDefinedSymbol from = getFromSymbol(symbolOccurrence); + var from = getFromSymbol(symbolOccurrence); return new Reference(from, symbol, uri, range, occurrenceType); }) .filter(ReferenceIndex::isReferenceAccessible); @@ -307,20 +306,12 @@ private Optional getSourceDefinedSymbol(Symbol symbolEntity } private SourceDefinedSymbol getFromSymbol(SymbolOccurrence symbolOccurrence) { - var uri = symbolOccurrence.location().getUri(); var position = symbolOccurrence.location().getRange().getStart(); - var symbolTree = Optional.ofNullable(serverContext.getDocument(uri)) - .map(DocumentContext::getSymbolTree); - return symbolTree - .map(SymbolTree::getChildrenFlat) - .stream() - .flatMap(Collection::stream) - .filter(sourceDefinedSymbol -> sourceDefinedSymbol.getSymbolKind() != SymbolKind.Namespace) - .filter(symbol -> Ranges.containsPosition(symbol.getRange(), position)) - .findFirst() - .or(() -> symbolTree.map(SymbolTree::getModule)) + return Optional.ofNullable(serverContext.getDocument(uri)) + .map(DocumentContext::getSymbolTree) + .map(symbolTree -> symbolTree.getSymbolAtPosition(position)) .orElseThrow(); } diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/LexicalSemanticTokensSupplier.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/LexicalSemanticTokensSupplier.java index c765d9f0087..abc753bd94f 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/LexicalSemanticTokensSupplier.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/LexicalSemanticTokensSupplier.java @@ -30,15 +30,14 @@ import org.springframework.stereotype.Component; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Set; /** - * Сапплаер семантических токенов для лексических элементов: строк, чисел, операторов и ключевых слов. + * Сапплаер семантических токенов для лексических элементов: чисел, операторов и ключевых слов. *

- * Исключает строки, которые содержат запросы SDBL (они обрабатываются в {@link QuerySemanticTokensSupplier}). + * Строки обрабатываются в {@link StringSemanticTokensSupplier}. */ @Component @RequiredArgsConstructor @@ -53,8 +52,7 @@ public class LexicalSemanticTokensSupplier implements SemanticTokensSupplier { BSLLexer.STRING, BSLLexer.STRINGPART, BSLLexer.STRINGSTART, - BSLLexer.STRINGTAIL, - BSLLexer.PREPROC_STRING + BSLLexer.STRINGTAIL ); private static final Set OPERATOR_TYPES = Set.of( @@ -107,14 +105,13 @@ public class LexicalSemanticTokensSupplier implements SemanticTokensSupplier { public List getSemanticTokens(DocumentContext documentContext) { List entries = new ArrayList<>(); var tokensFromDefaultChannel = documentContext.getTokensFromDefaultChannel(); - var stringsWithQueries = collectStringsWithQueries(documentContext); for (Token token : tokensFromDefaultChannel) { var tokenType = token.getType(); var tokenText = Objects.toString(token.getText(), ""); if (!tokenText.isEmpty()) { - // Skip string tokens that contain SDBL tokens - they'll be handled by QuerySemanticTokensSupplier - if (STRING_TYPES.contains(tokenType) && stringsWithQueries.contains(token)) { + // Skip STRING tokens - they are handled by StringSemanticTokensSupplier + if (STRING_TYPES.contains(tokenType)) { continue; } selectAndAddSemanticToken(entries, token, tokenType); @@ -124,46 +121,15 @@ public List getSemanticTokens(DocumentContext documentContex return entries; } - private Set collectStringsWithQueries(DocumentContext documentContext) { - var queries = documentContext.getQueries(); - if (queries.isEmpty()) { - return Set.of(); - } - - var stringsToSkip = new HashSet(); - for (var query : queries) { - for (Token queryToken : query.getTokens()) { - if (queryToken.getChannel() != Token.DEFAULT_CHANNEL) { - continue; - } - int queryLine = queryToken.getLine(); - for (var bslToken : documentContext.getTokensFromDefaultChannel()) { - if (!STRING_TYPES.contains(bslToken.getType())) { - continue; - } - if (bslToken.getLine() == queryLine) { - var bslRange = Ranges.create(bslToken); - int queryStart = queryToken.getCharPositionInLine(); - int queryEnd = queryStart + queryToken.getText().length(); - if (queryStart >= bslRange.getStart().getCharacter() && queryEnd <= bslRange.getEnd().getCharacter()) { - stringsToSkip.add(bslToken); - } - } - } - } - } - return stringsToSkip; - } - private void selectAndAddSemanticToken(List entries, Token token, int tokenType) { // Skip '&' and all ANNOTATION_* symbol tokens here to avoid duplicate Decorator emission (handled via AST) if (tokenType == BSLLexer.AMPERSAND || ANNOTATION_TOKENS.contains(tokenType)) { return; } - if (STRING_TYPES.contains(tokenType)) { + if (tokenType == BSLLexer.DATETIME) { helper.addRange(entries, Ranges.create(token), SemanticTokenTypes.String); - } else if (tokenType == BSLLexer.DATETIME) { + } else if (tokenType == BSLLexer.PREPROC_STRING) { helper.addRange(entries, Ranges.create(token), SemanticTokenTypes.String); } else if (NUMBER_TYPES.contains(tokenType)) { helper.addRange(entries, Ranges.create(token), SemanticTokenTypes.Number); diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/QuerySemanticTokensSupplier.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/QuerySemanticTokensSupplier.java deleted file mode 100644 index 5b45116ba2d..00000000000 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/QuerySemanticTokensSupplier.java +++ /dev/null @@ -1,576 +0,0 @@ -/* - * 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.semantictokens; - -import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; -import com.github._1c_syntax.bsl.languageserver.utils.Ranges; -import com.github._1c_syntax.bsl.parser.BSLLexer; -import com.github._1c_syntax.bsl.parser.SDBLLexer; -import com.github._1c_syntax.bsl.parser.SDBLParser; -import com.github._1c_syntax.bsl.parser.SDBLParserBaseVisitor; -import lombok.RequiredArgsConstructor; -import org.antlr.v4.runtime.Token; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.SemanticTokenModifiers; -import org.eclipse.lsp4j.SemanticTokenTypes; -import org.jspecify.annotations.Nullable; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Сапплаер семантических токенов для запросов SDBL (язык запросов 1С). - *

- * Обрабатывает токены запросов и разделяет строки BSL, содержащие запросы. - */ -@Component -@RequiredArgsConstructor -public class QuerySemanticTokensSupplier implements SemanticTokensSupplier { - - private static final Set STRING_TYPES = Set.of( - BSLLexer.STRING, - BSLLexer.STRINGPART, - BSLLexer.STRINGSTART, - BSLLexer.STRINGTAIL - ); - - // SDBL (Query Language) token types - private static final Set SDBL_KEYWORDS = createSdblKeywords(); - private static final Set SDBL_FUNCTIONS = createSdblFunctions(); - private static final Set SDBL_METADATA_TYPES = createSdblMetadataTypes(); - private static final Set SDBL_LITERALS = createSdblLiterals(); - private static final Set SDBL_OPERATORS = createSdblOperators(); - private static final Set SDBL_STRINGS = Set.of(SDBLLexer.STR); - private static final Set SDBL_COMMENTS = Set.of(SDBLLexer.LINE_COMMENT); - private static final Set SDBL_EDS = Set.of( - SDBLLexer.EDS_CUBE, - SDBLLexer.EDS_TABLE, - SDBLLexer.EDS_CUBE_DIMTABLE - ); - private static final Set SDBL_NUMBERS = Set.of(SDBLLexer.DECIMAL, SDBLLexer.FLOAT); - - private static final String[] NO_MODIFIERS = new String[0]; - private static final String[] DEFAULT_LIBRARY = new String[]{SemanticTokenModifiers.DefaultLibrary}; - - private final SemanticTokensHelper helper; - - @Override - public List getSemanticTokens(DocumentContext documentContext) { - List entries = new ArrayList<>(); - var queries = documentContext.getQueries(); - - if (queries.isEmpty()) { - return entries; - } - - // Collect all SDBL tokens grouped by line - var sdblTokensByLine = new HashMap>(); - for (var query : queries) { - for (Token token : query.getTokens()) { - if (token.getChannel() != Token.DEFAULT_CHANNEL) { - continue; - } - int zeroIndexedLine = token.getLine() - 1; - sdblTokensByLine.computeIfAbsent(zeroIndexedLine, k -> new ArrayList<>()).add(token); - } - } - - if (sdblTokensByLine.isEmpty()) { - return entries; - } - - // Find and split BSL strings that contain SDBL tokens - var stringsToSkip = collectStringsWithQueries(documentContext, sdblTokensByLine); - addSplitStringTokens(entries, stringsToSkip, sdblTokensByLine); - - // Add all SDBL tokens - for (var query : queries) { - for (Token token : query.getTokens()) { - if (token.getChannel() != Token.DEFAULT_CHANNEL) { - continue; - } - addSdblToken(entries, token); - } - } - - // Add AST-based semantic tokens (aliases, field names, metadata names, etc.) - for (var query : queries) { - var visitor = new SdblSemanticTokensVisitor(helper, entries); - visitor.visit(query.getAst()); - } - - return entries; - } - - private Set collectStringsWithQueries(DocumentContext documentContext, HashMap> sdblTokensByLine) { - var stringsToSkip = new HashSet(); - var bslStringTokens = documentContext.getTokensFromDefaultChannel().stream() - .filter(token -> STRING_TYPES.contains(token.getType())) - .toList(); - - for (Token bslString : bslStringTokens) { - var stringRange = Ranges.create(bslString); - int stringLine = stringRange.getStart().getLine(); - - var sdblTokensOnLine = sdblTokensByLine.get(stringLine); - if (sdblTokensOnLine == null || sdblTokensOnLine.isEmpty()) { - continue; - } - - var hasOverlappingTokens = sdblTokensOnLine.stream() - .anyMatch(sdblToken -> { - var sdblRange = Ranges.create(sdblToken); - return Ranges.containsRange(stringRange, sdblRange); - }); - - if (hasOverlappingTokens) { - stringsToSkip.add(bslString); - } - } - - return stringsToSkip; - } - - private void addSplitStringTokens( - List entries, - Set stringsToSkip, - HashMap> sdblTokensByLine - ) { - int stringTypeIdx = helper.getTypeIndex(SemanticTokenTypes.String); - if (stringTypeIdx < 0) { - return; - } - - for (Token stringToken : stringsToSkip) { - var stringRange = Ranges.create(stringToken); - int stringLine = stringRange.getStart().getLine(); - - var sdblTokensOnLine = sdblTokensByLine.get(stringLine); - if (sdblTokensOnLine == null || sdblTokensOnLine.isEmpty()) { - continue; - } - - int stringStart = stringRange.getStart().getCharacter(); - int stringEnd = stringRange.getEnd().getCharacter(); - - var overlappingTokens = sdblTokensOnLine.stream() - .filter(sdblToken -> { - int sdblStart = sdblToken.getCharPositionInLine(); - int sdblEnd = sdblStart + (int) sdblToken.getText().codePoints().count(); - return sdblStart >= stringStart && sdblEnd <= stringEnd; - }) - .sorted(Comparator.comparingInt(Token::getCharPositionInLine)) - .toList(); - - if (overlappingTokens.isEmpty()) { - continue; - } - - // Split the STRING token around SDBL tokens - int currentPos = stringStart; - - for (Token sdblToken : overlappingTokens) { - int sdblStart = sdblToken.getCharPositionInLine(); - int sdblEnd = sdblStart + (int) sdblToken.getText().codePoints().count(); - - // Add string part before SDBL token - if (currentPos < sdblStart) { - entries.add(new SemanticTokenEntry( - stringLine, - currentPos, - sdblStart - currentPos, - stringTypeIdx, - 0 - )); - } - - currentPos = sdblEnd; - } - - // Add final string part after last SDBL token - if (currentPos < stringEnd) { - entries.add(new SemanticTokenEntry( - stringLine, - currentPos, - stringEnd - currentPos, - stringTypeIdx, - 0 - )); - } - } - } - - private void addSdblToken(List entries, Token token) { - var tokenType = token.getType(); - var semanticTypeAndModifiers = getSdblTokenTypeAndModifiers(tokenType); - if (semanticTypeAndModifiers != null) { - // ANTLR uses 1-indexed line numbers, convert to 0-indexed for LSP Range - int zeroIndexedLine = token.getLine() - 1; - int start = token.getCharPositionInLine(); - int length = (int) token.getText().codePoints().count(); - var range = new Range( - new Position(zeroIndexedLine, start), - new Position(zeroIndexedLine, start + length) - ); - helper.addRange(entries, range, semanticTypeAndModifiers.type, semanticTypeAndModifiers.modifiers); - } - } - - @Nullable - private SdblTokenTypeAndModifiers getSdblTokenTypeAndModifiers(int tokenType) { - if (SDBL_KEYWORDS.contains(tokenType)) { - return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Keyword, NO_MODIFIERS); - } else if (SDBL_FUNCTIONS.contains(tokenType)) { - return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Function, DEFAULT_LIBRARY); - } else if (SDBL_METADATA_TYPES.contains(tokenType) || SDBL_EDS.contains(tokenType)) { - return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Namespace, NO_MODIFIERS); - } else if (SDBL_LITERALS.contains(tokenType)) { - return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Keyword, NO_MODIFIERS); - } else if (SDBL_OPERATORS.contains(tokenType)) { - return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Operator, NO_MODIFIERS); - } else if (SDBL_STRINGS.contains(tokenType)) { - return new SdblTokenTypeAndModifiers(SemanticTokenTypes.String, NO_MODIFIERS); - } else if (SDBL_COMMENTS.contains(tokenType)) { - return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Comment, NO_MODIFIERS); - } else if (SDBL_NUMBERS.contains(tokenType)) { - return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Number, NO_MODIFIERS); - } - return null; - } - - private record SdblTokenTypeAndModifiers(String type, String[] modifiers) { - } - - /** - * Visitor for SDBL AST to add semantic tokens based on context. - */ - private static class SdblSemanticTokensVisitor extends SDBLParserBaseVisitor { - private final SemanticTokensHelper helper; - private final List entries; - - public SdblSemanticTokensVisitor(SemanticTokensHelper helper, List entries) { - this.helper = helper; - this.entries = entries; - } - - @Override - public Void visitQuery(SDBLParser.QueryContext ctx) { - var temporaryTableName = ctx.temporaryTableName; - if (temporaryTableName != null) { - helper.addContextRange(entries, temporaryTableName, SemanticTokenTypes.Variable, SemanticTokenModifiers.Declaration); - } - return super.visitQuery(ctx); - } - - @Override - public Void visitDataSource(SDBLParser.DataSourceContext ctx) { - var alias = ctx.alias(); - if (alias != null && alias.identifier() != null) { - helper.addTokenRange(entries, alias.identifier().getStart(), SemanticTokenTypes.Variable, SemanticTokenModifiers.Declaration); - } - return super.visitDataSource(ctx); - } - - @Override - public Void visitSelectedField(SDBLParser.SelectedFieldContext ctx) { - var alias = ctx.alias(); - if (alias != null && alias.identifier() != null) { - helper.addTokenRange(entries, alias.identifier().getStart(), SemanticTokenTypes.Variable, SemanticTokenModifiers.Declaration); - } - return super.visitSelectedField(ctx); - } - - @Override - public Void visitMdo(SDBLParser.MdoContext ctx) { - var tableName = ctx.tableName; - if (tableName != null) { - helper.addTokenRange(entries, tableName.getStart(), SemanticTokenTypes.Class); - } - return super.visitMdo(ctx); - } - - @Override - public Void visitVirtualTable(SDBLParser.VirtualTableContext ctx) { - var virtualTableNameToken = ctx.virtualTableName; - if (virtualTableNameToken != null) { - helper.addTokenRange(entries, virtualTableNameToken, SemanticTokenTypes.Method); - } - return super.visitVirtualTable(ctx); - } - - @Override - public Void visitTable(SDBLParser.TableContext ctx) { - var tableName = ctx.tableName; - if (tableName != null) { - helper.addTokenRange(entries, tableName.getStart(), SemanticTokenTypes.Variable); - } - - var objectTableName = ctx.objectTableName; - if (objectTableName != null) { - helper.addTokenRange(entries, objectTableName.getStart(), SemanticTokenTypes.Class); - } - - return super.visitTable(ctx); - } - - @Override - public Void visitColumn(SDBLParser.ColumnContext ctx) { - var identifiers = ctx.identifier(); - if (identifiers != null && !identifiers.isEmpty()) { - if (identifiers.size() == 1) { - helper.addTokenRange(entries, identifiers.get(0).getStart(), SemanticTokenTypes.Variable); - } else { - helper.addTokenRange(entries, identifiers.get(0).getStart(), SemanticTokenTypes.Variable); - helper.addTokenRange(entries, identifiers.get(identifiers.size() - 1).getStart(), SemanticTokenTypes.Property); - } - } - return super.visitColumn(ctx); - } - - @Override - public Void visitParameter(SDBLParser.ParameterContext ctx) { - var ampersand = ctx.AMPERSAND(); - var parameterName = ctx.name; - if (ampersand != null && parameterName != null) { - helper.addContextRange(entries, ctx, SemanticTokenTypes.Parameter, SemanticTokenModifiers.Readonly); - } - return super.visitParameter(ctx); - } - - @Override - public Void visitValueFunction(SDBLParser.ValueFunctionContext ctx) { - var type = ctx.type; - var mdoName = ctx.mdoName; - var predefinedName = ctx.predefinedName; - var emptyRef = ctx.emptyFer; - var systemName = ctx.systemName; - - if (type != null && mdoName != null) { - if (type.getType() == SDBLLexer.ENUM_TYPE) { - helper.addTokenRange(entries, mdoName.getStart(), SemanticTokenTypes.Enum); - } else { - helper.addTokenRange(entries, mdoName.getStart(), SemanticTokenTypes.Class); - } - - if (predefinedName != null) { - helper.addTokenRange(entries, predefinedName.getStart(), SemanticTokenTypes.EnumMember); - } else if (emptyRef != null) { - helper.addTokenRange(entries, emptyRef, SemanticTokenTypes.EnumMember); - } - } else if (systemName != null && predefinedName != null) { - helper.addTokenRange(entries, systemName.getStart(), SemanticTokenTypes.Enum); - helper.addTokenRange(entries, predefinedName.getStart(), SemanticTokenTypes.EnumMember); - } - - var routePointName = ctx.routePointName; - if (routePointName != null) { - helper.addTokenRange(entries, routePointName.getStart(), SemanticTokenTypes.EnumMember); - } - - return super.visitValueFunction(ctx); - } - } - - // SDBL token type factory methods - private static Set createSdblKeywords() { - return Set.of( - SDBLLexer.ALL, - SDBLLexer.ALLOWED, - SDBLLexer.AND, - SDBLLexer.AS, - SDBLLexer.ASC, - SDBLLexer.AUTOORDER, - SDBLLexer.BETWEEN, - SDBLLexer.BY_EN, - SDBLLexer.CASE, - SDBLLexer.CAST, - SDBLLexer.DESC, - SDBLLexer.DISTINCT, - SDBLLexer.DROP, - SDBLLexer.ELSE, - SDBLLexer.END, - SDBLLexer.ESCAPE, - SDBLLexer.FOR, - SDBLLexer.FROM, - SDBLLexer.FULL, - SDBLLexer.GROUP, - SDBLLexer.HAVING, - SDBLLexer.HIERARCHY, - SDBLLexer.HIERARCHY_FOR_IN, - SDBLLexer.IN, - SDBLLexer.INDEX, - SDBLLexer.INNER, - SDBLLexer.INTO, - SDBLLexer.IS, - SDBLLexer.JOIN, - SDBLLexer.LEFT, - SDBLLexer.LIKE, - SDBLLexer.NOT, - SDBLLexer.OF, - SDBLLexer.ONLY, - SDBLLexer.ON_EN, - SDBLLexer.OR, - SDBLLexer.ORDER, - SDBLLexer.OVERALL, - SDBLLexer.OUTER, - SDBLLexer.PERIODS, - SDBLLexer.PO_RU, - SDBLLexer.REFS, - SDBLLexer.RIGHT, - SDBLLexer.SELECT, - SDBLLexer.SET, - SDBLLexer.THEN, - SDBLLexer.TOP, - SDBLLexer.TOTALS, - SDBLLexer.UNION, - SDBLLexer.UPDATE, - SDBLLexer.WHEN, - SDBLLexer.WHERE, - SDBLLexer.EMPTYREF, - SDBLLexer.GROUPEDBY, - SDBLLexer.GROUPING - ); - } - - private static Set createSdblFunctions() { - return Set.of( - SDBLLexer.AVG, - SDBLLexer.BEGINOFPERIOD, - SDBLLexer.BOOLEAN, - SDBLLexer.COUNT, - SDBLLexer.DATE, - SDBLLexer.DATEADD, - SDBLLexer.DATEDIFF, - SDBLLexer.DATETIME, - SDBLLexer.DAY, - SDBLLexer.DAYOFYEAR, - SDBLLexer.EMPTYTABLE, - SDBLLexer.ENDOFPERIOD, - SDBLLexer.HALFYEAR, - SDBLLexer.HOUR, - SDBLLexer.ISNULL, - SDBLLexer.MAX, - SDBLLexer.MIN, - SDBLLexer.MINUTE, - SDBLLexer.MONTH, - SDBLLexer.NUMBER, - SDBLLexer.QUARTER, - SDBLLexer.PRESENTATION, - SDBLLexer.RECORDAUTONUMBER, - SDBLLexer.REFPRESENTATION, - SDBLLexer.SECOND, - SDBLLexer.STRING, - SDBLLexer.SUBSTRING, - SDBLLexer.SUM, - SDBLLexer.TENDAYS, - SDBLLexer.TYPE, - SDBLLexer.VALUE, - SDBLLexer.VALUETYPE, - SDBLLexer.WEEK, - SDBLLexer.WEEKDAY, - SDBLLexer.YEAR, - SDBLLexer.INT, - SDBLLexer.ACOS, - SDBLLexer.ASIN, - SDBLLexer.ATAN, - SDBLLexer.COS, - SDBLLexer.SIN, - SDBLLexer.TAN, - SDBLLexer.LOG, - SDBLLexer.LOG10, - SDBLLexer.EXP, - SDBLLexer.POW, - SDBLLexer.SQRT, - SDBLLexer.LOWER, - SDBLLexer.STRINGLENGTH, - SDBLLexer.TRIMALL, - SDBLLexer.TRIML, - SDBLLexer.TRIMR, - SDBLLexer.UPPER, - SDBLLexer.ROUND, - SDBLLexer.STOREDDATASIZE, - SDBLLexer.UUID, - SDBLLexer.STRFIND, - SDBLLexer.STRREPLACE - ); - } - - private static Set createSdblMetadataTypes() { - return Set.of( - SDBLLexer.ACCOUNTING_REGISTER_TYPE, - SDBLLexer.ACCUMULATION_REGISTER_TYPE, - SDBLLexer.BUSINESS_PROCESS_TYPE, - SDBLLexer.CALCULATION_REGISTER_TYPE, - SDBLLexer.CATALOG_TYPE, - SDBLLexer.CHART_OF_ACCOUNTS_TYPE, - SDBLLexer.CHART_OF_CALCULATION_TYPES_TYPE, - SDBLLexer.CHART_OF_CHARACTERISTIC_TYPES_TYPE, - SDBLLexer.CONSTANT_TYPE, - SDBLLexer.DOCUMENT_TYPE, - SDBLLexer.DOCUMENT_JOURNAL_TYPE, - SDBLLexer.ENUM_TYPE, - SDBLLexer.EXCHANGE_PLAN_TYPE, - SDBLLexer.EXTERNAL_DATA_SOURCE_TYPE, - SDBLLexer.FILTER_CRITERION_TYPE, - SDBLLexer.INFORMATION_REGISTER_TYPE, - SDBLLexer.SEQUENCE_TYPE, - SDBLLexer.TASK_TYPE - ); - } - - private static Set createSdblLiterals() { - return Set.of( - SDBLLexer.TRUE, - SDBLLexer.FALSE, - SDBLLexer.UNDEFINED, - SDBLLexer.NULL - ); - } - - private static Set createSdblOperators() { - return Set.of( - SDBLLexer.SEMICOLON, - SDBLLexer.DOT, - SDBLLexer.PLUS, - SDBLLexer.MINUS, - SDBLLexer.MUL, - SDBLLexer.QUOTIENT, - SDBLLexer.ASSIGN, - SDBLLexer.LESS_OR_EQUAL, - SDBLLexer.LESS, - SDBLLexer.NOT_EQUAL, - SDBLLexer.GREATER_OR_EQUAL, - SDBLLexer.GREATER, - SDBLLexer.COMMA, - SDBLLexer.BRACE, - SDBLLexer.BRACE_START, - SDBLLexer.NUMBER_SIGH - ); - } -} - 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 new file mode 100644 index 00000000000..69cc5155548 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplier.java @@ -0,0 +1,347 @@ +/* + * 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.semantictokens; + +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; +import com.github._1c_syntax.bsl.languageserver.semantictokens.strings.SdblAstTokenCollector; +import com.github._1c_syntax.bsl.languageserver.semantictokens.strings.SdblTokenTypes; +import com.github._1c_syntax.bsl.languageserver.semantictokens.strings.SpecialContextVisitor; +import com.github._1c_syntax.bsl.languageserver.semantictokens.strings.StringContext; +import com.github._1c_syntax.bsl.languageserver.semantictokens.strings.SubToken; +import com.github._1c_syntax.bsl.languageserver.semantictokens.strings.TokenPosition; +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 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.stereotype.Component; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Сапплаер семантических токенов для строк BSL и запросов SDBL. + *

+ * Централизованно обрабатывает все строковые токены и разбивает их на подтокены + * в зависимости от контекста: + *

+ */ +@Component +@RequiredArgsConstructor +public class StringSemanticTokensSupplier implements SemanticTokensSupplier { + + private static final Set STRING_TYPES = Set.of( + BSLLexer.STRING, + BSLLexer.STRINGPART, + BSLLexer.STRINGSTART, + BSLLexer.STRINGTAIL + ); + + private final SemanticTokensHelper helper; + + @Override + public List getSemanticTokens(DocumentContext documentContext) { + List entries = new ArrayList<>(); + + // Собираем информацию о контекстах строк + var specialStringContexts = collectSpecialStringContexts(documentContext); + var queryStringContexts = collectQueryStringContexts(documentContext); + + // Обрабатываем все строковые токены + var stringTokens = documentContext.getTokensFromDefaultChannel().stream() + .filter(token -> STRING_TYPES.contains(token.getType())) + .toList(); + + for (Token stringToken : stringTokens) { + processStringToken(entries, stringToken, specialStringContexts, queryStringContexts); + } + + return entries; + } + + private void processStringToken( + List entries, + Token stringToken, + Map specialContexts, + Map queryContexts + ) { + // Проверяем, является ли строка частью запроса + var queryContext = queryContexts.get(stringToken); + if (queryContext != null) { + processQueryString(entries, stringToken, queryContext); + return; + } + + // Проверяем специальные контексты (НСтр, СтрШаблон) + var context = specialContexts.get(stringToken); + if (context != null) { + processSpecialContext(entries, stringToken, context); + return; + } + + // Обычная строка - добавляем токен для всей строки + var stringRange = Ranges.create(stringToken); + helper.addRange(entries, stringRange, SemanticTokenTypes.String); + } + + private void processQueryString( + List entries, + Token stringToken, + QueryContext queryContext + ) { + var stringRange = Ranges.create(stringToken); + int stringLine = stringRange.getStart().getLine(); + int stringStart = stringRange.getStart().getCharacter(); + int stringEnd = stringRange.getEnd().getCharacter(); + + // Получаем SDBL токены на этой строке + var sdblTokensOnLine = queryContext.sdblTokensByLine().get(stringLine); + if (sdblTokensOnLine == null || sdblTokensOnLine.isEmpty()) { + // Нет SDBL токенов - добавляем как обычную строку + helper.addRange(entries, stringRange, SemanticTokenTypes.String); + return; + } + + // Фильтруем токены, которые находятся внутри этой строки + var overlappingTokens = sdblTokensOnLine.stream() + .filter(sdblToken -> { + int sdblStart = sdblToken.getCharPositionInLine(); + int sdblEnd = sdblStart + (int) sdblToken.getText().codePoints().count(); + return sdblStart >= stringStart && sdblEnd <= stringEnd; + }) + .sorted(Comparator.comparingInt(Token::getCharPositionInLine)) + .toList(); + + if (overlappingTokens.isEmpty()) { + helper.addRange(entries, stringRange, SemanticTokenTypes.String); + return; + } + + // Разбиваем строку на части и добавляем SDBL токены + int currentPos = stringStart; + var skipPositions = queryContext.skipPositions(); + + for (Token sdblToken : overlappingTokens) { + int sdblStart = sdblToken.getCharPositionInLine(); + int sdblEnd = sdblStart + (int) sdblToken.getText().codePoints().count(); + + // Проверяем, нужно ли пропустить этот токен (например, он уже был обработан как часть параметра) + if (skipPositions.contains(new TokenPosition(stringLine, sdblStart, sdblEnd - sdblStart))) { + continue; + } + + // Добавляем часть строки до SDBL токена + if (currentPos < sdblStart) { + helper.addEntry(entries, stringLine, currentPos, sdblStart - currentPos, SemanticTokenTypes.String); + } + + // Добавляем SDBL токен с учётом AST-переопределений и получаем фактическую длину + int actualLength = addSdblToken(entries, sdblToken, queryContext.astTokenOverrides()); + + currentPos = sdblStart + actualLength; + } + + // Добавляем финальную часть строки + if (currentPos < stringEnd) { + helper.addEntry(entries, stringLine, currentPos, stringEnd - currentPos, SemanticTokenTypes.String); + } + } + + private int addSdblToken( + List entries, + Token token, + Map astTokenOverrides + ) { + int zeroIndexedLine = token.getLine() - 1; + int start = token.getCharPositionInLine(); + int length = (int) token.getText().codePoints().count(); + + // Сначала проверяем AST-переопределение + var tokenPosition = new TokenPosition(zeroIndexedLine, start, length); + var astOverride = astTokenOverrides.get(tokenPosition); + if (astOverride != null) { + // Используем переопределённую длину, если она задана + int effectiveLength = astOverride.overrideLength() > 0 ? astOverride.overrideLength() : length; + var range = new Range( + new Position(zeroIndexedLine, start), + new Position(zeroIndexedLine, start + effectiveLength) + ); + helper.addRange(entries, range, astOverride.type(), astOverride.modifiers()); + return effectiveLength; + } + + // Иначе используем тип на основе лексера + var tokenType = token.getType(); + var semanticTypeAndModifiers = SdblTokenTypes.getTokenTypeAndModifiers(tokenType); + if (semanticTypeAndModifiers != null) { + var range = new Range( + new Position(zeroIndexedLine, start), + new Position(zeroIndexedLine, start + length) + ); + helper.addRange(entries, range, semanticTypeAndModifiers.type(), semanticTypeAndModifiers.modifiers()); + } + return length; + } + + private void processSpecialContext( + List entries, + Token stringToken, + StringContext context + ) { + var stringRange = Ranges.create(stringToken); + String tokenText = stringToken.getText(); + int tokenLine = stringToken.getLine() - 1; // 0-indexed + int tokenStart = stringToken.getCharPositionInLine(); + int stringEnd = stringRange.getEnd().getCharacter(); + + List subTokens = new ArrayList<>(); + + if (context == StringContext.NSTR || context == StringContext.NSTR_AND_STR_TEMPLATE) { + var positions = MultilingualStringAnalyser.findLanguageKeyPositions(tokenText); + for (var position : positions) { + subTokens.add(new SubToken( + tokenStart + position.start(), + position.length(), + SemanticTokenTypes.Property + )); + } + } + + if (context == StringContext.STR_TEMPLATE || context == StringContext.NSTR_AND_STR_TEMPLATE) { + var positions = MultilingualStringAnalyser.findPlaceholderPositions(tokenText); + for (var position : positions) { + subTokens.add(new SubToken( + tokenStart + position.start(), + position.length(), + SemanticTokenTypes.Parameter + )); + } + } + + if (subTokens.isEmpty()) { + helper.addRange(entries, stringRange, SemanticTokenTypes.String); + return; + } + + subTokens.sort(Comparator.comparingInt(SubToken::start)); + + // Разбиваем строку на части вокруг подтокенов + int currentPos = tokenStart; + + for (SubToken subToken : subTokens) { + if (currentPos < subToken.start()) { + helper.addEntry(entries, tokenLine, currentPos, subToken.start() - currentPos, SemanticTokenTypes.String); + } + helper.addEntry(entries, tokenLine, subToken.start(), subToken.length(), subToken.type()); + currentPos = subToken.start() + subToken.length(); + } + + if (currentPos < stringEnd) { + helper.addEntry(entries, tokenLine, currentPos, stringEnd - currentPos, SemanticTokenTypes.String); + } + } + + private Map collectSpecialStringContexts(DocumentContext documentContext) { + Map contexts = new HashMap<>(); + var visitor = new SpecialContextVisitor(contexts); + visitor.visit(documentContext.getAst()); + return contexts; + } + + private Map collectQueryStringContexts(DocumentContext documentContext) { + Map contexts = new HashMap<>(); + var queries = documentContext.getQueries(); + + if (queries.isEmpty()) { + return contexts; + } + + // Собираем SDBL токены по строкам + var sdblTokensByLine = new HashMap>(); + for (var query : queries) { + for (Token token : query.getTokens()) { + if (token.getChannel() != Token.DEFAULT_CHANNEL) { + continue; + } + int zeroIndexedLine = token.getLine() - 1; + sdblTokensByLine.computeIfAbsent(zeroIndexedLine, k -> new ArrayList<>()).add(token); + } + } + + // Собираем AST-based переопределения типов токенов и позиции для пропуска + var astTokenOverrides = new HashMap(); + var skipPositions = new HashSet(); + for (var query : queries) { + var collector = new SdblAstTokenCollector(astTokenOverrides, skipPositions); + collector.visit(query.getAst()); + } + + // Определяем, какие строки содержат запросы + var stringTokens = documentContext.getTokensFromDefaultChannel().stream() + .filter(token -> STRING_TYPES.contains(token.getType())) + .toList(); + + var queryContext = new QueryContext(sdblTokensByLine, astTokenOverrides, skipPositions); + + for (Token stringToken : stringTokens) { + var stringRange = Ranges.create(stringToken); + int stringLine = stringRange.getStart().getLine(); + + var sdblTokensOnLine = sdblTokensByLine.get(stringLine); + if (sdblTokensOnLine == null || sdblTokensOnLine.isEmpty()) { + continue; + } + + // Проверяем, есть ли SDBL токены внутри этой строки + int stringStart = stringRange.getStart().getCharacter(); + int stringEnd = stringRange.getEnd().getCharacter(); + + boolean hasOverlapping = sdblTokensOnLine.stream() + .anyMatch(sdblToken -> { + int sdblStart = sdblToken.getCharPositionInLine(); + int sdblEnd = sdblStart + (int) sdblToken.getText().codePoints().count(); + return sdblStart >= stringStart && sdblEnd <= stringEnd; + }); + + if (hasOverlapping) { + contexts.put(stringToken, queryContext); + } + } + + return contexts; + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/SymbolsSemanticTokensSupplier.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/SymbolsSemanticTokensSupplier.java index 588c87084f5..f19b1e59287 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/SymbolsSemanticTokensSupplier.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/SymbolsSemanticTokensSupplier.java @@ -26,8 +26,8 @@ import com.github._1c_syntax.bsl.languageserver.context.symbol.VariableSymbol; import com.github._1c_syntax.bsl.languageserver.context.symbol.variable.VariableKind; import com.github._1c_syntax.bsl.languageserver.references.ReferenceIndex; -import com.github._1c_syntax.bsl.languageserver.references.ReferenceResolver; import com.github._1c_syntax.bsl.languageserver.references.model.OccurrenceType; +import com.github._1c_syntax.bsl.languageserver.references.model.Reference; import com.github._1c_syntax.bsl.languageserver.utils.Ranges; import lombok.RequiredArgsConstructor; import org.eclipse.lsp4j.SemanticTokenModifiers; @@ -45,7 +45,6 @@ @RequiredArgsConstructor public class SymbolsSemanticTokensSupplier implements SemanticTokensSupplier { - private final ReferenceResolver referenceResolver; private final ReferenceIndex referenceIndex; private final SemanticTokensHelper helper; @@ -64,32 +63,24 @@ public List getSemanticTokens(DocumentContext documentContex } } - // Add variable symbols + // Add explicit variable declarations from SymbolTree for (var variableSymbol : symbolTree.getVariables()) { if (variableSymbol.getKind() == VariableKind.PARAMETER) { continue; } - var nameRange = variableSymbol.getVariableNameRange(); if (!Ranges.isEmpty(nameRange)) { - boolean isDefinition = referenceResolver.findReference(documentContext.getUri(), nameRange.getStart()) - .map(ref -> ref.getOccurrenceType() == OccurrenceType.DEFINITION) - .orElse(false); - if (isDefinition) { - helper.addRange(entries, nameRange, SemanticTokenTypes.Variable, SemanticTokenModifiers.Definition); - } else { - helper.addRange(entries, nameRange, SemanticTokenTypes.Variable); - } + helper.addRange(entries, nameRange, SemanticTokenTypes.Variable, SemanticTokenModifiers.Definition); } } - // Add variable references from ReferenceIndex + // Add variable references from ReferenceIndex (includes both definitions and usages) var references = referenceIndex.getReferencesFrom(uri, SymbolKind.Variable); references.stream() - .filter(reference -> reference.isSourceDefinedSymbolReference()) + .filter(Reference::isSourceDefinedSymbolReference) .forEach(reference -> reference.getSourceDefinedSymbol() - .filter(symbol -> symbol instanceof VariableSymbol) - .map(symbol -> (VariableSymbol) symbol) + .filter(VariableSymbol.class::isInstance) + .map(VariableSymbol.class::cast) .ifPresent(variableSymbol -> { var tokenType = variableSymbol.getKind() == VariableKind.PARAMETER ? SemanticTokenTypes.Parameter diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/AstTokenInfo.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/AstTokenInfo.java new file mode 100644 index 00000000000..9170c471aae --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/AstTokenInfo.java @@ -0,0 +1,42 @@ +/* + * 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.semantictokens.strings; + +/** + * Информация об AST-токене с типом и модификаторами. + * + * @param type тип семантического токена (из SemanticTokenTypes) + * @param modifiers модификаторы токена (из SemanticTokenModifiers) + * @param overrideLength если > 0, используется вместо длины лексерного токена + */ +public record AstTokenInfo(String type, String[] modifiers, int overrideLength) { + /** + * Создаёт информацию о токене без переопределения длины. + * + * @param type тип семантического токена + * @param modifiers модификаторы токена + */ + public AstTokenInfo(String type, String[] modifiers) { + this(type, modifiers, 0); + } +} + diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/QueryContext.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/QueryContext.java new file mode 100644 index 00000000000..a212a7ad95d --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/QueryContext.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.semantictokens.strings; + +import org.antlr.v4.runtime.Token; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Контекст для строк, содержащих запросы SDBL. + * + * @param sdblTokensByLine токены SDBL, сгруппированные по номеру строки + * @param astTokenOverrides переопределения типов токенов на основе AST + * @param skipPositions позиции токенов, которые нужно пропустить при обработке + */ +public record QueryContext( + Map> sdblTokensByLine, + Map astTokenOverrides, + Set skipPositions +) { +} + diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SdblAstTokenCollector.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SdblAstTokenCollector.java new file mode 100644 index 00000000000..c4069ca2910 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SdblAstTokenCollector.java @@ -0,0 +1,204 @@ +/* + * 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.semantictokens.strings; + +import com.github._1c_syntax.bsl.parser.SDBLLexer; +import com.github._1c_syntax.bsl.parser.SDBLParser; +import com.github._1c_syntax.bsl.parser.SDBLParserBaseVisitor; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.eclipse.lsp4j.SemanticTokenModifiers; +import org.eclipse.lsp4j.SemanticTokenTypes; + +import java.util.Map; +import java.util.Set; + +/** + * Visitor для сбора AST-based переопределений токенов SDBL. + *

+ * Собирает информацию о типах токенов на основе контекста AST, + * что позволяет более точно определить семантику токенов + * (алиасы, имена таблиц, колонки, параметры и т.д.). + */ +public class SdblAstTokenCollector extends SDBLParserBaseVisitor { + + private static final String[] NO_MODIFIERS = new String[0]; + private static final String[] DECLARATION_MODIFIER = new String[]{SemanticTokenModifiers.Declaration}; + private static final String[] READONLY_MODIFIER = new String[]{SemanticTokenModifiers.Readonly}; + + private final Map astTokenOverrides; + private final Set skipPositions; + + /** + * Создаёт коллектор AST-токенов. + * + * @param astTokenOverrides Map для заполнения переопределениями типов токенов + * @param skipPositions Set позиций токенов для пропуска при обработке + */ + public SdblAstTokenCollector(Map astTokenOverrides, Set skipPositions) { + this.astTokenOverrides = astTokenOverrides; + this.skipPositions = skipPositions; + } + + private void addTokenOverride(Token token, String type, String[] modifiers) { + int line = token.getLine() - 1; + int start = token.getCharPositionInLine(); + int length = (int) token.getText().codePoints().count(); + astTokenOverrides.put(new TokenPosition(line, start, length), new AstTokenInfo(type, modifiers)); + } + + @Override + public Void visitQuery(SDBLParser.QueryContext ctx) { + var temporaryTableName = ctx.temporaryTableName; + if (temporaryTableName != null) { + addContextTokens(temporaryTableName, SemanticTokenTypes.Variable, DECLARATION_MODIFIER); + } + return super.visitQuery(ctx); + } + + private void addContextTokens(ParserRuleContext ctx, String type, String[] modifiers) { + addTokenOverride(ctx.getStart(), type, modifiers); + } + + @Override + public Void visitDataSource(SDBLParser.DataSourceContext ctx) { + var alias = ctx.alias(); + if (alias != null && alias.identifier() != null) { + addTokenOverride(alias.identifier().getStart(), SemanticTokenTypes.Variable, DECLARATION_MODIFIER); + } + return super.visitDataSource(ctx); + } + + @Override + public Void visitSelectedField(SDBLParser.SelectedFieldContext ctx) { + var alias = ctx.alias(); + if (alias != null && alias.identifier() != null) { + addTokenOverride(alias.identifier().getStart(), SemanticTokenTypes.Variable, DECLARATION_MODIFIER); + } + return super.visitSelectedField(ctx); + } + + @Override + public Void visitMdo(SDBLParser.MdoContext ctx) { + var tableName = ctx.tableName; + if (tableName != null) { + addTokenOverride(tableName.getStart(), SemanticTokenTypes.Class, NO_MODIFIERS); + } + return super.visitMdo(ctx); + } + + @Override + public Void visitVirtualTable(SDBLParser.VirtualTableContext ctx) { + var virtualTableNameToken = ctx.virtualTableName; + if (virtualTableNameToken != null) { + addTokenOverride(virtualTableNameToken, SemanticTokenTypes.Method, NO_MODIFIERS); + } + return super.visitVirtualTable(ctx); + } + + @Override + public Void visitTable(SDBLParser.TableContext ctx) { + var tableName = ctx.tableName; + if (tableName != null) { + addTokenOverride(tableName.getStart(), SemanticTokenTypes.Variable, NO_MODIFIERS); + } + + var objectTableName = ctx.objectTableName; + if (objectTableName != null) { + addTokenOverride(objectTableName.getStart(), SemanticTokenTypes.Class, NO_MODIFIERS); + } + + return super.visitTable(ctx); + } + + @Override + public Void visitColumn(SDBLParser.ColumnContext ctx) { + var identifiers = ctx.identifier(); + if (identifiers != null && !identifiers.isEmpty()) { + if (identifiers.size() == 1) { + addTokenOverride(identifiers.get(0).getStart(), SemanticTokenTypes.Variable, NO_MODIFIERS); + } else { + addTokenOverride(identifiers.get(0).getStart(), SemanticTokenTypes.Variable, NO_MODIFIERS); + addTokenOverride(identifiers.get(identifiers.size() - 1).getStart(), SemanticTokenTypes.Property, NO_MODIFIERS); + } + } + return super.visitColumn(ctx); + } + + @Override + public Void visitParameter(SDBLParser.ParameterContext ctx) { + var ampersand = ctx.AMPERSAND(); + var parameterName = ctx.name; + if (ampersand != null && parameterName != null) { + // Для параметра добавляем override по токену & + // с увеличенной длиной, охватывающей весь параметр (& + имя) + var ampersandToken = ampersand.getSymbol(); + int line = ampersandToken.getLine() - 1; + int start = ampersandToken.getCharPositionInLine(); + int ampersandLength = (int) ampersandToken.getText().codePoints().count(); + int nameLength = (int) parameterName.getText().codePoints().count(); + int totalLength = ampersandLength + nameLength; + + // Добавляем override для токена & с увеличенной длиной (весь параметр) + astTokenOverrides.put(new TokenPosition(line, start, ampersandLength), + new AstTokenInfo(SemanticTokenTypes.Parameter, READONLY_MODIFIER, totalLength)); + + // Добавляем позицию токена-имени параметра в skipPositions, чтобы его не обрабатывать отдельно + skipPositions.add(new TokenPosition(line, start + ampersandLength, nameLength)); + } + return super.visitParameter(ctx); + } + + @Override + public Void visitValueFunction(SDBLParser.ValueFunctionContext ctx) { + var type = ctx.type; + var mdoName = ctx.mdoName; + var predefinedName = ctx.predefinedName; + var emptyRef = ctx.emptyFer; + var systemName = ctx.systemName; + + if (type != null && mdoName != null) { + if (type.getType() == SDBLLexer.ENUM_TYPE) { + addTokenOverride(mdoName.getStart(), SemanticTokenTypes.Enum, NO_MODIFIERS); + } else { + addTokenOverride(mdoName.getStart(), SemanticTokenTypes.Class, NO_MODIFIERS); + } + + if (predefinedName != null) { + addTokenOverride(predefinedName.getStart(), SemanticTokenTypes.EnumMember, NO_MODIFIERS); + } else if (emptyRef != null) { + addTokenOverride(emptyRef, SemanticTokenTypes.EnumMember, NO_MODIFIERS); + } + } else if (systemName != null && predefinedName != null) { + addTokenOverride(systemName.getStart(), SemanticTokenTypes.Enum, NO_MODIFIERS); + addTokenOverride(predefinedName.getStart(), SemanticTokenTypes.EnumMember, NO_MODIFIERS); + } + + var routePointName = ctx.routePointName; + if (routePointName != null) { + addTokenOverride(routePointName.getStart(), SemanticTokenTypes.EnumMember, NO_MODIFIERS); + } + + return super.visitValueFunction(ctx); + } +} + diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SdblTokenTypes.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SdblTokenTypes.java new file mode 100644 index 00000000000..3d07bfdfbb4 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SdblTokenTypes.java @@ -0,0 +1,273 @@ +/* + * 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.semantictokens.strings; + +import com.github._1c_syntax.bsl.parser.SDBLLexer; +import org.eclipse.lsp4j.SemanticTokenModifiers; +import org.eclipse.lsp4j.SemanticTokenTypes; +import org.jspecify.annotations.Nullable; + +import java.util.Set; + +/** + * Утилитный класс для определения типов токенов SDBL. + *

+ * Содержит наборы токенов для различных категорий (ключевые слова, функции, + * типы метаданных и т.д.) и методы для определения семантического типа токена. + */ +public final class SdblTokenTypes { + + private static final Set SDBL_KEYWORDS = createSdblKeywords(); + private static final Set SDBL_FUNCTIONS = createSdblFunctions(); + private static final Set SDBL_METADATA_TYPES = createSdblMetadataTypes(); + private static final Set SDBL_LITERALS = createSdblLiterals(); + private static final Set SDBL_OPERATORS = createSdblOperators(); + private static final Set SDBL_STRINGS = Set.of(SDBLLexer.STR); + private static final Set SDBL_COMMENTS = Set.of(SDBLLexer.LINE_COMMENT); + private static final Set SDBL_EDS = Set.of( + SDBLLexer.EDS_CUBE, + SDBLLexer.EDS_TABLE, + SDBLLexer.EDS_CUBE_DIMTABLE + ); + private static final Set SDBL_NUMBERS = Set.of(SDBLLexer.DECIMAL, SDBLLexer.FLOAT); + + private static final String[] NO_MODIFIERS = new String[0]; + private static final String[] DEFAULT_LIBRARY = new String[]{SemanticTokenModifiers.DefaultLibrary}; + + private SdblTokenTypes() { + // Utility class + } + + /** + * Тип и модификаторы токена SDBL. + * + * @param type тип семантического токена + * @param modifiers модификаторы токена + */ + public record SdblTokenTypeAndModifiers(String type, String[] modifiers) { + } + + /** + * Определяет тип и модификаторы для токена SDBL. + * + * @param tokenType тип токена из лексера + * @return тип и модификаторы или null если токен не распознан + */ + @Nullable + public static SdblTokenTypeAndModifiers getTokenTypeAndModifiers(int tokenType) { + if (SDBL_KEYWORDS.contains(tokenType)) { + return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Keyword, NO_MODIFIERS); + } else if (SDBL_FUNCTIONS.contains(tokenType)) { + return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Function, DEFAULT_LIBRARY); + } else if (SDBL_METADATA_TYPES.contains(tokenType) || SDBL_EDS.contains(tokenType)) { + return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Namespace, NO_MODIFIERS); + } else if (SDBL_LITERALS.contains(tokenType)) { + return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Keyword, NO_MODIFIERS); + } else if (SDBL_OPERATORS.contains(tokenType)) { + return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Operator, NO_MODIFIERS); + } else if (SDBL_STRINGS.contains(tokenType)) { + return new SdblTokenTypeAndModifiers(SemanticTokenTypes.String, NO_MODIFIERS); + } else if (SDBL_COMMENTS.contains(tokenType)) { + return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Comment, NO_MODIFIERS); + } else if (SDBL_NUMBERS.contains(tokenType)) { + return new SdblTokenTypeAndModifiers(SemanticTokenTypes.Number, NO_MODIFIERS); + } + return null; + } + + private static Set createSdblKeywords() { + return Set.of( + SDBLLexer.ALL, + SDBLLexer.ALLOWED, + SDBLLexer.AND, + SDBLLexer.AS, + SDBLLexer.ASC, + SDBLLexer.AUTOORDER, + SDBLLexer.BETWEEN, + SDBLLexer.BY_EN, + SDBLLexer.CASE, + SDBLLexer.CAST, + SDBLLexer.DESC, + SDBLLexer.DISTINCT, + SDBLLexer.DROP, + SDBLLexer.ELSE, + SDBLLexer.END, + SDBLLexer.ESCAPE, + SDBLLexer.FOR, + SDBLLexer.FROM, + SDBLLexer.FULL, + SDBLLexer.GROUP, + SDBLLexer.HAVING, + SDBLLexer.HIERARCHY, + SDBLLexer.HIERARCHY_FOR_IN, + SDBLLexer.IN, + SDBLLexer.INDEX, + SDBLLexer.INNER, + SDBLLexer.INTO, + SDBLLexer.IS, + SDBLLexer.JOIN, + SDBLLexer.LEFT, + SDBLLexer.LIKE, + SDBLLexer.NOT, + SDBLLexer.OF, + SDBLLexer.ONLY, + SDBLLexer.ON_EN, + SDBLLexer.OR, + SDBLLexer.ORDER, + SDBLLexer.OVERALL, + SDBLLexer.OUTER, + SDBLLexer.PERIODS, + SDBLLexer.PO_RU, + SDBLLexer.REFS, + SDBLLexer.RIGHT, + SDBLLexer.SELECT, + SDBLLexer.SET, + SDBLLexer.THEN, + SDBLLexer.TOP, + SDBLLexer.TOTALS, + SDBLLexer.UNION, + SDBLLexer.UPDATE, + SDBLLexer.WHEN, + SDBLLexer.WHERE, + SDBLLexer.EMPTYREF, + SDBLLexer.GROUPEDBY, + SDBLLexer.GROUPING + ); + } + + private static Set createSdblFunctions() { + return Set.of( + SDBLLexer.AVG, + SDBLLexer.BEGINOFPERIOD, + SDBLLexer.BOOLEAN, + SDBLLexer.COUNT, + SDBLLexer.DATE, + SDBLLexer.DATEADD, + SDBLLexer.DATEDIFF, + SDBLLexer.DATETIME, + SDBLLexer.DAY, + SDBLLexer.DAYOFYEAR, + SDBLLexer.EMPTYTABLE, + SDBLLexer.ENDOFPERIOD, + SDBLLexer.HALFYEAR, + SDBLLexer.HOUR, + SDBLLexer.ISNULL, + SDBLLexer.MAX, + SDBLLexer.MIN, + SDBLLexer.MINUTE, + SDBLLexer.MONTH, + SDBLLexer.NUMBER, + SDBLLexer.QUARTER, + SDBLLexer.PRESENTATION, + SDBLLexer.RECORDAUTONUMBER, + SDBLLexer.REFPRESENTATION, + SDBLLexer.SECOND, + SDBLLexer.STRING, + SDBLLexer.SUBSTRING, + SDBLLexer.SUM, + SDBLLexer.TENDAYS, + SDBLLexer.TYPE, + SDBLLexer.VALUE, + SDBLLexer.VALUETYPE, + SDBLLexer.WEEK, + SDBLLexer.WEEKDAY, + SDBLLexer.YEAR, + SDBLLexer.INT, + SDBLLexer.ACOS, + SDBLLexer.ASIN, + SDBLLexer.ATAN, + SDBLLexer.COS, + SDBLLexer.SIN, + SDBLLexer.TAN, + SDBLLexer.LOG, + SDBLLexer.LOG10, + SDBLLexer.EXP, + SDBLLexer.POW, + SDBLLexer.SQRT, + SDBLLexer.LOWER, + SDBLLexer.STRINGLENGTH, + SDBLLexer.TRIMALL, + SDBLLexer.TRIML, + SDBLLexer.TRIMR, + SDBLLexer.UPPER, + SDBLLexer.ROUND, + SDBLLexer.STOREDDATASIZE, + SDBLLexer.UUID, + SDBLLexer.STRFIND, + SDBLLexer.STRREPLACE + ); + } + + private static Set createSdblMetadataTypes() { + return Set.of( + SDBLLexer.ACCOUNTING_REGISTER_TYPE, + SDBLLexer.ACCUMULATION_REGISTER_TYPE, + SDBLLexer.BUSINESS_PROCESS_TYPE, + SDBLLexer.CALCULATION_REGISTER_TYPE, + SDBLLexer.CATALOG_TYPE, + SDBLLexer.CHART_OF_ACCOUNTS_TYPE, + SDBLLexer.CHART_OF_CALCULATION_TYPES_TYPE, + SDBLLexer.CHART_OF_CHARACTERISTIC_TYPES_TYPE, + SDBLLexer.CONSTANT_TYPE, + SDBLLexer.DOCUMENT_TYPE, + SDBLLexer.DOCUMENT_JOURNAL_TYPE, + SDBLLexer.ENUM_TYPE, + SDBLLexer.EXCHANGE_PLAN_TYPE, + SDBLLexer.EXTERNAL_DATA_SOURCE_TYPE, + SDBLLexer.FILTER_CRITERION_TYPE, + SDBLLexer.INFORMATION_REGISTER_TYPE, + SDBLLexer.SEQUENCE_TYPE, + SDBLLexer.TASK_TYPE + ); + } + + private static Set createSdblLiterals() { + return Set.of( + SDBLLexer.TRUE, + SDBLLexer.FALSE, + SDBLLexer.UNDEFINED, + SDBLLexer.NULL + ); + } + + private static Set createSdblOperators() { + return Set.of( + SDBLLexer.SEMICOLON, + SDBLLexer.DOT, + SDBLLexer.PLUS, + SDBLLexer.MINUS, + SDBLLexer.MUL, + SDBLLexer.QUOTIENT, + SDBLLexer.ASSIGN, + SDBLLexer.LESS_OR_EQUAL, + SDBLLexer.LESS, + SDBLLexer.NOT_EQUAL, + SDBLLexer.GREATER_OR_EQUAL, + SDBLLexer.GREATER, + SDBLLexer.COMMA, + SDBLLexer.BRACE, + SDBLLexer.BRACE_START, + SDBLLexer.NUMBER_SIGH + ); + } +} + 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 new file mode 100644 index 00000000000..de314e65eee --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SpecialContextVisitor.java @@ -0,0 +1,263 @@ +/* + * 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.semantictokens.strings; + +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; +import com.github._1c_syntax.bsl.parser.BSLParser; +import com.github._1c_syntax.bsl.parser.BSLParserBaseVisitor; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.Token; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Visitor для поиска вызовов НСтр и СтрШаблон. + *

+ * Собирает информацию о строковых токенах, находящихся в контексте + * вызовов НСтр/NStr и СтрШаблон/StrTemplate. + *

+ * Также поддерживает поиск строк-шаблонов, которые присвоены переменным, + * а затем используются в вызове СтрШаблон. + */ +public class SpecialContextVisitor extends BSLParserBaseVisitor { + + private static final Set STRING_TYPES = Set.of( + BSLLexer.STRING, + BSLLexer.STRINGPART, + BSLLexer.STRINGSTART, + BSLLexer.STRINGTAIL + ); + + private final Map contexts; + + /** + * Создаёт visitor для сбора контекстов строк. + * + * @param contexts Map для заполнения контекстами строк + */ + public SpecialContextVisitor(Map contexts) { + this.contexts = contexts; + } + + @Override + public Void visitGlobalMethodCall(BSLParser.GlobalMethodCallContext ctx) { + StringContext context = null; + + if (MultilingualStringAnalyser.isNStrCall(ctx)) { + context = StringContext.NSTR; + } else if (MultilingualStringAnalyser.isStrTemplateCall(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); + + if (stringTokens.isEmpty() && context == StringContext.STR_TEMPLATE) { + // Первый параметр не строковый литерал - возможно, это переменная + // Пытаемся найти присвоение этой переменной + stringTokens = findStringTokensFromVariable(firstParam, ctx); + } + + for (Token token : stringTokens) { + contexts.merge(token, context, StringContext::combine); + } + } + } + + return super.visitGlobalMethodCall(ctx); + } + + private List getStringTokensFromParam(BSLParser.CallParamContext callParam) { + List stringTokens = new ArrayList<>(); + var tokens = Trees.getTokens(callParam); + + for (Token token : tokens) { + if (STRING_TYPES.contains(token.getType())) { + stringTokens.add(token); + } + } + + return stringTokens; + } + + /** + * Ищет строковые токены из присвоения переменной, используемой в СтрШаблон. + *

+ * Например, для кода: + *

+   * НовыйШаблон = "%1 %2";
+   * Результат = СтрШаблон(НовыйШаблон, А, Б);
+   * 
+ * найдёт строку "%1 %2" и вернёт её токены. + *

+ * Также поддерживает случай: + *

+   * Шаблон = НСтр("ru = 'Сценарий %1'");
+   * Результат = СтрШаблон(Шаблон, Параметр);
+   * 
+ * найдёт строку "ru = 'Сценарий %1'" и вернёт её токены. + */ + private List findStringTokensFromVariable( + BSLParser.CallParamContext callParam, + BSLParser.GlobalMethodCallContext callContext + ) { + // Получаем имя переменной из первого параметра + var varName = extractVariableName(callParam); + if (varName == null) { + return List.of(); + } + + // Ищем присвоение этой переменной выше по коду + var assignmentExpression = findAssignedExpression(varName, callContext); + if (assignmentExpression == null) { + return List.of(); + } + + // Извлекаем строковые токены из присвоения + return extractStringTokensFromExpression(assignmentExpression); + } + + @Nullable + private String extractVariableName(BSLParser.CallParamContext callParam) { + return Optional.of(callParam) + .map(BSLParser.CallParamContext::expression) + .map(BSLParser.ExpressionContext::member) + .filter(members -> members.size() == 1) + .map(members -> members.get(0)) + .map(BSLParser.MemberContext::complexIdentifier) + .map(BSLParser.ComplexIdentifierContext::IDENTIFIER) + .map(id -> id.getSymbol().getText()) + .orElse(null); + } + + private BSLParser.@Nullable ExpressionContext findAssignedExpression(String varName, ParserRuleContext callContext) { + // Находим statement, содержащий вызов СтрШаблон + var currentStatement = Trees.getRootParent(callContext, BSLParser.RULE_statement); + if (currentStatement == null) { + return null; + } + + // Ищем предыдущие statements + var prevStatement = getPreviousStatement((BSLParser.StatementContext) currentStatement); + while (prevStatement != null) { + var assignment = prevStatement.assignment(); + if (assignment != null && isAssignmentForVar(varName, assignment)) { + // Нашли присвоение - возвращаем выражение + return assignment.expression(); + } + prevStatement = getPreviousStatement(prevStatement); + } + + return null; + } + + private BSLParser.@Nullable StatementContext getPreviousStatement(BSLParser.StatementContext statement) { + var parent = statement.getParent(); + if (parent == null) { + return null; + } + + var children = parent.children; + if (children == null) { + return null; + } + + int pos = children.indexOf(statement); + for (int i = pos - 1; i >= 0; i--) { + var child = children.get(i); + if (child instanceof BSLParser.StatementContext prevStmt) { + return prevStmt; + } + } + + return null; + } + + private boolean isAssignmentForVar(String varName, BSLParser.AssignmentContext assignment) { + var lValue = assignment.lValue(); + if (lValue == null) { + return false; + } + var identifier = lValue.IDENTIFIER(); + return identifier != null && identifier.getText().equalsIgnoreCase(varName); + } + + /** + * Извлекает строковые токены из выражения. + * Поддерживает как простые строковые литералы, так и вызовы НСтр. + */ + private List extractStringTokensFromExpression(BSLParser.ExpressionContext expression) { + // Пробуем получить простую строку + var stringContext = Optional.of(expression) + .map(BSLParser.ExpressionContext::member) + .filter(members -> members.size() == 1) + .map(members -> members.get(0)) + .map(BSLParser.MemberContext::constValue) + .map(BSLParser.ConstValueContext::string) + .orElse(null); + + if (stringContext != null) { + return getStringTokensFromContext(stringContext); + } + + // Пробуем получить вызов НСтр + var globalMethodCall = Optional.of(expression) + .map(BSLParser.ExpressionContext::member) + .filter(members -> members.size() == 1) + .map(members -> members.get(0)) + .map(BSLParser.MemberContext::complexIdentifier) + .map(BSLParser.ComplexIdentifierContext::globalMethodCall) + .orElse(null); + + if (globalMethodCall != null && MultilingualStringAnalyser.isNStrCall(globalMethodCall)) { + var callParams = globalMethodCall.doCall().callParamList().callParam(); + if (!callParams.isEmpty()) { + return getStringTokensFromParam(callParams.get(0)); + } + } + + return List.of(); + } + + private List getStringTokensFromContext(BSLParser.StringContext stringContext) { + List stringTokens = new ArrayList<>(); + var tokens = Trees.getTokens(stringContext); + + for (Token token : tokens) { + if (STRING_TYPES.contains(token.getType())) { + stringTokens.add(token); + } + } + + return stringTokens; + } +} diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/StringContext.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/StringContext.java new file mode 100644 index 00000000000..0fd25fd07f9 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/StringContext.java @@ -0,0 +1,56 @@ +/* + * 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.semantictokens.strings; + +/** + * Контекст строки для определения типа обработки. + */ +public enum StringContext { + /** + * Строка в контексте вызова НСтр/NStr. + */ + NSTR, + /** + * Строка в контексте вызова СтрШаблон/StrTemplate. + */ + STR_TEMPLATE, + /** + * Строка в контексте вызова НСтр/NStr внутри СтрШаблон/StrTemplate или наоборот. + * Например: СтрШаблон(НСтр("ru = 'Текст %1'"), Параметр) + */ + NSTR_AND_STR_TEMPLATE; + + /** + * Объединяет два контекста. + * Если контексты разные (NSTR и STR_TEMPLATE), возвращает NSTR_AND_STR_TEMPLATE. + * + * @param other другой контекст для объединения + * @return объединённый контекст + */ + public StringContext combine(StringContext other) { + if (this == other) { + return this; + } + return NSTR_AND_STR_TEMPLATE; + } +} + diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SubToken.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SubToken.java new file mode 100644 index 00000000000..2547503c833 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/SubToken.java @@ -0,0 +1,33 @@ +/* + * 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.semantictokens.strings; + +/** + * Подтокен внутри строки (языковой ключ или плейсхолдер). + * + * @param start начальная позиция в строке + * @param length длина подтокена + * @param type тип семантического токена + */ +public record SubToken(int start, int length, String type) { +} + diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/TokenPosition.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/TokenPosition.java new file mode 100644 index 00000000000..33fa05ed658 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/TokenPosition.java @@ -0,0 +1,33 @@ +/* + * 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.semantictokens.strings; + +/** + * Позиция токена для использования в качестве ключа в Map. + * + * @param line номер строки (0-based) + * @param start начальная позиция в строке + * @param length длина токена + */ +public record TokenPosition(int line, int start, int length) { +} + diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/package-info.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/package-info.java new file mode 100644 index 00000000000..ff3ee0eb5d1 --- /dev/null +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/strings/package-info.java @@ -0,0 +1,36 @@ +/* + * 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. + */ +/** + * Вспомогательные классы для обработки строковых семантических токенов. + *

+ * Содержит классы для: + *

    + *
  • Определения контекста строк (НСтр, СтрШаблон, запросы)
  • + *
  • Сбора AST-based переопределений токенов SDBL
  • + *
  • Хранения информации о позициях и типах токенов
  • + *
+ */ +@NullMarked +package com.github._1c_syntax.bsl.languageserver.semantictokens.strings; + +import org.jspecify.annotations.NullMarked; + diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/MultilingualStringAnalyser.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/MultilingualStringAnalyser.java index da4a693a923..d479ccd9241 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/MultilingualStringAnalyser.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/utils/MultilingualStringAnalyser.java @@ -29,33 +29,58 @@ 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). *

* Проверяет наличие всех объявленных языков в многоязычных строках * и анализирует использование в шаблонах. + *

+ * Также предоставляет статические методы для поиска языковых ключей и плейсхолдеров + * в строках для целей семантической подсветки. */ 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( + "(?(Arrays.asList(matcher.replaceAll("").split(","))); } + + /** + * Запись для хранения позиции совпадения в строке. + * + * @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 findLanguageKeyPositions(String text) { + List 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 findPlaceholderPositions(String text) { + List 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; + } + private static boolean isNotMultilingualString(BSLParser.GlobalMethodCallContext 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) { @@ -103,7 +197,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) { @@ -198,10 +292,10 @@ public String getMissingLanguages() { */ public boolean isParentTemplate() { Objects.requireNonNull(globalMethodCallContext, "Call parse method first"); - return isParentTemplate || istVariableUsingInTemplate(); + return isParentTemplate || isVariableUsedInTemplate(); } - private boolean istVariableUsingInTemplate() { + private boolean isVariableUsedInTemplate() { if (variableName == null) { return false; } diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/LexicalSemanticTokensSupplierTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/LexicalSemanticTokensSupplierTest.java index 73d4f39fd46..1045834dc84 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/LexicalSemanticTokensSupplierTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/LexicalSemanticTokensSupplierTest.java @@ -65,6 +65,8 @@ void testKeywords() { @Test void testStrings() { + // Note: STRING tokens are now handled by StringSemanticTokensSupplier + // This test verifies that LexicalSemanticTokensSupplier does NOT process regular strings // given String bsl = """ Процедура Тест() @@ -82,7 +84,8 @@ void testStrings() { var stringTokens = tokens.stream() .filter(t -> t.type() == stringTypeIdx) .toList(); - assertThat(stringTokens).hasSize(1); + // String tokens are handled by StringSemanticTokensSupplier, so should be 0 + assertThat(stringTokens).isEmpty(); } @Test diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/QuerySemanticTokensSupplierTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/QuerySemanticTokensSupplierTest.java deleted file mode 100644 index e127b396e89..00000000000 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/QuerySemanticTokensSupplierTest.java +++ /dev/null @@ -1,200 +0,0 @@ -/* - * 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.semantictokens; - -import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterEachTestMethod; -import com.github._1c_syntax.bsl.languageserver.util.TestUtils; -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 static org.assertj.core.api.Assertions.assertThat; - -@SpringBootTest -@CleanupContextBeforeClassAndAfterEachTestMethod -class QuerySemanticTokensSupplierTest { - - @Autowired - private QuerySemanticTokensSupplier supplier; - - @Autowired - private SemanticTokensLegend legend; - - @Test - void testSimpleSelect() { - // given - String bsl = """ - Функция Тест() - Запрос = "Выбрать * из Справочник.Контрагенты"; - КонецФункции - """; - - var documentContext = TestUtils.getDocumentContext(bsl); - - // when - var tokens = supplier.getSemanticTokens(documentContext); - - // then - int keywordTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Keyword); - int namespaceTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Namespace); - int classTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Class); - - var keywordTokens = tokens.stream().filter(t -> t.type() == keywordTypeIdx).toList(); - var namespaceTokens = tokens.stream().filter(t -> t.type() == namespaceTypeIdx).toList(); - var classTokens = tokens.stream().filter(t -> t.type() == classTypeIdx).toList(); - - // Выбрать, из - assertThat(keywordTokens).hasSizeGreaterThanOrEqualTo(2); - // Справочник - assertThat(namespaceTokens).hasSize(1); - // Контрагенты - assertThat(classTokens).hasSize(1); - } - - @Test - void testQueryWithParameter() { - // given - String bsl = """ - Функция Тест() - Запрос = "Выбрать * где Код = &Параметр"; - КонецФункции - """; - - var documentContext = TestUtils.getDocumentContext(bsl); - - // when - var tokens = supplier.getSemanticTokens(documentContext); - - // then - int parameterTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Parameter); - var parameterTokens = tokens.stream().filter(t -> t.type() == parameterTypeIdx).toList(); - - // &Параметр - assertThat(parameterTokens).hasSize(1); - assertThat(parameterTokens.get(0).line()).isEqualTo(1); - } - - @Test - void testQueryWithVirtualTable() { - // given - String bsl = """ - Функция Тест() - Запрос = "Выбрать * из РегистрСведений.КурсыВалют.СрезПоследних()"; - КонецФункции - """; - - var documentContext = TestUtils.getDocumentContext(bsl); - - // when - var tokens = supplier.getSemanticTokens(documentContext); - - // then - int methodTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Method); - var methodTokens = tokens.stream().filter(t -> t.type() == methodTypeIdx).toList(); - - // СрезПоследних - assertThat(methodTokens).hasSize(1); - } - - @Test - void testQueryWithValueFunction() { - // given - String bsl = """ - Функция Тест() - Запрос = "Выбрать * где Валюта = Значение(Справочник.Валюты.Рубль)"; - КонецФункции - """; - - var documentContext = TestUtils.getDocumentContext(bsl); - - // when - var tokens = supplier.getSemanticTokens(documentContext); - - // then - int namespaceTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Namespace); - int classTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Class); - int enumMemberTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.EnumMember); - - var namespaceTokens = tokens.stream().filter(t -> t.type() == namespaceTypeIdx).toList(); - var classTokens = tokens.stream().filter(t -> t.type() == classTypeIdx).toList(); - var enumMemberTokens = tokens.stream().filter(t -> t.type() == enumMemberTypeIdx).toList(); - - // Справочник - assertThat(namespaceTokens).hasSize(1); - // Валюты - assertThat(classTokens).hasSize(1); - // Рубль - assertThat(enumMemberTokens).hasSize(1); - } - - @Test - void testQueryWithEnumValue() { - // given - String bsl = """ - Функция Тест() - Запрос = "Выбрать * где Пол = Значение(Перечисление.Пол.Мужской)"; - КонецФункции - """; - - var documentContext = TestUtils.getDocumentContext(bsl); - - // when - var tokens = supplier.getSemanticTokens(documentContext); - - // then - int enumTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Enum); - int enumMemberTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.EnumMember); - - var enumTokens = tokens.stream().filter(t -> t.type() == enumTypeIdx).toList(); - var enumMemberTokens = tokens.stream().filter(t -> t.type() == enumMemberTypeIdx).toList(); - - // Пол (enum) - assertThat(enumTokens).hasSize(1); - // Мужской (enum member) - assertThat(enumMemberTokens).hasSize(1); - } - - @Test - void testSplitsStringAroundQueryTokens() { - // given - String bsl = """ - Функция Тест() - Запрос = "Выбрать Поле из Справочник.Контрагенты"; - КонецФункции - """; - - var documentContext = TestUtils.getDocumentContext(bsl); - - // when - var tokens = supplier.getSemanticTokens(documentContext); - - // then - int stringTypeIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.String); - var stringTokens = tokens.stream().filter(t -> t.type() == stringTypeIdx).toList(); - - // String parts (quotes, spaces) around query tokens - assertThat(stringTokens).isNotEmpty(); - } -} - 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 new file mode 100644 index 00000000000..247caa77e39 --- /dev/null +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/semantictokens/StringSemanticTokensSupplierTest.java @@ -0,0 +1,560 @@ +/* + * 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.semantictokens; + +import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterEachTestMethod; +import com.github._1c_syntax.bsl.languageserver.util.TestUtils; +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 java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@CleanupContextBeforeClassAndAfterEachTestMethod +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(); + } + + // ==================== Regular String Tests ==================== + + @Test + void testSimpleString() { + // given + String bsl = """ + Процедура Тест() + Текст = "Привет мир"; + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); + assertThat(stringTokens).hasSize(1); + } + + @Test + void testMultilineString() { + // given + String bsl = """ + Процедура Тест() + Текст = "Первая строка + |Вторая строка + |Третья строка"; + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); + // STRINGSTART, 2x STRINGPART, or STRINGTAIL + assertThat(stringTokens).hasSizeGreaterThanOrEqualTo(3); + } + + // ==================== NStr Tests ==================== + + @Test + void testNStrLanguageKeys() { + // given + String bsl = """ + Процедура Тест() + Текст = НСтр("ru='Привет'; en='Hello'"); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); + // ru, en + assertThat(propertyTokens).hasSize(2); + + // Check that string parts are also present + var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); + assertThat(stringTokens).isNotEmpty(); + } + + @Test + void testNStrEnglishName() { + // given + String bsl = """ + Процедура Тест() + Текст = NStr("ru='Привет'; en='Hello'"); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var propertyTokens = tokensOfType(tokens, SemanticTokenTypes.Property); + // ru, en + assertThat(propertyTokens).hasSize(2); + } + + // ==================== StrTemplate Tests ==================== + + @Test + void testStrTemplatePlaceholders() { + // given + String bsl = """ + Процедура Тест() + Текст = СтрШаблон("Наименование: %1, версия: %2", Наименование, Версия); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); + // %1, %2 + assertThat(parameterTokens).hasSize(2); + + // Check that string parts are also present + var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); + assertThat(stringTokens).isNotEmpty(); + } + + @Test + void testStrTemplateEnglishName() { + // given + String bsl = """ + Процедура Тест() + Текст = StrTemplate("Name: %1, version: %2", Name, Version); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); + // %1, %2 + assertThat(parameterTokens).hasSize(2); + } + + @Test + void testStrTemplatePlaceholdersWithParentheses() { + // given + String bsl = """ + Процедура Тест() + Текст = СтрШаблон("%(1)%(2)", "Первая", "Вторая"); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); + // %(1), %(2) + assertThat(parameterTokens).hasSize(2); + } + + @Test + void testStrTemplateWithVariable() { + // given - шаблон задан в переменной, затем используется в СтрШаблон + String bsl = """ + Процедура Тест() + НовыйШаблон = "%1 %2"; + Результат = СтрШаблон(НовыйШаблон, А, Б); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then - плейсхолдеры в строке-присвоении должны подсвечиваться + var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); + // %1, %2 из строки НовыйШаблон = "%1 %2" + assertThat(parameterTokens).hasSize(2); + } + + @Test + void testStrTemplateWithVariableAndParentheses() { + // given - шаблон с %(1) синтаксисом в переменной + String bsl = """ + Процедура Тест() + Шаблон = "%(1)%(2)"; + Текст = СтрШаблон(Шаблон, "Первая", "Вторая"); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); + // %(1), %(2) + assertThat(parameterTokens).hasSize(2); + } + + // ==================== Combined NStr + StrTemplate Tests ==================== + + @Test + void testNStrInsideStrTemplate() { + // given - НСтр внутри СтрШаблон: СтрШаблон(НСтр("ru = 'Текст %1'"), Параметр) + String bsl = """ + Процедура Тест() + Сообщить(СтрШаблон(НСтр("ru = 'Сценарий %1'"), Параметр)); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // 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); + } + + @Test + void testNStrInsideStrTemplateMultiple() { + // given - НСтр с несколькими языками и несколькими плейсхолдерами + String bsl = """ + Процедура Тест() + Результат = СтрШаблон(НСтр("ru = 'Привет %1'; en = 'Hello %2'"), Имя, Name); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // 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); + } + + @Test + void testNStrVariableInStrTemplate() { + // given - НСтр присвоен переменной, которая затем используется в СтрШаблон + String bsl = """ + Процедура Тест() + Шаблон = НСтр("ru = 'Сценарий %1'"); + ТекстПредупреждения = СтрШаблон(Шаблон, Параметр); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // 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); + } + + @Test + void testNStrVariableInStrTemplateMultiple() { + // given - НСтр с несколькими языками и плейсхолдерами в переменной + String bsl = """ + Процедура Тест() + Шаблон = НСтр("ru = 'Привет %1 и %2'; en = 'Hello %1 and %2'"); + Результат = СтрШаблон(Шаблон, Имя1, Имя2); + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // 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); + } + + // ==================== Query String Tests ==================== + + @Test + void testQueryStringSplit() { + // given + String bsl = """ + Процедура Тест() + Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура"; + КонецПроцедуры + """; + + // 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); + } + + @Test + void testMultilineQueryString() { + // given + String bsl = """ + Процедура Тест() + Запрос.Текст = "ВЫБРАТЬ + | Ссылка + |ИЗ + | Справочник.Номенклатура"; + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // then + var stringTokens = tokensOfType(tokens, SemanticTokenTypes.String); + + // Should have string parts on each line + assertThat(stringTokens).isNotEmpty(); + } + + // ==================== Mixed Context Tests ==================== + + @Test + void testNStrAndQueryInSameMethod() { + // given + String bsl = """ + Процедура Тест() + Сообщение = НСтр("ru='Выполняется запрос'"); + Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура"; + КонецПроцедуры + """; + + // when + var tokens = tokens(bsl); + + // 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); + } + + @Test + void testRegularStringNotAffectedByOtherContexts() { + // given + String bsl = """ + Процедура Тест() + ОбычнаяСтрока = "Это просто строка без НСтр и запроса"; + КонецПроцедуры + """; + + // 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 parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); + assertThat(parameterTokens).isEmpty(); + } + + // ==================== SDBL Query Token Tests ==================== + + @Test + void testSimpleSelect() { + // given + String bsl = """ + Функция Тест() + Запрос = "Выбрать * из Справочник.Контрагенты"; + КонецФункции + """; + + // when + var tokens = tokens(bsl); + + // 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); + } + + @Test + void testQueryWithParameter() { + // given + String bsl = """ + Функция Тест() + Запрос = "Выбрать * где Код = &Параметр"; + КонецФункции + """; + + // when + var tokens = tokens(bsl); + + // then + var parameterTokens = tokensOfType(tokens, SemanticTokenTypes.Parameter); + + // &Параметр - один объединённый токен + assertThat(parameterTokens).hasSize(1); + assertThat(parameterTokens.get(0).line()).isEqualTo(1); + } + + @Test + void testQueryWithVirtualTable() { + // given + String bsl = """ + Функция Тест() + Запрос = "Выбрать * из РегистрСведений.КурсыВалют.СрезПоследних()"; + КонецФункции + """; + + // when + var tokens = tokens(bsl); + + // then + var methodTokens = tokensOfType(tokens, SemanticTokenTypes.Method); + + // СрезПоследних + assertThat(methodTokens).hasSize(1); + } + + @Test + void testQueryWithValueFunction() { + // given + String bsl = """ + Функция Тест() + Запрос = "Выбрать * где Валюта = Значение(Справочник.Валюты.Рубль)"; + КонецФункции + """; + + // when + var tokens = tokens(bsl); + + // 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); + } + + @Test + void testQueryWithEnumValue() { + // given + String bsl = """ + Функция Тест() + Запрос = "Выбрать * где Пол = Значение(Перечисление.Пол.Мужской)"; + КонецФункции + """; + + // when + var tokens = tokens(bsl); + + // then + var enumTokens = tokensOfType(tokens, SemanticTokenTypes.Enum); + var enumMemberTokens = tokensOfType(tokens, SemanticTokenTypes.EnumMember); + + // Пол (enum) + assertThat(enumTokens).hasSize(1); + // Мужской (enum member) + assertThat(enumMemberTokens).hasSize(1); + } + + @Test + void testQueryWithAggregateFunction() { + // given + String bsl = """ + Функция Тест() + Запрос = "Выбрать Количество(*) из Справочник.Товары"; + КонецФункции + """; + + // when + var tokens = tokens(bsl); + + // then + var functionTokens = tokensOfType(tokens, SemanticTokenTypes.Function); + + // Количество + assertThat(functionTokens).hasSize(1); + } +} +