@@ -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}
0 commit comments