Skip to content

Commit 78ca30f

Browse files
authored
Merge branch 'develop' into copilot/add-semantic-tokens-support-again
2 parents 6130158 + 116269d commit 78ca30f

File tree

2 files changed

+221
-34
lines changed

2 files changed

+221
-34
lines changed

src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProvider.java

Lines changed: 143 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ private void onDestroy() {
9999
* Cached semantic token data associated with a document.
100100
*
101101
* @param uri URI of the document
102-
* @param data token data list
102+
* @param data token data as int array (more efficient than List<Integer>)
103103
*/
104-
private record CachedTokenData(URI uri, List<Integer> data) {
104+
private record CachedTokenData(URI uri, int[] data) {
105105
}
106106

107107
/**
@@ -118,14 +118,14 @@ public SemanticTokens getSemanticTokensFull(
118118
// Collect tokens from all suppliers in parallel
119119
var entries = collectTokens(documentContext);
120120

121-
// Build delta-encoded data
122-
List<Integer> data = toDeltaEncoded(entries);
121+
// Build delta-encoded data as int array
122+
int[] data = toDeltaEncodedArray(entries);
123123

124124
// Generate a unique resultId and cache the data
125125
String resultId = generateResultId();
126126
cacheTokenData(resultId, documentContext.getUri(), data);
127127

128-
return new SemanticTokens(resultId, data);
128+
return new SemanticTokens(resultId, toList(data));
129129
}
130130

131131
/**
@@ -145,16 +145,16 @@ public Either<SemanticTokens, SemanticTokensDelta> getSemanticTokensFullDelta(
145145
// Collect tokens from all suppliers in parallel
146146
var entries = collectTokens(documentContext);
147147

148-
// Build delta-encoded data
149-
List<Integer> currentData = toDeltaEncoded(entries);
148+
// Build delta-encoded data as int array
149+
int[] currentData = toDeltaEncodedArray(entries);
150150

151151
// Generate new resultId
152152
String resultId = generateResultId();
153153

154154
// If previous data is not available or belongs to a different document, return full tokens
155155
if (previousData == null || !previousData.uri().equals(documentContext.getUri())) {
156156
cacheTokenData(resultId, documentContext.getUri(), currentData);
157-
return Either.forLeft(new SemanticTokens(resultId, currentData));
157+
return Either.forLeft(new SemanticTokens(resultId, toList(currentData)));
158158
}
159159

160160
// Compute delta edits
@@ -302,53 +302,159 @@ private static String generateResultId() {
302302
/**
303303
* Cache token data with the given resultId.
304304
*/
305-
private void cacheTokenData(String resultId, URI uri, List<Integer> data) {
305+
private void cacheTokenData(String resultId, URI uri, int[] data) {
306306
tokenCache.put(resultId, new CachedTokenData(uri, data));
307307
}
308308

309309
/**
310310
* Compute edits to transform previousData into currentData.
311-
* Uses a simple algorithm that produces a single edit covering the entire change.
311+
* <p>
312+
* Учитывает структуру семантических токенов (группы по 5 элементов: deltaLine, deltaStart, length, type, modifiers)
313+
* и смещение строк при вставке/удалении строк в документе.
312314
*/
313-
private static List<SemanticTokensEdit> computeEdits(List<Integer> previousData, List<Integer> currentData) {
314-
// Find the first differing index
315-
int minSize = Math.min(previousData.size(), currentData.size());
316-
int prefixMatch = 0;
317-
while (prefixMatch < minSize && previousData.get(prefixMatch).equals(currentData.get(prefixMatch))) {
318-
prefixMatch++;
315+
private static List<SemanticTokensEdit> computeEdits(int[] prev, int[] curr) {
316+
final int TOKEN_SIZE = 5;
317+
318+
int prevTokenCount = prev.length / TOKEN_SIZE;
319+
int currTokenCount = curr.length / TOKEN_SIZE;
320+
321+
if (prevTokenCount == 0 && currTokenCount == 0) {
322+
return List.of();
323+
}
324+
325+
// Находим первый отличающийся токен и одновременно вычисляем сумму deltaLine для prefix
326+
int firstDiffToken = 0;
327+
int prefixAbsLine = 0;
328+
int minTokens = Math.min(prevTokenCount, currTokenCount);
329+
330+
outer:
331+
for (int i = 0; i < minTokens; i++) {
332+
int base = i * TOKEN_SIZE;
333+
for (int j = 0; j < TOKEN_SIZE; j++) {
334+
if (prev[base + j] != curr[base + j]) {
335+
firstDiffToken = i;
336+
break outer;
337+
}
338+
}
339+
prefixAbsLine += prev[base]; // накапливаем deltaLine
340+
firstDiffToken = i + 1;
319341
}
320342

321-
// If both are identical, return empty edits
322-
if (prefixMatch == previousData.size() && prefixMatch == currentData.size()) {
343+
// Если все токены одинаковые
344+
if (firstDiffToken == minTokens && prevTokenCount == currTokenCount) {
323345
return List.of();
324346
}
325347

326-
// Find the last differing index (from the end)
327-
int suffixMatch = 0;
328-
while (suffixMatch < minSize - prefixMatch
329-
&& previousData.get(previousData.size() - 1 - suffixMatch)
330-
.equals(currentData.get(currentData.size() - 1 - suffixMatch))) {
331-
suffixMatch++;
348+
// Вычисляем смещение строк инкрементально от prefixAbsLine
349+
int prevSuffixAbsLine = prefixAbsLine;
350+
for (int i = firstDiffToken; i < prevTokenCount; i++) {
351+
prevSuffixAbsLine += prev[i * TOKEN_SIZE];
332352
}
353+
int currSuffixAbsLine = prefixAbsLine;
354+
for (int i = firstDiffToken; i < currTokenCount; i++) {
355+
currSuffixAbsLine += curr[i * TOKEN_SIZE];
356+
}
357+
int lineOffset = currSuffixAbsLine - prevSuffixAbsLine;
358+
359+
// Находим последний отличающийся токен с учётом смещения строк
360+
int suffixMatchTokens = findSuffixMatchWithOffset(prev, curr, firstDiffToken, lineOffset, TOKEN_SIZE);
333361

334-
// Calculate the range to replace
335-
int deleteStart = prefixMatch;
336-
int deleteCount = previousData.size() - prefixMatch - suffixMatch;
337-
int insertEnd = currentData.size() - suffixMatch;
362+
// Вычисляем границы редактирования
363+
int deleteEndToken = prevTokenCount - suffixMatchTokens;
364+
int insertEndToken = currTokenCount - suffixMatchTokens;
338365

339-
// Extract the data to insert
340-
List<Integer> insertData = currentData.subList(prefixMatch, insertEnd);
366+
int deleteStart = firstDiffToken * TOKEN_SIZE;
367+
int deleteCount = (deleteEndToken - firstDiffToken) * TOKEN_SIZE;
368+
int insertEnd = insertEndToken * TOKEN_SIZE;
369+
370+
if (deleteCount == 0 && deleteStart == insertEnd) {
371+
return List.of();
372+
}
373+
374+
// Создаём список для вставки из среза массива
375+
List<Integer> insertData = toList(Arrays.copyOfRange(curr, deleteStart, insertEnd));
341376

342377
var edit = new SemanticTokensEdit();
343378
edit.setStart(deleteStart);
344379
edit.setDeleteCount(deleteCount);
345380
if (!insertData.isEmpty()) {
346-
edit.setData(new ArrayList<>(insertData));
381+
edit.setData(insertData);
347382
}
348383

349384
return List.of(edit);
350385
}
351386

387+
/**
388+
* Находит количество совпадающих токенов с конца, учитывая смещение строк.
389+
* <p>
390+
* При дельта-кодировании токены после точки вставки идентичны,
391+
* кроме первого токена, у которого deltaLine смещён на lineOffset.
392+
* При вставке текста без перевода строки (lineOffset == 0), первый токен
393+
* может иметь смещённый deltaStart.
394+
*/
395+
private static int findSuffixMatchWithOffset(int[] prev, int[] curr, int firstDiffToken, int lineOffset, int tokenSize) {
396+
final int DELTA_LINE_INDEX = 0;
397+
final int DELTA_START_INDEX = 1;
398+
399+
int prevTokenCount = prev.length / tokenSize;
400+
int currTokenCount = curr.length / tokenSize;
401+
402+
int maxPrevSuffix = prevTokenCount - firstDiffToken;
403+
int maxCurrSuffix = currTokenCount - firstDiffToken;
404+
int maxSuffix = Math.min(maxPrevSuffix, maxCurrSuffix);
405+
406+
int suffixMatch = 0;
407+
boolean foundBoundary = false;
408+
409+
for (int i = 0; i < maxSuffix; i++) {
410+
int prevIdx = (prevTokenCount - 1 - i) * tokenSize;
411+
int currIdx = (currTokenCount - 1 - i) * tokenSize;
412+
413+
// Для граничного токена при inline-редактировании (lineOffset == 0)
414+
// разрешаем различие в deltaStart
415+
int firstFieldToCheck = (!foundBoundary && lineOffset == 0) ? DELTA_START_INDEX + 1 : DELTA_START_INDEX;
416+
417+
// Проверяем поля кроме deltaLine (и возможно deltaStart для граничного токена)
418+
boolean otherFieldsMatch = true;
419+
for (int j = firstFieldToCheck; j < tokenSize; j++) {
420+
if (prev[prevIdx + j] != curr[currIdx + j]) {
421+
otherFieldsMatch = false;
422+
break;
423+
}
424+
}
425+
426+
if (!otherFieldsMatch) {
427+
break;
428+
}
429+
430+
// Теперь проверяем deltaLine
431+
int prevDeltaLine = prev[prevIdx + DELTA_LINE_INDEX];
432+
int currDeltaLine = curr[currIdx + DELTA_LINE_INDEX];
433+
434+
if (prevDeltaLine == currDeltaLine) {
435+
// Полное совпадение (или совпадение с учётом deltaStart при inline-редактировании)
436+
suffixMatch++;
437+
// Если это был граничный токен при inline-редактировании, отмечаем его найденным
438+
if (!foundBoundary && lineOffset == 0) {
439+
int prevDeltaStart = prev[prevIdx + DELTA_START_INDEX];
440+
int currDeltaStart = curr[currIdx + DELTA_START_INDEX];
441+
if (prevDeltaStart != currDeltaStart) {
442+
foundBoundary = true;
443+
}
444+
}
445+
} else if (!foundBoundary && currDeltaLine - prevDeltaLine == lineOffset) {
446+
// Граничный токен — deltaLine отличается ровно на lineOffset
447+
suffixMatch++;
448+
foundBoundary = true;
449+
} else {
450+
// Не совпадает
451+
break;
452+
}
453+
}
454+
455+
return suffixMatch;
456+
}
457+
352458
/**
353459
* Collect tokens from all suppliers in parallel using ForkJoinPool.
354460
*/
@@ -364,7 +470,7 @@ private List<SemanticTokenEntry> collectTokens(DocumentContext documentContext)
364470
.join();
365471
}
366472

367-
private static List<Integer> toDeltaEncoded(List<SemanticTokenEntry> entries) {
473+
private static int[] toDeltaEncodedArray(List<SemanticTokenEntry> entries) {
368474
// de-dup and sort
369475
Set<SemanticTokenEntry> uniq = new HashSet<>(entries);
370476
List<SemanticTokenEntry> sorted = new ArrayList<>(uniq);
@@ -395,7 +501,10 @@ private static List<Integer> toDeltaEncoded(List<SemanticTokenEntry> entries) {
395501
first = false;
396502
}
397503

398-
// Convert to List<Integer> for LSP4J API
399-
return Arrays.stream(data).boxed().toList();
504+
return data;
505+
}
506+
507+
private static List<Integer> toList(int[] array) {
508+
return Arrays.stream(array).boxed().toList();
400509
}
401510
}

src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SemanticTokensProviderTest.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,84 @@ void deltaWithLineInsertedInMiddle_shouldReturnOptimalDelta() {
13441344
assertThat(editSize).isLessThan(originalDataSize);
13451345
}
13461346

1347+
@Test
1348+
void deltaWithTextInsertedOnSameLine_shouldReturnOptimalDelta() {
1349+
// given - simulate inserting text on the same line without line breaks
1350+
// This tests the case raised by @nixel2007: text insertion without newline
1351+
String bsl1 = """
1352+
Перем А;
1353+
""";
1354+
1355+
String bsl2 = """
1356+
Перем Новая, А;
1357+
""";
1358+
1359+
DocumentContext context1 = TestUtils.getDocumentContext(bsl1);
1360+
referenceIndexFiller.fill(context1);
1361+
TextDocumentIdentifier textDocId1 = TestUtils.getTextDocumentIdentifier(context1.getUri());
1362+
SemanticTokens tokens1 = provider.getSemanticTokensFull(context1, new SemanticTokensParams(textDocId1));
1363+
1364+
// Verify original tokens structure
1365+
var decoded1 = decode(tokens1.getData());
1366+
var expected1 = List.of(
1367+
new ExpectedToken(0, 0, 5, SemanticTokenTypes.Keyword, "Перем"),
1368+
new ExpectedToken(0, 6, 1, SemanticTokenTypes.Variable, SemanticTokenModifiers.Definition, "А"),
1369+
new ExpectedToken(0, 7, 1, SemanticTokenTypes.Operator, ";")
1370+
);
1371+
assertTokensMatch(decoded1, expected1);
1372+
1373+
DocumentContext context2 = TestUtils.getDocumentContext(context1.getUri(), bsl2);
1374+
referenceIndexFiller.fill(context2);
1375+
SemanticTokens tokens2 = provider.getSemanticTokensFull(context2, new SemanticTokensParams(textDocId1));
1376+
1377+
// Verify modified tokens structure
1378+
var decoded2 = decode(tokens2.getData());
1379+
var expected2 = List.of(
1380+
new ExpectedToken(0, 0, 5, SemanticTokenTypes.Keyword, "Перем"),
1381+
new ExpectedToken(0, 6, 5, SemanticTokenTypes.Variable, SemanticTokenModifiers.Definition, "Новая"),
1382+
new ExpectedToken(0, 11, 1, SemanticTokenTypes.Operator, ","),
1383+
new ExpectedToken(0, 13, 1, SemanticTokenTypes.Variable, SemanticTokenModifiers.Definition, "А"),
1384+
new ExpectedToken(0, 14, 1, SemanticTokenTypes.Operator, ";")
1385+
);
1386+
assertTokensMatch(decoded2, expected2);
1387+
1388+
// when
1389+
var deltaParams = new SemanticTokensDeltaParams(textDocId1, tokens1.getResultId());
1390+
var result = provider.getSemanticTokensFullDelta(context2, deltaParams);
1391+
1392+
// then - should return delta, not full tokens
1393+
assertThat(result.isRight()).isTrue();
1394+
var delta = result.getRight();
1395+
assertThat(delta.getEdits()).isNotEmpty();
1396+
assertThat(delta.getEdits()).hasSize(1);
1397+
1398+
// Verify the delta edit details
1399+
// Original: [Перем, А, ;] - 3 tokens = 15 integers
1400+
// Modified: [Перем, Новая, ,, А, ;] - 5 tokens = 25 integers
1401+
//
1402+
// With lineOffset=0 inline edit handling:
1403+
// - Prefix match: "Перем" (1 token = 5 integers)
1404+
// - Suffix match: "А" and ";" (2 tokens = 10 integers)
1405+
// Note: "А" matches because the algorithm allows deltaStart to differ when lineOffset=0
1406+
// - Edit deletes: nothing (0 integers)
1407+
// - Edit inserts: "Новая" and "," (2 tokens = 10 integers)
1408+
var edit = delta.getEdits().get(0);
1409+
assertThat(edit.getStart())
1410+
.as("Edit should start after the prefix match (Перем = 5 integers)")
1411+
.isEqualTo(5);
1412+
assertThat(edit.getDeleteCount())
1413+
.as("Edit should delete nothing (suffix match includes А and ;)")
1414+
.isEqualTo(0);
1415+
assertThat(edit.getData())
1416+
.as("Edit should insert Новая and , tokens (2 tokens = 10 integers)")
1417+
.isNotNull()
1418+
.hasSize(10);
1419+
1420+
// Verify the edit is optimal (smaller than sending all new tokens)
1421+
int editSize = edit.getDeleteCount() + edit.getData().size();
1422+
assertThat(editSize).isLessThan(tokens2.getData().size());
1423+
}
1424+
13471425
// endregion
13481426

13491427
// region Range tokens tests

0 commit comments

Comments
 (0)