diff --git a/build.gradle.kts b/build.gradle.kts index efdbf4bbdb8..9faf3566a0e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -87,7 +87,7 @@ dependencies { exclude("org.antlr", "ST4") exclude("org.antlr", "antlr-runtime") } - api("io.github.1c-syntax", "utils", "0.6.6") + api("io.github.1c-syntax", "utils", "0.6.8") api("io.github.1c-syntax", "mdclasses", "0.17.0") api("io.github.1c-syntax", "bsl-common-library", "0.9.0") api("io.github.1c-syntax", "supportconf", "0.15.0") diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java index 7352ee1c954..e963301e5d8 100644 --- a/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java +++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java @@ -24,13 +24,16 @@ import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; import com.github._1c_syntax.bsl.languageserver.context.symbol.ParameterDefinition; import com.github._1c_syntax.bsl.languageserver.context.symbol.SymbolTree; +import com.github._1c_syntax.bsl.languageserver.context.symbol.VariableSymbol; import com.github._1c_syntax.bsl.languageserver.context.symbol.description.MethodDescription; import com.github._1c_syntax.bsl.languageserver.context.symbol.description.SourceDefinedSymbolDescription; import com.github._1c_syntax.bsl.languageserver.context.symbol.variable.VariableDescription; +import com.github._1c_syntax.bsl.languageserver.context.symbol.variable.VariableKind; import com.github._1c_syntax.bsl.languageserver.events.LanguageServerInitializeRequestReceivedEvent; 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 com.github._1c_syntax.bsl.languageserver.utils.Trees; import com.github._1c_syntax.bsl.parser.BSLLexer; @@ -237,10 +240,13 @@ private void addVariableSymbols( BitSet documentationLines ) { for (var variableSymbol : symbolTree.getVariables()) { + if (variableSymbol.getKind() == VariableKind.PARAMETER) { + continue; + } + var nameRange = variableSymbol.getVariableNameRange(); if (!Ranges.isEmpty(nameRange)) { - Position pos = nameRange.getStart(); - boolean isDefinition = referenceResolver.findReference(documentContext.getUri(), pos) + boolean isDefinition = referenceResolver.findReference(documentContext.getUri(), nameRange.getStart()) .map(ref -> ref.getOccurrenceType() == OccurrenceType.DEFINITION) .orElse(false); if (isDefinition) { @@ -249,6 +255,7 @@ private void addVariableSymbols( addRange(entries, nameRange, SemanticTokenTypes.Variable); } } + variableSymbol.getDescription().ifPresent((VariableDescription description) -> { processVariableDescription(descriptionRanges, documentationLines, description); @@ -257,6 +264,24 @@ private void addVariableSymbols( ); }); } + + var references = referenceIndex.getReferencesFrom(documentContext.getUri(), SymbolKind.Variable); + references.stream() + .filter(Reference::isSourceDefinedSymbolReference) + .forEach(reference -> reference.getSourceDefinedSymbol() + .filter(symbol -> symbol instanceof VariableSymbol) + .map(symbol -> (VariableSymbol) symbol) + .ifPresent(variableSymbol -> { + var tokenType = variableSymbol.getKind() == VariableKind.PARAMETER + ? SemanticTokenTypes.Parameter + : SemanticTokenTypes.Variable; + + if (reference.getOccurrenceType() == OccurrenceType.DEFINITION) { + addRange(entries, reference.getSelectionRange(), tokenType, SemanticTokenModifiers.Definition); + } else { + addRange(entries, reference.getSelectionRange(), tokenType); + } + })); } private void addMethodSymbols(SymbolTree symbolTree, List entries, List descriptionRanges, BitSet documentationLines) { @@ -264,7 +289,7 @@ private void addMethodSymbols(SymbolTree symbolTree, List entries, L var semanticTokenType = method.isFunction() ? SemanticTokenTypes.Function : SemanticTokenTypes.Method; addRange(entries, method.getSubNameRange(), semanticTokenType); for (ParameterDefinition parameter : method.getParameters()) { - addRange(entries, parameter.getRange(), SemanticTokenTypes.Parameter); + addRange(entries, parameter.getRange(), SemanticTokenTypes.Parameter, SemanticTokenModifiers.Definition); } method.getDescription().ifPresent((MethodDescription description) -> processVariableDescription(descriptionRanges, documentationLines, description) diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java index 97eea6f2acc..fe8f145f971 100644 --- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java +++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java @@ -22,10 +22,10 @@ package com.github._1c_syntax.bsl.languageserver.providers; import com.github._1c_syntax.bsl.languageserver.context.DocumentContext; +import com.github._1c_syntax.bsl.languageserver.references.ReferenceIndexFiller; +import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterEachTestMethod; import com.github._1c_syntax.bsl.languageserver.util.TestUtils; import com.github._1c_syntax.bsl.parser.BSLLexer; -import com.github._1c_syntax.bsl.languageserver.references.ReferenceIndex; -import com.github._1c_syntax.bsl.languageserver.references.model.Reference; import com.github._1c_syntax.bsl.languageserver.context.symbol.MethodSymbol; import org.antlr.v4.runtime.Token; import org.eclipse.lsp4j.Position; @@ -39,8 +39,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.annotation.DirtiesContext; -import org.eclipse.lsp4j.SymbolKind; import java.util.ArrayList; import java.util.HashSet; @@ -50,14 +48,8 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.springframework.test.context.bean.override.mockito.MockitoBean; - -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - @SpringBootTest -@DirtiesContext +@CleanupContextBeforeClassAndAfterEachTestMethod class SemanticTokensProviderTest { @Autowired @@ -66,8 +58,8 @@ class SemanticTokensProviderTest { @Autowired private SemanticTokensLegend legend; - @MockitoBean - private ReferenceIndex referenceIndex; + @Autowired + private ReferenceIndexFiller referenceIndexFiller; @BeforeEach void init() { @@ -88,6 +80,7 @@ void emitsExpectedTokenTypes() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -127,6 +120,7 @@ void emitsMacroForAllPreprocTokens() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -188,6 +182,7 @@ void emitsOperatorsForPunctuators() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -243,6 +238,7 @@ void annotationWithoutParams_isDecoratorOnly() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -274,6 +270,7 @@ void annotationWithStringParam_tokenizesNameParenAndString() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -308,6 +305,7 @@ void customAnnotationWithNamedStringParam_marksIdentifierAsParameter() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -348,6 +346,7 @@ void useDirective_isNamespace() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -377,6 +376,7 @@ void datetimeAndUndefinedTrueFalse_areHighlighted() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -413,6 +413,7 @@ void methodDescriptionComments_areMarkedWithDocumentationModifier() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -448,6 +449,7 @@ void variableDescriptionLeadingAndTrailing_areMarkedWithDocumentationModifier() ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -486,6 +488,7 @@ void multilineDocumentation_isMergedIntoSingleToken_whenClientSupportsIt() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -524,6 +527,7 @@ void regionName_isHighlightedAsVariable() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -557,6 +561,7 @@ void variableDefinition_hasDefinitionModifier() { ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); // when @@ -582,35 +587,20 @@ void variableDefinition_hasDefinitionModifier() { void sameFileMethodCall_isHighlightedAsMethodTokenAtCallSite() { // given: a method and a call to another method in the same file String bsl = String.join("\n", + "Процедура CallMe()", + "КонецПроцедуры", + "", "Процедура Бар()", " CallMe();", "КонецПроцедуры" ); DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); - // compute selection range for 'CallMe' on line 1 - int callLine = 1; - int callStart = bsl.split("\n")[callLine].indexOf("CallMe"); - Range callRange = new Range(new Position(callLine, callStart), new Position(callLine, callStart + "CallMe".length())); - - // mock a same-file reference pointing to a method symbol owned by this document - Reference ref = mock(Reference.class, RETURNS_DEEP_STUBS); - MethodSymbol toSymbol = MethodSymbol.builder() - .name("CallMe") - .owner(documentContext) - .function(false) - .range(new Range(new Position(0, 0), new Position(0, 0))) - .subNameRange(new Range(new Position(0, 0), new Position(0, 0))) - .build(); - - when(ref.isSourceDefinedSymbolReference()).thenReturn(true); - when(ref.getSourceDefinedSymbol()).thenReturn(java.util.Optional.of(toSymbol)); - when(ref.getSelectionRange()).thenReturn(callRange); - - when(referenceIndex.getReferencesFrom(documentContext.getUri(), SymbolKind.Method)) - .thenReturn(List.of(ref)); + // compute selection range for 'CallMe' on line 4 + int callLine = 4; // when SemanticTokens tokens = provider.getSemanticTokensFull(documentContext, new SemanticTokensParams(textDocumentIdentifier)); @@ -618,12 +608,172 @@ void sameFileMethodCall_isHighlightedAsMethodTokenAtCallSite() { int methodIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Method); assertThat(methodIdx).isGreaterThanOrEqualTo(0); - // then: there is a Method token on the call line (line 1) + // then: there is a Method token on the call line (line 4) List decoded = decode(tokens.getData()); long methodsOnCallLine = decoded.stream().filter(t -> t.line == callLine && t.type == methodIdx).count(); assertThat(methodsOnCallLine).isGreaterThanOrEqualTo(1); } + @Test + void parameterAndVariableTokenTypes() { + String bsl = String.join("\n", + "Процедура Тест(Парам1, Парам2)", + " Перем ЛокальнаяПеременная;", + " НеявнаяПеременная = 1;", + " ЛокальнаяПеременная2 = 2;", + " Результат = 3;", + " Для ПеременнаяЦикла = 1 По 10 Цикл", + " КонецЦикла;", + "КонецПроцедуры" + ); + + DocumentContext documentContext = TestUtils.getDocumentContext(bsl); + referenceIndexFiller.fill(documentContext); + TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); + + SemanticTokens tokens = provider.getSemanticTokensFull(documentContext, new SemanticTokensParams(textDocumentIdentifier)); + + int paramIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Parameter); + int varIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Variable); + assertThat(paramIdx).isGreaterThanOrEqualTo(0); + assertThat(varIdx).isGreaterThanOrEqualTo(0); + + List decoded = decode(tokens.getData()); + + long paramsInSignature = decoded.stream() + .filter(t -> t.line == 0 && t.type == paramIdx) + .count(); + assertThat(paramsInSignature).as("Parameters in signature").isEqualTo(2); + + long localVarDeclaration = decoded.stream() + .filter(t -> t.line == 1 && t.type == varIdx) + .count(); + assertThat(localVarDeclaration).as("Explicit variable declaration").isEqualTo(1); + + long implicitVarDeclaration1 = decoded.stream() + .filter(t -> t.line == 2 && t.type == varIdx) + .count(); + assertThat(implicitVarDeclaration1).as("First implicit variable declaration").isEqualTo(1); + + long implicitVarDeclaration2 = decoded.stream() + .filter(t -> t.line == 3 && t.type == varIdx) + .count(); + assertThat(implicitVarDeclaration2).as("Second implicit variable declaration").isEqualTo(1); + + long implicitVarDeclaration3 = decoded.stream() + .filter(t -> t.line == 4 && t.type == varIdx) + .count(); + assertThat(implicitVarDeclaration3).as("Third implicit variable declaration").isEqualTo(1); + + long forLoopVar = decoded.stream() + .filter(t -> t.line == 5 && t.type == varIdx) + .count(); + assertThat(forLoopVar).as("For loop variable").isEqualTo(1); + + long allParams = decoded.stream() + .filter(t -> t.type == paramIdx) + .count(); + assertThat(allParams).as("Total parameters").isEqualTo(2); + + long allVars = decoded.stream() + .filter(t -> t.type == varIdx) + .count(); + assertThat(allVars).as("Total variables").isEqualTo(5); + } + + @Test + void parameterAndVariableUsages() { + var documentContext = TestUtils.getDocumentContextFromFile( + "./src/test/resources/providers/SemanticTokensProviderParameterTest.bsl" + ); + referenceIndexFiller.fill(documentContext); + + TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri()); + SemanticTokens tokens = provider.getSemanticTokensFull(documentContext, new SemanticTokensParams(textDocumentIdentifier)); + + int paramIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Parameter); + int varIdx = legend.getTokenTypes().indexOf(SemanticTokenTypes.Variable); + assertThat(paramIdx).isGreaterThanOrEqualTo(0); + assertThat(varIdx).isGreaterThanOrEqualTo(0); + + List decoded = decode(tokens.getData()); + + long paramsLine0 = decoded.stream() + .filter(t -> t.line == 0 && t.type == paramIdx) + .count(); + assertThat(paramsLine0).as("Parameters in signature (line 0)").isEqualTo(2); + + long varsLine1 = decoded.stream() + .filter(t -> t.line == 1 && t.type == varIdx) + .count(); + assertThat(varsLine1).as("Local variable declaration (line 1)").isEqualTo(1); + + long varsLine3 = decoded.stream() + .filter(t -> t.line == 3 && t.type == varIdx) + .count(); + assertThat(varsLine3).as("Variable usage on left side (line 3)").isEqualTo(1); + + long paramsLine3 = decoded.stream() + .filter(t -> t.line == 3 && t.type == paramIdx) + .count(); + assertThat(paramsLine3).as("Parameter usage on right side (line 3)").isEqualTo(1); + + long varsLine4 = decoded.stream() + .filter(t -> t.line == 4 && t.type == varIdx) + .count(); + assertThat(varsLine4).as("Variable usage (line 4)").isEqualTo(1); + + long paramsLine4 = decoded.stream() + .filter(t -> t.line == 4 && t.type == paramIdx) + .count(); + assertThat(paramsLine4).as("Parameter usages (line 4)").isEqualTo(2); + + long paramsLine6 = decoded.stream() + .filter(t -> t.line == 6 && t.type == paramIdx) + .count(); + assertThat(paramsLine6).as("Parameter in condition (line 6)").isEqualTo(1); + + long paramsLine7 = decoded.stream() + .filter(t -> t.line == 7 && t.type == paramIdx) + .count(); + assertThat(paramsLine7).as("Parameter in Сообщить (line 7)").isEqualTo(1); + + long varsLine8 = decoded.stream() + .filter(t -> t.line == 8 && t.type == varIdx) + .count(); + assertThat(varsLine8).as("Variable assignment (line 8)").isEqualTo(1); + + long paramsLine8 = decoded.stream() + .filter(t -> t.line == 8 && t.type == paramIdx) + .count(); + assertThat(paramsLine8).as("Parameters in expression (line 8)").isEqualTo(2); + + long varsLine11 = decoded.stream() + .filter(t -> t.line == 11 && t.type == varIdx) + .count(); + assertThat(varsLine11).as("For loop variable (line 11)").isEqualTo(1); + + long paramsLine11 = decoded.stream() + .filter(t -> t.line == 11 && t.type == paramIdx) + .count(); + assertThat(paramsLine11).as("Parameter in loop bound (line 11)").isEqualTo(1); + + long varsLine12 = decoded.stream() + .filter(t -> t.line == 12 && t.type == varIdx) + .count(); + assertThat(varsLine12).as("Loop variable usage (line 12)").isEqualTo(1); + + long totalParams = decoded.stream() + .filter(t -> t.type == paramIdx) + .count(); + assertThat(totalParams).as("Total parameter tokens").isGreaterThanOrEqualTo(10); + + long totalVars = decoded.stream() + .filter(t -> t.type == varIdx) + .count(); + assertThat(totalVars).as("Total variable tokens").isGreaterThanOrEqualTo(6); + } + // helpers private record DecodedToken(int line, int start, int length, int type, int modifiers) {} diff --git a/src/test/resources/providers/SemanticTokensProviderParameterTest.bsl b/src/test/resources/providers/SemanticTokensProviderParameterTest.bsl new file mode 100644 index 00000000000..82a0925acc6 --- /dev/null +++ b/src/test/resources/providers/SemanticTokensProviderParameterTest.bsl @@ -0,0 +1,17 @@ +Процедура Тест(Парам1, Парам2) + Перем ЛокальнаяПеременная; + + ЛокальнаяПеременная = Парам1; + Парам2 = ЛокальнаяПеременная + Парам1; + + Если Парам1 > 0 Тогда + Сообщить(Парам2); + ЛокальнаяПеременная = Парам1 + Парам2; + КонецЕсли; + + Для Счетчик = 1 По Парам2 Цикл + Сообщить(Счетчик); + КонецЦикла; + + Возврат; +КонецПроцедуры