|
30 | 30 | import jakarta.annotation.PostConstruct; |
31 | 31 | import jakarta.annotation.PreDestroy; |
32 | 32 | import lombok.RequiredArgsConstructor; |
| 33 | +import org.eclipse.lsp4j.Range; |
33 | 34 | import org.eclipse.lsp4j.SemanticTokens; |
34 | 35 | import org.eclipse.lsp4j.SemanticTokensDelta; |
35 | 36 | import org.eclipse.lsp4j.SemanticTokensDeltaParams; |
36 | 37 | import org.eclipse.lsp4j.SemanticTokensEdit; |
37 | 38 | import org.eclipse.lsp4j.SemanticTokensParams; |
| 39 | +import org.eclipse.lsp4j.SemanticTokensRangeParams; |
38 | 40 | import org.eclipse.lsp4j.jsonrpc.messages.Either; |
39 | 41 | import org.springframework.context.event.EventListener; |
40 | 42 | import org.springframework.stereotype.Component; |
|
57 | 59 | /** |
58 | 60 | * Провайдер для предоставления семантических токенов. |
59 | 61 | * <p> |
60 | | - * Обрабатывает запросы {@code textDocument/semanticTokens/full} и {@code textDocument/semanticTokens/full/delta}. |
| 62 | + * Обрабатывает запросы {@code textDocument/semanticTokens/full}, {@code textDocument/semanticTokens/full/delta} |
| 63 | + * и {@code textDocument/semanticTokens/range}. |
61 | 64 | * |
62 | 65 | * @see <a href="https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens">Semantic Tokens specification</a> |
63 | 66 | */ |
64 | 67 | @Component |
65 | 68 | @RequiredArgsConstructor |
66 | 69 | public class SemanticTokensProvider { |
67 | 70 |
|
| 71 | + /** |
| 72 | + * Порог количества токенов, при превышении которого используется параллельная обработка. |
| 73 | + */ |
| 74 | + private static final int PARALLEL_PROCESSING_THRESHOLD = 1000; |
| 75 | + |
68 | 76 | @SuppressWarnings("NullAway.Init") |
69 | 77 | private ExecutorService executorService; |
70 | 78 |
|
@@ -164,6 +172,93 @@ public Either<SemanticTokens, SemanticTokensDelta> getSemanticTokensFullDelta( |
164 | 172 | return Either.forRight(delta); |
165 | 173 | } |
166 | 174 |
|
| 175 | + /** |
| 176 | + * Получить семантические токены для указанного диапазона документа. |
| 177 | + * |
| 178 | + * @param documentContext Контекст документа |
| 179 | + * @param params Параметры запроса с диапазоном |
| 180 | + * @return Семантические токены для указанного диапазона в дельта-кодированном формате |
| 181 | + */ |
| 182 | + public SemanticTokens getSemanticTokensRange( |
| 183 | + DocumentContext documentContext, |
| 184 | + SemanticTokensRangeParams params |
| 185 | + ) { |
| 186 | + Range range = params.getRange(); |
| 187 | + |
| 188 | + // Collect tokens from all suppliers in parallel |
| 189 | + var entries = collectTokens(documentContext); |
| 190 | + |
| 191 | + // Filter tokens that fall within the specified range |
| 192 | + var filteredEntries = filterTokensByRange(entries, range); |
| 193 | + |
| 194 | + // Build delta-encoded data as int array |
| 195 | + int[] data = toDeltaEncodedArray(filteredEntries); |
| 196 | + |
| 197 | + // Range requests do not use resultId caching as per LSP specification |
| 198 | + return new SemanticTokens(toList(data)); |
| 199 | + } |
| 200 | + |
| 201 | + /** |
| 202 | + * Фильтрует токены, оставляя только те, которые попадают в указанный диапазон. |
| 203 | + * <p> |
| 204 | + * Токен считается попадающим в диапазон, если он хотя бы частично пересекается с ним. |
| 205 | + * <p> |
| 206 | + * Оптимизация: сначала выполняется быстрая фильтрация по строкам (простое сравнение целых чисел), |
| 207 | + * затем для граничных строк проверяются позиции символов. Использует параллельную обработку |
| 208 | + * для больших объемов данных. |
| 209 | + * |
| 210 | + * @param entries Список токенов |
| 211 | + * @param range Диапазон для фильтрации |
| 212 | + * @return Отфильтрованный список токенов |
| 213 | + */ |
| 214 | + private static List<SemanticTokenEntry> filterTokensByRange(List<SemanticTokenEntry> entries, Range range) { |
| 215 | + int startLine = range.getStart().getLine(); |
| 216 | + int startChar = range.getStart().getCharacter(); |
| 217 | + int endLine = range.getEnd().getLine(); |
| 218 | + int endChar = range.getEnd().getCharacter(); |
| 219 | + |
| 220 | + // Use parallel stream for large collections to leverage multiple cores |
| 221 | + var stream = entries.size() > PARALLEL_PROCESSING_THRESHOLD |
| 222 | + ? entries.parallelStream() |
| 223 | + : entries.stream(); |
| 224 | + |
| 225 | + return stream |
| 226 | + // Quick line-based pre-filter (simple integer comparison) |
| 227 | + .filter(token -> token.line() >= startLine && token.line() <= endLine) |
| 228 | + // Detailed check for boundary lines only |
| 229 | + .filter(token -> isTokenInRangeDetailed(token, startLine, startChar, endLine, endChar)) |
| 230 | + .toList(); |
| 231 | + } |
| 232 | + |
| 233 | + /** |
| 234 | + * Проверяет позицию символов для токенов на граничных строках. |
| 235 | + * <p> |
| 236 | + * Предполагается, что токен уже прошел проверку по строкам (находится между startLine и endLine). |
| 237 | + */ |
| 238 | + private static boolean isTokenInRangeDetailed( |
| 239 | + SemanticTokenEntry token, |
| 240 | + int startLine, |
| 241 | + int startChar, |
| 242 | + int endLine, |
| 243 | + int endChar |
| 244 | + ) { |
| 245 | + int tokenLine = token.line(); |
| 246 | + int tokenStart = token.start(); |
| 247 | + int tokenEnd = tokenStart + token.length(); |
| 248 | + |
| 249 | + // Token is on the start line - check if it ends after range start |
| 250 | + if (tokenLine == startLine && tokenEnd <= startChar) { |
| 251 | + return false; |
| 252 | + } |
| 253 | + |
| 254 | + // Token is on the end line - check if it starts before range end |
| 255 | + if (tokenLine == endLine && tokenStart >= endChar) { |
| 256 | + return false; |
| 257 | + } |
| 258 | + |
| 259 | + return true; |
| 260 | + } |
| 261 | + |
167 | 262 | /** |
168 | 263 | * Обрабатывает событие закрытия документа в контексте сервера. |
169 | 264 | * <p> |
|
0 commit comments