getDocument(String mdoRef, ModuleType moduleTyp
return Optional.empty();
}
+ /**
+ * Получить документ по URI.
+ *
+ * URI должен быть уже нормализован (например, получен из DocumentContext или через Absolute.uri).
+ *
+ * @param uri нормализованный URI документа
+ * @return Контекст документа или {@code null}, если документ не найден
+ */
@Nullable
public DocumentContext getDocument(URI uri) {
- return documents.get(Absolute.uri(uri));
+ return documents.get(uri);
+ }
+
+ /**
+ * Получить документ по URI с нормализацией.
+ *
+ * Используется для внешних вызовов (CLI, Service), где URI может быть не нормализован.
+ *
+ * @param uri URI документа (будет нормализован)
+ * @return Контекст документа или {@code null}, если документ не найден
+ */
+ @Nullable
+ public DocumentContext getDocumentUnsafe(URI uri) {
+ return getDocument(Absolute.uri(uri));
+ }
+
+
+ /**
+ * Получить документ по строковому URI с нормализацией.
+ *
+ * Используется для внешних вызовов (CLI, Service), где URI может быть не нормализован.
+ *
+ * @param uri строковый URI документа
+ * @return Контекст документа или {@code null}, если документ не найден
+ */
+ @Nullable
+ public DocumentContext getDocumentUnsafe(String uri) {
+ return getDocument(Absolute.uri(uri));
}
public Map getDocuments(String mdoRef) {
return documentsByMDORef.getOrDefault(mdoRef, Collections.emptyMap());
}
+ /**
+ * Добавить документ в контекст.
+ *
+ * URI должен быть уже нормализован.
+ *
+ * @param uri нормализованный URI документа
+ * @return Контекст документа
+ */
public DocumentContext addDocument(URI uri) {
contextLock.readLock().lock();
@@ -181,16 +219,22 @@ public DocumentContext addDocument(URI uri) {
return documentContext;
}
+ /**
+ * Удалить документ из контекста.
+ *
+ * URI должен быть уже нормализован.
+ *
+ * @param uri нормализованный URI документа
+ */
public void removeDocument(URI uri) {
- var absoluteURI = Absolute.uri(uri);
- var documentContext = documents.get(absoluteURI);
+ var documentContext = documents.get(uri);
if (openedDocuments.contains(documentContext)) {
- throw new IllegalStateException(String.format("Document %s is opened", absoluteURI));
+ throw new IllegalStateException(String.format("Document %s is opened", uri));
}
- removeDocumentMdoRefByUri(absoluteURI);
+ removeDocumentMdoRefByUri(uri);
states.remove(documentContext);
- documents.remove(absoluteURI);
+ documents.remove(uri);
}
public void clear() {
@@ -295,12 +339,10 @@ public CF getConfiguration() {
}
private DocumentContext createDocumentContext(URI uri) {
- var absoluteURI = Absolute.uri(uri);
-
- var documentContext = documentContextProvider.getObject(absoluteURI);
+ var documentContext = documentContextProvider.getObject(uri);
- documents.put(absoluteURI, documentContext);
- addMdoRefByUri(absoluteURI, documentContext);
+ documents.put(uri, documentContext);
+ addMdoRefByUri(uri, documentContext);
return documentContext;
}
diff --git a/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/events/ServerContextDocumentClosedEvent.java b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/events/ServerContextDocumentClosedEvent.java
new file mode 100644
index 00000000000..e139654463d
--- /dev/null
+++ b/src/main/java/com/github/_1c_syntax/bsl/languageserver/context/events/ServerContextDocumentClosedEvent.java
@@ -0,0 +1,70 @@
+/*
+ * 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.context.events;
+
+import com.github._1c_syntax.bsl.languageserver.context.DocumentContext;
+import com.github._1c_syntax.bsl.languageserver.context.ServerContext;
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+import java.io.Serial;
+
+/**
+ * Событие, публикуемое при закрытии документа в контексте сервера.
+ *
+ * Событие генерируется контекстом сервера {@link ServerContext} при вызове метода
+ * {@link ServerContext#closeDocument(DocumentContext)} и содержит закрытый документ.
+ *
+ * Подписчики на это событие могут выполнить очистку связанных с документом данных,
+ * таких как кэшированные семантические токены и другие временные данные.
+ *
+ * Событие публикуется после того, как документ был помечен как закрытый.
+ *
+ * @see ServerContext#closeDocument(DocumentContext)
+ */
+public class ServerContextDocumentClosedEvent extends ApplicationEvent {
+
+ @Serial
+ private static final long serialVersionUID = 8274629847264754220L;
+
+ /**
+ * Закрытый документ.
+ */
+ @Getter
+ private final DocumentContext documentContext;
+
+ /**
+ * Создает новое событие закрытия документа в контексте сервера.
+ *
+ * @param source контекст сервера, в котором был закрыт документ
+ * @param documentContext закрытый документ
+ */
+ public ServerContextDocumentClosedEvent(ServerContext source, DocumentContext documentContext) {
+ super(source);
+ this.documentContext = documentContext;
+ }
+
+ @Override
+ public ServerContext getSource() {
+ return (ServerContext) super.getSource();
+ }
+}
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 a433472ca23..dedb74b771d 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
@@ -22,24 +22,42 @@
package com.github._1c_syntax.bsl.languageserver.providers;
import com.github._1c_syntax.bsl.languageserver.context.DocumentContext;
+import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentClosedEvent;
+import com.github._1c_syntax.bsl.languageserver.context.events.ServerContextDocumentRemovedEvent;
import com.github._1c_syntax.bsl.languageserver.semantictokens.SemanticTokenEntry;
import com.github._1c_syntax.bsl.languageserver.semantictokens.SemanticTokensSupplier;
+import com.github._1c_syntax.bsl.languageserver.utils.NamedForkJoinWorkerThreadFactory;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
import lombok.RequiredArgsConstructor;
import org.eclipse.lsp4j.SemanticTokens;
+import org.eclipse.lsp4j.SemanticTokensDelta;
+import org.eclipse.lsp4j.SemanticTokensDeltaParams;
+import org.eclipse.lsp4j.SemanticTokensEdit;
import org.eclipse.lsp4j.SemanticTokensParams;
+import org.eclipse.lsp4j.jsonrpc.messages.Either;
+import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
+import java.net.URI;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
/**
* Провайдер для предоставления семантических токенов.
*
- * Обрабатывает запросы {@code textDocument/semanticTokens/full}.
+ * Обрабатывает запросы {@code textDocument/semanticTokens/full} и {@code textDocument/semanticTokens/full/delta}.
*
* @see Semantic Tokens specification
*/
@@ -47,8 +65,37 @@
@RequiredArgsConstructor
public class SemanticTokensProvider {
+ @SuppressWarnings("NullAway.Init")
+ private ExecutorService executorService;
+
private final List suppliers;
+ /**
+ * Cache for storing previous token data by resultId.
+ * Key: resultId, Value: token data list
+ */
+ private final Map tokenCache = new ConcurrentHashMap<>();
+
+ @PostConstruct
+ private void init() {
+ var factory = new NamedForkJoinWorkerThreadFactory("semantic-tokens-");
+ executorService = new ForkJoinPool(ForkJoinPool.getCommonPoolParallelism(), factory, null, true);
+ }
+
+ @PreDestroy
+ private void onDestroy() {
+ executorService.shutdown();
+ }
+
+ /**
+ * Cached semantic token data associated with a document.
+ *
+ * @param uri URI of the document
+ * @param data token data list
+ */
+ private record CachedTokenData(URI uri, List data) {
+ }
+
/**
* Получить семантические токены для всего документа.
*
@@ -56,16 +103,170 @@ public class SemanticTokensProvider {
* @param params Параметры запроса
* @return Семантические токены в дельта-кодированном формате
*/
- public SemanticTokens getSemanticTokensFull(DocumentContext documentContext, @SuppressWarnings("unused") SemanticTokensParams params) {
- // Collect tokens from all suppliers
- List entries = suppliers.stream()
- .map(supplier -> supplier.getSemanticTokens(documentContext))
- .flatMap(Collection::stream)
- .toList();
+ public SemanticTokens getSemanticTokensFull(
+ DocumentContext documentContext,
+ @SuppressWarnings("unused") SemanticTokensParams params
+ ) {
+ // Collect tokens from all suppliers in parallel
+ var entries = collectTokens(documentContext);
// Build delta-encoded data
List data = toDeltaEncoded(entries);
- return new SemanticTokens(data);
+
+ // Generate a unique resultId and cache the data
+ String resultId = generateResultId();
+ cacheTokenData(resultId, documentContext.getUri(), data);
+
+ return new SemanticTokens(resultId, data);
+ }
+
+ /**
+ * Получить дельту семантических токенов относительно предыдущего результата.
+ *
+ * @param documentContext Контекст документа
+ * @param params Параметры запроса с previousResultId
+ * @return Либо дельту токенов, либо полные токены, если предыдущий результат недоступен
+ */
+ public Either getSemanticTokensFullDelta(
+ DocumentContext documentContext,
+ SemanticTokensDeltaParams params
+ ) {
+ String previousResultId = params.getPreviousResultId();
+ CachedTokenData previousData = tokenCache.get(previousResultId);
+
+ // Collect tokens from all suppliers in parallel
+ var entries = collectTokens(documentContext);
+
+ // Build delta-encoded data
+ List currentData = toDeltaEncoded(entries);
+
+ // Generate new resultId
+ String resultId = generateResultId();
+
+ // If previous data is not available or belongs to a different document, return full tokens
+ if (previousData == null || !previousData.uri().equals(documentContext.getUri())) {
+ cacheTokenData(resultId, documentContext.getUri(), currentData);
+ return Either.forLeft(new SemanticTokens(resultId, currentData));
+ }
+
+ // Compute delta edits
+ List edits = computeEdits(previousData.data(), currentData);
+
+ // Cache the new data
+ cacheTokenData(resultId, documentContext.getUri(), currentData);
+
+ // Remove the old cached data
+ tokenCache.remove(previousResultId);
+
+ var delta = new SemanticTokensDelta();
+ delta.setResultId(resultId);
+ delta.setEdits(edits);
+ return Either.forRight(delta);
+ }
+
+ /**
+ * Обрабатывает событие закрытия документа в контексте сервера.
+ *
+ * При закрытии документа очищает кэшированные данные семантических токенов.
+ *
+ * @param event событие закрытия документа
+ */
+ @EventListener
+ public void handleDocumentClosed(ServerContextDocumentClosedEvent event) {
+ clearCache(event.getDocumentContext().getUri());
+ }
+
+ /**
+ * Обрабатывает событие удаления документа из контекста сервера.
+ *
+ * При удалении документа очищает кэшированные данные семантических токенов.
+ *
+ * @param event событие удаления документа
+ */
+ @EventListener
+ public void handleDocumentRemoved(ServerContextDocumentRemovedEvent event) {
+ clearCache(event.getUri());
+ }
+
+ /**
+ * Очищает кэшированные данные токенов для указанного документа.
+ *
+ * @param uri URI документа, для которого нужно очистить кэш
+ */
+ protected void clearCache(URI uri) {
+ tokenCache.entrySet().removeIf(entry -> entry.getValue().uri().equals(uri));
+ }
+
+ /**
+ * Generate a unique result ID for caching.
+ */
+ private static String generateResultId() {
+ return UUID.randomUUID().toString();
+ }
+
+ /**
+ * Cache token data with the given resultId.
+ */
+ private void cacheTokenData(String resultId, URI uri, List data) {
+ tokenCache.put(resultId, new CachedTokenData(uri, data));
+ }
+
+ /**
+ * Compute edits to transform previousData into currentData.
+ * Uses a simple algorithm that produces a single edit covering the entire change.
+ */
+ private static List computeEdits(List previousData, List currentData) {
+ // Find the first differing index
+ int minSize = Math.min(previousData.size(), currentData.size());
+ int prefixMatch = 0;
+ while (prefixMatch < minSize && previousData.get(prefixMatch).equals(currentData.get(prefixMatch))) {
+ prefixMatch++;
+ }
+
+ // If both are identical, return empty edits
+ if (prefixMatch == previousData.size() && prefixMatch == currentData.size()) {
+ return List.of();
+ }
+
+ // Find the last differing index (from the end)
+ int suffixMatch = 0;
+ while (suffixMatch < minSize - prefixMatch
+ && previousData.get(previousData.size() - 1 - suffixMatch)
+ .equals(currentData.get(currentData.size() - 1 - suffixMatch))) {
+ suffixMatch++;
+ }
+
+ // Calculate the range to replace
+ int deleteStart = prefixMatch;
+ int deleteCount = previousData.size() - prefixMatch - suffixMatch;
+ int insertEnd = currentData.size() - suffixMatch;
+
+ // Extract the data to insert
+ List insertData = currentData.subList(prefixMatch, insertEnd);
+
+ var edit = new SemanticTokensEdit();
+ edit.setStart(deleteStart);
+ edit.setDeleteCount(deleteCount);
+ if (!insertData.isEmpty()) {
+ edit.setData(new ArrayList<>(insertData));
+ }
+
+ return List.of(edit);
+ }
+
+ /**
+ * Collect tokens from all suppliers in parallel using ForkJoinPool.
+ */
+ private List collectTokens(DocumentContext documentContext) {
+ return CompletableFuture
+ .supplyAsync(
+ () -> suppliers.parallelStream()
+ .map(supplier -> supplier.getSemanticTokens(documentContext))
+ .flatMap(Collection::stream)
+ .toList(),
+ executorService
+ )
+ .join();
}
private static List toDeltaEncoded(List entries) {
@@ -76,9 +277,11 @@ private static List toDeltaEncoded(List entries) {
.comparingInt(SemanticTokenEntry::line)
.thenComparingInt(SemanticTokenEntry::start));
- List data = new ArrayList<>(sorted.size() * 5);
+ // Use int[] to avoid boxing overhead during computation
+ int[] data = new int[sorted.size() * 5];
var prevLine = 0;
var prevChar = 0;
+ var index = 0;
var first = true;
for (SemanticTokenEntry tokenEntry : sorted) {
@@ -86,16 +289,18 @@ private static List toDeltaEncoded(List entries) {
int prevCharOrZero = (deltaLine == 0) ? prevChar : 0;
int deltaStart = first ? tokenEntry.start() : (tokenEntry.start() - prevCharOrZero);
- data.add(deltaLine);
- data.add(deltaStart);
- data.add(tokenEntry.length());
- data.add(tokenEntry.type());
- data.add(tokenEntry.modifiers());
+ data[index++] = deltaLine;
+ data[index++] = deltaStart;
+ data[index++] = tokenEntry.length();
+ data[index++] = tokenEntry.type();
+ data[index++] = tokenEntry.modifiers();
prevLine = tokenEntry.line();
prevChar = tokenEntry.start();
first = false;
}
- return data;
+
+ // Convert to List for LSP4J API
+ return Arrays.stream(data).boxed().toList();
}
}
diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java
index befaef99b47..82377608092 100644
--- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java
+++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentServiceTest.java
@@ -25,6 +25,7 @@
import com.github._1c_syntax.bsl.languageserver.jsonrpc.DiagnosticParams;
import com.github._1c_syntax.bsl.languageserver.util.CleanupContextBeforeClassAndAfterClass;
import com.github._1c_syntax.bsl.languageserver.utils.Ranges;
+import com.github._1c_syntax.utils.Absolute;
import org.apache.commons.io.FileUtils;
import org.eclipse.lsp4j.DidChangeTextDocumentParams;
import org.eclipse.lsp4j.DidCloseTextDocumentParams;
@@ -95,7 +96,7 @@ void didChangeIncremental() throws IOException {
var didOpenParams = new DidOpenTextDocumentParams(textDocumentItem);
textDocumentService.didOpen(didOpenParams);
- var documentContext = serverContext.getDocument(textDocumentItem.getUri());
+ var documentContext = serverContext.getDocumentUnsafe(textDocumentItem.getUri());
assertThat(documentContext).isNotNull();
// when - incremental change: insert text at position
@@ -123,7 +124,7 @@ void didChangeIncrementalMultipleChanges() throws IOException {
var didOpenParams = new DidOpenTextDocumentParams(textDocumentItem);
textDocumentService.didOpen(didOpenParams);
- var documentContext = serverContext.getDocument(textDocumentItem.getUri());
+ var documentContext = serverContext.getDocumentUnsafe(textDocumentItem.getUri());
assertThat(documentContext).isNotNull();
// when - multiple incremental changes
@@ -157,7 +158,7 @@ void didChangeIncrementalDelete() throws IOException {
var didOpenParams = new DidOpenTextDocumentParams(textDocumentItem);
textDocumentService.didOpen(didOpenParams);
- var documentContext = serverContext.getDocument(textDocumentItem.getUri());
+ var documentContext = serverContext.getDocumentUnsafe(textDocumentItem.getUri());
assertThat(documentContext).isNotNull();
// when - incremental change: delete text
@@ -334,7 +335,7 @@ private File getTestFile() {
private TextDocumentItem getTextDocumentItem() throws IOException {
File file = getTestFile();
- String uri = file.toURI().toString();
+ String uri = Absolute.uri(file).toString();
String fileContent = FileUtils.readFileToString(file, StandardCharsets.UTF_8);
@@ -344,7 +345,7 @@ private TextDocumentItem getTextDocumentItem() throws IOException {
private TextDocumentIdentifier getTextDocumentIdentifier() {
// TODO: Переделать на TestUtils.getTextDocumentIdentifier();
File file = getTestFile();
- String uri = file.toURI().toString();
+ String uri = Absolute.uri(file).toString();
return new TextDocumentIdentifier(uri);
}
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 ded5168b9c1..869726045ce 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
@@ -28,6 +28,7 @@
import org.eclipse.lsp4j.SemanticTokenModifiers;
import org.eclipse.lsp4j.SemanticTokenTypes;
import org.eclipse.lsp4j.SemanticTokens;
+import org.eclipse.lsp4j.SemanticTokensDeltaParams;
import org.eclipse.lsp4j.SemanticTokensLegend;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.TextDocumentIdentifier;
@@ -35,6 +36,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
+import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -1096,5 +1098,249 @@ void sdblQuery_tableWithObjectTableName() {
}
// endregion
+
+ // region Delta tokens tests
+
+ @Test
+ void fullTokensReturnsResultId() {
+ // given
+ String bsl = """
+ Перем А;
+ """;
+
+ // when
+ SemanticTokens tokens = getTokens(bsl);
+
+ // then
+ assertThat(tokens.getResultId()).isNotNull();
+ assertThat(tokens.getResultId()).isNotEmpty();
+ }
+
+ @Test
+ void deltaWithSameDocument_returnsEmptyEdits() {
+ // given
+ String bsl = """
+ Перем А;
+ """;
+
+ DocumentContext documentContext = TestUtils.getDocumentContext(bsl);
+ referenceIndexFiller.fill(documentContext);
+ TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri());
+
+ SemanticTokens initialTokens = provider.getSemanticTokensFull(
+ documentContext,
+ new SemanticTokensParams(textDocumentIdentifier)
+ );
+ assertThat(initialTokens.getResultId()).isNotNull();
+
+ // when
+ var deltaParams = new SemanticTokensDeltaParams(textDocumentIdentifier, initialTokens.getResultId());
+ var result = provider.getSemanticTokensFullDelta(documentContext, deltaParams);
+
+ // then
+ assertThat(result.isRight()).isTrue();
+ var delta = result.getRight();
+ assertThat(delta.getResultId()).isNotNull();
+ assertThat(delta.getResultId()).isNotEqualTo(initialTokens.getResultId());
+ assertThat(delta.getEdits()).isEmpty();
+ }
+
+ @Test
+ void deltaWithUnknownPreviousResultId_returnsFullTokens() {
+ // given
+ String bsl = """
+ Перем А;
+ """;
+
+ DocumentContext documentContext = TestUtils.getDocumentContext(bsl);
+ referenceIndexFiller.fill(documentContext);
+ TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri());
+
+ // when
+ var deltaParams = new SemanticTokensDeltaParams(textDocumentIdentifier, "unknown-result-id");
+ var result = provider.getSemanticTokensFullDelta(documentContext, deltaParams);
+
+ // then
+ assertThat(result.isLeft()).isTrue();
+ var fullTokens = result.getLeft();
+ assertThat(fullTokens.getResultId()).isNotNull();
+ assertThat(fullTokens.getData()).isNotEmpty();
+ }
+
+ @Test
+ void deltaWithChangedDocument_returnsEdits() {
+ // given
+ String bsl1 = """
+ Перем А;
+ """;
+
+ String bsl2 = """
+ Перем А;
+ Перем Б;
+ """;
+
+ DocumentContext context1 = TestUtils.getDocumentContext(bsl1);
+ referenceIndexFiller.fill(context1);
+ TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri());
+ SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1));
+
+ URI differentUri = URI.create("file:///fake/different-document.bsl");
+ DocumentContext context2 = TestUtils.getDocumentContext(differentUri, bsl2);
+ referenceIndexFiller.fill(context2);
+ TextDocumentIdentifier textDocId2 = TestUtils.getTextDocumentIdentifier(context2.getUri());
+
+ // when
+ var deltaParams = new SemanticTokensDeltaParams(textDocId2, tokens1.getResultId());
+ var result = provider.getSemanticTokensFullDelta(context2, deltaParams);
+
+ // then
+ assertThat(result.isLeft()).isTrue();
+ var fullTokens = result.getLeft();
+ assertThat(fullTokens.getResultId()).isNotNull();
+ }
+
+ @Test
+ void deltaWithModifiedSameDocument_returnsEdits() {
+ // given
+ String bsl1 = """
+ Перем А;
+ """;
+
+ String bsl2 = """
+ Перем А;
+ Перем Б;
+ """;
+
+ DocumentContext context1 = TestUtils.getDocumentContext(bsl1);
+ referenceIndexFiller.fill(context1);
+ TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri());
+ SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1));
+
+ DocumentContext context2 = TestUtils.getDocumentContext(context1.getUri(), bsl2);
+ referenceIndexFiller.fill(context2);
+
+ // when
+ var deltaParams = new SemanticTokensDeltaParams(textDocId1, tokens1.getResultId());
+ var result = provider.getSemanticTokensFullDelta(context2, deltaParams);
+
+ // then
+ assertThat(result.isRight()).isTrue();
+ var delta = result.getRight();
+ assertThat(delta.getResultId()).isNotNull();
+ assertThat(delta.getEdits()).isNotEmpty();
+ var edit = delta.getEdits().get(0);
+ assertThat(edit.getDeleteCount() + (edit.getData() != null ? edit.getData().size() : 0))
+ .isGreaterThan(0);
+ }
+
+ @Test
+ void clearCache_removesCachedTokenData() {
+ // given
+ String bsl = """
+ Перем А;
+ """;
+
+ DocumentContext documentContext = TestUtils.getDocumentContext(bsl);
+ referenceIndexFiller.fill(documentContext);
+ TextDocumentIdentifier textDocumentIdentifier = TestUtils.getTextDocumentIdentifier(documentContext.getUri());
+
+ SemanticTokens initialTokens = provider.getSemanticTokensFull(
+ documentContext,
+ new SemanticTokensParams(textDocumentIdentifier)
+ );
+
+ // when
+ provider.clearCache(documentContext.getUri());
+
+ var deltaParams = new SemanticTokensDeltaParams(textDocumentIdentifier, initialTokens.getResultId());
+ var result = provider.getSemanticTokensFullDelta(documentContext, deltaParams);
+
+ // then
+ assertThat(result.isLeft()).isTrue();
+ }
+
+ @Test
+ void deltaWithLineInsertedAtBeginning_shouldHaveSmallDelta() {
+ // given
+ String bsl1 = """
+ Перем А;
+ Перем Б;
+ Перем В;
+ """;
+
+ String bsl2 = """
+ Перем Новая;
+ Перем А;
+ Перем Б;
+ Перем В;
+ """;
+
+ DocumentContext context1 = TestUtils.getDocumentContext(bsl1);
+ referenceIndexFiller.fill(context1);
+ TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri());
+ SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1));
+
+ DocumentContext context2 = TestUtils.getDocumentContext(context1.getUri(), bsl2);
+ referenceIndexFiller.fill(context2);
+
+ // when
+ var deltaParams = new SemanticTokensDeltaParams(textDocId1, tokens1.getResultId());
+ var result = provider.getSemanticTokensFullDelta(context2, deltaParams);
+
+ // then - should return delta with small edits (just the new token + changed deltaLine)
+ assertThat(result.isRight()).isTrue();
+ var delta = result.getRight();
+ var edit = delta.getEdits().get(0);
+ // For inserting at beginning: prefix=0, suffix should match most of the old data
+ // deleteCount should be small (just the first deltaLine that changed)
+ // insertData should be the new token + updated first deltaLine
+ assertThat(edit.getDeleteCount()).isLessThan(tokens1.getData().size());
+ }
+
+ @Test
+ void deltaWithLineInsertedInMiddle_shouldReturnOptimalDelta() {
+ // given - simulate inserting a line in middle of document
+ String bsl1 = """
+ Перем А;
+ Перем Б;
+ Перем В;
+ Перем Г;
+ """;
+
+ String bsl2 = """
+ Перем А;
+ Перем Б;
+ Перем Новая;
+ Перем В;
+ Перем Г;
+ """;
+
+ DocumentContext context1 = TestUtils.getDocumentContext(bsl1);
+ referenceIndexFiller.fill(context1);
+ TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri());
+ SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1));
+ int originalDataSize = tokens1.getData().size();
+
+ DocumentContext context2 = TestUtils.getDocumentContext(context1.getUri(), bsl2);
+ referenceIndexFiller.fill(context2);
+
+ // when
+ var deltaParams = new SemanticTokensDeltaParams(textDocId1, tokens1.getResultId());
+ var result = provider.getSemanticTokensFullDelta(context2, deltaParams);
+
+ // then - should return delta, not full tokens
+ assertThat(result.isRight()).isTrue();
+ var delta = result.getRight();
+ assertThat(delta.getEdits()).isNotEmpty();
+ var edit = delta.getEdits().get(0);
+ // For insertion in middle:
+ // - prefix matches up to insertion point
+ // - suffix matches tokens after insertion (they have same relative deltaLine)
+ // - The edit should be smaller than the full data
+ int editSize = edit.getDeleteCount() + (edit.getData() != null ? edit.getData().size() : 0);
+ assertThat(editSize).isLessThan(originalDataSize);
+ }
+
+ // endregion
}
diff --git a/src/test/java/com/github/_1c_syntax/bsl/languageserver/util/TestUtils.java b/src/test/java/com/github/_1c_syntax/bsl/languageserver/util/TestUtils.java
index 73f08d9b17b..610d2ad56f8 100644
--- a/src/test/java/com/github/_1c_syntax/bsl/languageserver/util/TestUtils.java
+++ b/src/test/java/com/github/_1c_syntax/bsl/languageserver/util/TestUtils.java
@@ -68,7 +68,8 @@ public static DocumentContext getDocumentContext(String fileContent, @Nullable S
}
public static DocumentContext getDocumentContext(URI uri, String fileContent, ServerContext context) {
- var documentContext = context.addDocument(uri);
+ var normalizedUri = Absolute.uri(uri);
+ var documentContext = context.addDocument(normalizedUri);
context.rebuildDocument(documentContext, fileContent, 0);
return documentContext;
}