-
Notifications
You must be signed in to change notification settings - Fork 121
Оптимизация хранения семантических токенов, фикс расчёта дельт #3706
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
65674c2
1c03de3
529b40d
61a2db0
b3da273
f154def
805fb88
a967801
9ee333e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -91,9 +91,9 @@ private void onDestroy() { | |||||||||||||||||||||||||||||
| * Cached semantic token data associated with a document. | ||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||
| * @param uri URI of the document | ||||||||||||||||||||||||||||||
| * @param data token data list | ||||||||||||||||||||||||||||||
| * @param data token data as int array (more efficient than List<Integer>) | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| private record CachedTokenData(URI uri, List<Integer> data) { | ||||||||||||||||||||||||||||||
| private record CachedTokenData(URI uri, int[] data) { | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
|
|
@@ -110,14 +110,14 @@ public SemanticTokens getSemanticTokensFull( | |||||||||||||||||||||||||||||
| // Collect tokens from all suppliers in parallel | ||||||||||||||||||||||||||||||
| var entries = collectTokens(documentContext); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Build delta-encoded data | ||||||||||||||||||||||||||||||
| List<Integer> data = toDeltaEncoded(entries); | ||||||||||||||||||||||||||||||
| // Build delta-encoded data as int array | ||||||||||||||||||||||||||||||
| int[] data = toDeltaEncodedArray(entries); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Generate a unique resultId and cache the data | ||||||||||||||||||||||||||||||
| String resultId = generateResultId(); | ||||||||||||||||||||||||||||||
| cacheTokenData(resultId, documentContext.getUri(), data); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return new SemanticTokens(resultId, data); | ||||||||||||||||||||||||||||||
| return new SemanticTokens(resultId, toList(data)); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
|
|
@@ -137,16 +137,16 @@ public Either<SemanticTokens, SemanticTokensDelta> getSemanticTokensFullDelta( | |||||||||||||||||||||||||||||
| // Collect tokens from all suppliers in parallel | ||||||||||||||||||||||||||||||
| var entries = collectTokens(documentContext); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Build delta-encoded data | ||||||||||||||||||||||||||||||
| List<Integer> currentData = toDeltaEncoded(entries); | ||||||||||||||||||||||||||||||
| // Build delta-encoded data as int array | ||||||||||||||||||||||||||||||
| int[] currentData = toDeltaEncodedArray(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)); | ||||||||||||||||||||||||||||||
| return Either.forLeft(new SemanticTokens(resultId, toList(currentData))); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Compute delta edits | ||||||||||||||||||||||||||||||
|
|
@@ -207,53 +207,159 @@ private static String generateResultId() { | |||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Cache token data with the given resultId. | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| private void cacheTokenData(String resultId, URI uri, List<Integer> data) { | ||||||||||||||||||||||||||||||
| private void cacheTokenData(String resultId, URI uri, int[] 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. | ||||||||||||||||||||||||||||||
| * <p> | ||||||||||||||||||||||||||||||
| * Учитывает структуру семантических токенов (группы по 5 элементов: deltaLine, deltaStart, length, type, modifiers) | ||||||||||||||||||||||||||||||
| * и смещение строк при вставке/удалении строк в документе. | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| private static List<SemanticTokensEdit> computeEdits(List<Integer> previousData, List<Integer> 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++; | ||||||||||||||||||||||||||||||
| private static List<SemanticTokensEdit> computeEdits(int[] prev, int[] curr) { | ||||||||||||||||||||||||||||||
| final int TOKEN_SIZE = 5; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| int prevTokenCount = prev.length / TOKEN_SIZE; | ||||||||||||||||||||||||||||||
| int currTokenCount = curr.length / TOKEN_SIZE; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (prevTokenCount == 0 && currTokenCount == 0) { | ||||||||||||||||||||||||||||||
| return List.of(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // If both are identical, return empty edits | ||||||||||||||||||||||||||||||
| if (prefixMatch == previousData.size() && prefixMatch == currentData.size()) { | ||||||||||||||||||||||||||||||
| // Находим первый отличающийся токен и одновременно вычисляем сумму deltaLine для prefix | ||||||||||||||||||||||||||||||
| int firstDiffToken = 0; | ||||||||||||||||||||||||||||||
| int prefixAbsLine = 0; | ||||||||||||||||||||||||||||||
| int minTokens = Math.min(prevTokenCount, currTokenCount); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| outer: | ||||||||||||||||||||||||||||||
| for (int i = 0; i < minTokens; i++) { | ||||||||||||||||||||||||||||||
| int base = i * TOKEN_SIZE; | ||||||||||||||||||||||||||||||
| for (int j = 0; j < TOKEN_SIZE; j++) { | ||||||||||||||||||||||||||||||
| if (prev[base + j] != curr[base + j]) { | ||||||||||||||||||||||||||||||
| firstDiffToken = i; | ||||||||||||||||||||||||||||||
| break outer; | ||||||||||||||||||||||||||||||
|
Comment on lines
+235
to
+241
|
||||||||||||||||||||||||||||||
| outer: | |
| for (int i = 0; i < minTokens; i++) { | |
| int base = i * TOKEN_SIZE; | |
| for (int j = 0; j < TOKEN_SIZE; j++) { | |
| if (prev[base + j] != curr[base + j]) { | |
| firstDiffToken = i; | |
| break outer; | |
| findFirstDiff: | |
| for (int i = 0; i < minTokens; i++) { | |
| int base = i * TOKEN_SIZE; | |
| for (int j = 0; j < TOKEN_SIZE; j++) { | |
| if (prev[base + j] != curr[base + j]) { | |
| firstDiffToken = i; | |
| break findFirstDiff; |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The condition check at line 275 may be incorrect. When deleteCount is 0 and deleteStart equals insertEnd, it means there's nothing to delete and nothing to insert. However, this check uses deleteStart (which is firstDiffToken * TOKEN_SIZE) rather than checking if there's actually any difference. A more correct check would be to verify if deleteCount is 0 and the insert range (from deleteStart to insertEnd) is empty, but the current logic already handles this correctly. Consider whether this early return is truly necessary, as the edit creation below would naturally produce an edit with deleteCount=0 and empty insertData, which might be semantically valid for LSP.
| if (deleteCount == 0 && deleteStart == insertEnd) { | |
| return List.of(); | |
| } |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test coverage for the new delta calculation algorithm should be expanded. The existing tests (deltaWithLineInsertedAtBeginning and deltaWithLineInsertedInMiddle) verify that the delta is smaller than the full data, but they don't verify the correctness of the actual edit content. Consider adding assertions to verify that applying the edit to the previous data actually produces the current data, to ensure the delta calculation is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot учитывает ли это вставку текста без перевода строки?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@coderabbitai ну или ты ответь
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nixel2007, да, код учитывает вставку текста без перевода строки, но обрабатывает её иначе, чем вставку/удаление целых строк.
При вставке текста на той же строке (без \n):
- Токены на этой строке получат изменённые значения
deltaStart(смещение позиции) - Токены на последующих строках сохранят те же
deltaLineзначения
Алгоритм обнаружит такие изменения через проверку всех полей токена в findSuffixMatchWithOffset:
// Сначала проверяем все поля кроме deltaLine
for (int j = 1; j < tokenSize; j++) { // j=1 это deltaStart
if (prev[prevIdx + j] != curr[currIdx + j]) {
otherFieldsMatch = false;
break;
}
}Если deltaStart отличается, токены не считаются совпадающими и попадут в область редактирования.
Оптимизация с lineOffset специально работает для вертикальных сдвигов (вставка/удаление строк), когда deltaLine изменяется, но сами токены остаются идентичными. Для горизонтальных сдвигов (вставка в той же строке) такой оптимизации нет, но это корректное поведение — позиции токенов действительно изменились и требуют обновления.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The constant TOKEN_SIZE is defined locally in multiple methods (computeEdits and findSuffixMatchWithOffset). Consider extracting this as a class-level private static final constant to avoid duplication and ensure consistency across methods. This would make the code more maintainable if the token structure ever changes.