Skip to content

feat: символьный индекс для workspace/symbol (ранжированный поиск)#4105

Merged
nixel2007 merged 17 commits into
developfrom
claude/workspacesymbol-index
Jun 14, 2026
Merged

feat: символьный индекс для workspace/symbol (ранжированный поиск)#4105
nixel2007 merged 17 commits into
developfrom
claude/workspacesymbol-index

Conversation

@nixel2007

@nixel2007 nixel2007 commented Jun 13, 2026

Copy link
Copy Markdown
Member

Проблема

workspace/symbol обходил все документы всех контекстов на каждый запрос, фильтровал имена регулярным выражением и усекал выдачу слепым .limit() — без какого-либо ранжирования. На больших конфигурациях это дорого, а «слепой лимит» (подход из обсуждения #4095) отбрасывает символы произвольно, а не по релевантности.

Решение (вариант C из #4095/#4080)

Инкрементальный WorkspaceSymbolIndex (@Component @WorkspaceScope, наследует AbstractDocumentLifecycleClearableIndex) вытесняет обход + слепой лимит: fuzzy-поиск со скорингом поверх индекса.

Наполнение. На DocumentContextContentChangedEvent документ переиндексируется: поддерживаемые символы (Method/Constructor + модульные/глобальные переменные; локальные не индексируются) собираются в лёгкие неизменяемые записи Entry с уже вычисленным containerName (через mdObject + ScriptVariant) и тегами. AST/дерево символов в записях не удерживаются.

Структура — PatriciaTrie по ключу = lowercase-имя символа; несколько символов с одним именем — общий список под одним ключом. indexedByUri оставлен для точечной инвалидации.

Префиксный путь сублинеен через PatriciaTrie.prefixMap(lowerQuery).

Fuzzy-добор — проход по уникальным записям. Непрерывная подстрока (не префикс) и подпоследовательность (ПрвДокПровестиДокумент) дерево по префиксу не покрывает — для них идёт проход по уникальным записям indexedByUri.

Ранжирование. Скор (меньше — релевантнее): точное 0 > префикс полного имени 1 > word-start 2 > подстрока 3 > подпоследовательность 4 + позиция. Tie-break: короче имя, затем раньше позиция в документе.

Без лимита. search возвращает всю выдачу, отсортированную по скору. Пустой запрос отдаёт все символы. CancelChecker проверяется периодически.

Потокобезопасность. PatriciaTrie не потокобезопасен — доступ под ReentrantReadWriteLock (read на search, write на переиндексации/clear).

SymbolProvider.getSymbols делегирует в индекс и маппит записи в WorkspaceSymbol.

Индексация по началам CamelCase-слов (word-start ключи)

Запись кладётся под несколькими ключами: lowercase-суффиксы имени от начала каждого CamelCase-слова (ПровестиДокументпровестидокумент и документ). Слова режутся StringUtils.splitByCharacterTypeCamelCase. Так prefixMap покрывает префикс полного имени и префикс начала любого слова. clear(uri) удаляет запись из всех её ключей (множество пересчитывается из имени keysOf). Fuzzy-добор и пустой запрос идут по уникальным записям, поэтому раздувание дерева их стоимость не увеличивает.

Что покрывает / не покрывает (честно)

  • Покрывает префиксным деревом (сублинейно): префикс полного имени; префикс начала любого CamelCase-слова.
  • НЕ покрывает, остаётся проходом по уникальным записям: произвольная подстрока внутри слова; подпоследовательность вразброс. Этот проход обязателен на каждый непустой запрос.
  • Однобуквенный запрос упирается не в поиск, а в размер результата и его сортировку. Лимит выдачи — отложен.

Исследованный и отклонённый вариант 2 (subsequence-обход trie)

Пробовали заменить плоский скан для подпоследовательности на рекурсивный обход собственного посимвольного trie с отсечением по глубине. По замерам на двух реальных конфигурациях вариант 2 оказался медленнее плоского скана (×3…×75) и примерно удваивал память индекса, поэтому отклонён. Оставлен вариант 1: PatriciaTrie + word-start ключи для префикса, плоский скан для подстроки/подпоследовательности. Мёртвый код варианта 2 удалён из ветки.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces a concurrent, thread-safe WorkspaceSymbolIndex with Patricia trie storage, prefix and fuzzy matching with deterministic ranking, and cancellation support; refactors SymbolProvider to delegate workspace/symbol queries to the index; and updates provider tests, integration tests, and adds a disabled benchmark to exercise indexing, searching, and Entry-to-WorkspaceSymbol mapping behavior.

Changes

Workspace Symbol Indexing Refactor

Layer / File(s) Summary
Entry record and SubsequenceCharTrie data structures
src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/Entry.java, src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/SubsequenceCharTrie.java
Immutable Entry record captures indexed symbol metadata (URI, name, lowercased name, LSP kind/range/tags, container name). SubsequenceCharTrie implements insertion/deletion with depth-based pruning, and forEachSubsequenceMatch() for efficient scattered-character matching via DFS.
WorkspaceSymbolIndex core
src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java
Concurrent index with Patricia trie + per-URI entry snapshots, read/write locking, lifecycle handlers (document content changes trigger reindexing; batch clearing is a no-op), symbol-tree collection into immutable Entry snapshots with containerName derivation, prefix matching via trie traversal, fuzzy matching (substring and subsequence-first-index scoring), deterministic Scored ranking (score, name length, start position), and ScanProgress for periodic cancellation checks.
SymbolProvider delegation to index
src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java
Injects WorkspaceSymbolIndex, extracts query from WorkspaceSymbolParams, delegates search/cancellation to index.search(...), and maps Entry objects to WorkspaceSymbol using index-provided uri, range, name, kind, tags, and containerName.
SubsequenceCharTrie unit tests
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/SubsequenceCharTrieTest.java
Validates trie subsequence traversal against flat reference implementation, pruning correctness with long queries, removal and recomputation, and full clear operation.
SymbolProvider unit tests
src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderTest.java
Mocks WorkspaceSymbolIndex instead of symbol trees; validates empty-query behavior returning all symbols, literal no-match semantics for special-character queries without exceptions, and correct mapping of index Entry objects with special characters to WorkspaceSymbol output.
SymbolProviderScriptVariantTest refactored
src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderScriptVariantTest.java
Updated to populate index via DocumentContextContentChangedEvent, construct SymbolProvider from index, and use reusable NO_CANCEL CancelChecker; replaced ServerContext setup with direct index wiring.
WorkspaceSymbolIndex integration tests
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java
End-to-end tests verifying exact/prefix/subsequence/substring matching modes, deterministic ranking (exact > prefix > fuzzy), trie-based prefix search exclusion, CamelCase word-start boundary detection and ranking, empty-query enumeration, document removal cleanup across all indexing keys, and cancellation propagation.
Disabled performance benchmark
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java
Disabled benchmark test that loads a workspace from a configured path, indexes symbols, runs warmup iterations, and measures/logs average and median search latency across a fixed query set, plus a separate subsequence enumeration micro-benchmark comparing flat-scan vs trie-based matching.

Sequence Diagram(s)

sequenceDiagram
  participant DocumentChange
  participant SymbolProvider
  participant WorkspaceSymbolIndex
  participant PatriciaTrie
  DocumentChange->>WorkspaceSymbolIndex: handleContentChanged(event)
  WorkspaceSymbolIndex->>PatriciaTrie: write lock and insert entry keys
  SymbolProvider->>WorkspaceSymbolIndex: search(query, cancelChecker)
  WorkspaceSymbolIndex->>PatriciaTrie: read lock and prefixMap(lowerQuery)
  WorkspaceSymbolIndex->>WorkspaceSymbolIndex: score and deduplicate matches
  WorkspaceSymbolIndex->>WorkspaceSymbolIndex: check cancellation periodically
  WorkspaceSymbolIndex-->>SymbolProvider: return sorted entries
  SymbolProvider-->>SymbolProvider: map entries to WorkspaceSymbol
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • 1c-syntax/bsl-language-server#4094: Both PRs modify SymbolProvider's getSymbols(...) to accept and act on a CancelChecker for workspace/symbol cancellation (main PR routes to WorkspaceSymbolIndex.search(..., cancelChecker), while the retrieved PR adds cancel checks during the old traversal).
  • 1c-syntax/bsl-language-server#4063: The main PR refactors SymbolProvider to delegate workspace/symbol searching to WorkspaceSymbolIndex and removes the legacy regex compilation/invalid-pattern fallback logic, which directly conflicts with the retrieved PR's change to add literal fallback via compilePattern in SymbolProvider.
  • 1c-syntax/bsl-language-server#3945: Both PRs are related: the main PR's new WorkspaceSymbolIndex/SymbolProvider workspace-symbol search indexes and maps SymbolKind.Constructor entries produced by the symbol-tree layer, which is exactly what the retrieved PR adds via the new ConstructorSymbol and SymbolTree/visitor integration.

Suggested reviewers

  • sfaqer

Poem

🐰 A trie of words in whispered rows,
Each symbol sorted as the query grows.
Fuzz and prefix dance to score,
Swift searching sends them by the door! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introduction of a workspace symbol index with ranked search functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/workspacesymbol-index

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java`:
- Around line 162-168: The loop over removed entries rewrites the same
charBuckets bucket once per entry causing O(n²) churn; instead group removed
entries by their initial key and perform a single computeIfPresent per bucket:
build a Map<Character, Set<SymbolType>> (or Set of lower-names) from
removed/removedSet keyed by entry.lowerName().charAt(0), then for each key call
charBuckets.computeIfPresent(key, (ignored, bucket) -> { var filtered =
withoutEntries(bucket, groupedRemovedSetForKey); return filtered.isEmpty() ?
null : filtered; }); apply the same grouping approach to the merge code that
currently does per-entry concat so each bucket is merged only once.
- Around line 207-223: The iteration over charBuckets in WorkspaceSymbolIndex is
non-deterministic because charBuckets is a ConcurrentHashMap and
Scored.compareTo can return 0 for different entries, so sort/normalize both
places: replace the raw for (var bucket : charBuckets.values()) loop with
iteration over a stable ordering (e.g., get a sorted list of
charBuckets.keySet() and iterate keys to retrieve buckets) so
traversal/insertion order is deterministic; and update Scored.compareTo (class
Scored) to add deterministic tie-breakers after existing comparisons (for
example compare a stable identifier from Scored.entry such as
entry.getFileUri(), then entry.range().start.line, then
entry.range().start.character, and finally a fallback like
Integer.compare(System.identityHashCode(entry), 0) only if needed) so compareTo
never returns 0 for distinct entries and limit-based truncation is stable.
Ensure any added entry accessors used in tie-breakers exist or are derived from
the Entry object referenced by Scored.entry().

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java`:
- Around line 175-190: The test currently fails by throwing
CancellationException on the very first check, so change the CancelChecker in
cancelledCheckerInterruptsSearchEventually to allow the initial check to pass
and only throw during the periodic in-loop checks exercised by index.search;
implement the CancelChecker as a lambda or anonymous class that uses an
AtomicInteger (or boolean flag) to count invocations and only throws new
CancellationException() when the invocation count exceeds a small threshold
(e.g., >0 or >5) so the first check succeeds and a later periodic check
interrupts the search in index.search.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 84767738-a25b-4777-8fc6-f0d5e434439c

📥 Commits

Reviewing files that changed from the base of the PR and between 2d17edb and 1cf0b65.

📒 Files selected for processing (5)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderScriptVariantTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java

Comment on lines +162 to +168
for (var entry : removed) {
var key = entry.lowerName().charAt(0);
charBuckets.computeIfPresent(key, (ignored, bucket) -> {
var filtered = withoutEntries(bucket, removedSet);
return filtered.isEmpty() ? null : filtered;
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid per-entry bucket rewrites during reindex/clear (quadratic churn).

Line 277 merges one symbol at a time, and each merge copies the whole immutable bucket (concat). Line 162 then rescans/rebuilds the same bucket once per removed entry. For large files with many same-initial symbols, this can degrade reindex/clear toward O(n²) copying.

⚙️ Suggested change
@@
-    for (var entry : removed) {
-      var key = entry.lowerName().charAt(0);
-      charBuckets.computeIfPresent(key, (ignored, bucket) -> {
-        var filtered = withoutEntries(bucket, removedSet);
-        return filtered.isEmpty() ? null : filtered;
-      });
-    }
+    var affectedKeys = removed.stream()
+      .map(entry -> entry.lowerName().charAt(0))
+      .collect(java.util.stream.Collectors.toSet());
+    for (var key : affectedKeys) {
+      charBuckets.computeIfPresent(key, (ignored, bucket) -> {
+        var filtered = withoutEntries(bucket, removedSet);
+        return filtered.isEmpty() ? null : filtered;
+      });
+    }
@@
-    for (var entry : collected) {
-      var key = entry.lowerName().charAt(0);
-      charBuckets.merge(key, List.of(entry), WorkspaceSymbolIndex::concat);
-    }
+    var groupedByKey = collected.stream()
+      .collect(java.util.stream.Collectors.groupingBy(
+        entry -> entry.lowerName().charAt(0)
+      ));
+    groupedByKey.forEach((key, entries) ->
+      charBuckets.merge(key, List.copyOf(entries), WorkspaceSymbolIndex::concat)
+    );

Also applies to: 277-280

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java`
around lines 162 - 168, The loop over removed entries rewrites the same
charBuckets bucket once per entry causing O(n²) churn; instead group removed
entries by their initial key and perform a single computeIfPresent per bucket:
build a Map<Character, Set<SymbolType>> (or Set of lower-names) from
removed/removedSet keyed by entry.lowerName().charAt(0), then for each key call
charBuckets.computeIfPresent(key, (ignored, bucket) -> { var filtered =
withoutEntries(bucket, groupedRemovedSetForKey); return filtered.isEmpty() ?
null : filtered; }); apply the same grouping approach to the merge code that
currently does per-entry concat so each bucket is merged only once.

Comment on lines +207 to +223
for (var bucket : charBuckets.values()) {
for (var entry : bucket) {
if ((++seen & (CANCEL_CHECK_INTERVAL - 1)) == 0) {
cancelChecker.checkCanceled();
}
var score = score(entry.lowerName(), lowerQuery);
if (score < 0) {
continue;
}
offer(heap, new Scored(score, entry), effectiveLimit);
}
}

var ordered = new ArrayList<>(heap);
ordered.sort(Comparator.naturalOrder());
return ordered.stream().map(Scored::entry).toList();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java"

echo "Inspect traversal and comparator determinism points:"
nl -ba "$FILE" | sed -n '203,247p;417,435p'

Repository: 1c-syntax/bsl-language-server

Length of output: 187


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java"

echo "Traversal block (lines 203-247):"
awk 'NR>=203 && NR<=247 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "Scored.compareTo block (lines 400-450):"
awk 'NR>=400 && NR<=450 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: 1c-syntax/bsl-language-server

Length of output: 3144


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java"

echo "Find charBuckets declaration and types/usages:"
rg -n "charBuckets" "$FILE"

echo
echo "Show surrounding context near the declaration (first match):"
line=$(rg -n "charBuckets" "$FILE" | head -n1 | cut -d: -f1)
start=$((line-25)); end=$((line+25))
awk -v s=$start -v e=$end 'NR>=s && NR<=e {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "Inspect offer()/heap-related code around where `offer(heap,` is defined (if present):"
rg -n "offer\\(heap" "$FILE" || true
rg -n "void offer|offer\\(" "$FILE"

Repository: 1c-syntax/bsl-language-server

Length of output: 3548


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java"

echo "Find Entry declaration/type:"
rg -n "class Entry|record Entry|interface Entry|Entry\\s*\\{" "$FILE"

echo
echo "Find methods used on Entry (uri/name/range) for confidence:"
rg -n "entry\\.uri\\(|entry\\.name\\(|entry\\.range\\(" "$FILE"

echo
echo "Inspect surrounding context for Entry definition (if record/class):"
line=$(rg -n "class Entry|record Entry|interface Entry" "$FILE" | head -n1 | cut -d: -f1)
start=$((line-20)); end=$((line+60))
awk -v s=$start -v e=$end 'NR>=s && NR<=e {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: 1c-syntax/bsl-language-server

Length of output: 2807


Make empty-query and tie ordering deterministic in WorkspaceSymbolIndex

charBuckets is a ConcurrentHashMap, so iterating charBuckets.values() (lines ~207-218 and ~235-245) can yield different traversal/insertion order across runs. Also Scored.compareTo (lines ~417-435) has no tie-breakers beyond score/name-length/start line/character, so it can return 0 for distinct Entrys, allowing subset/order drift when limit truncates.

🎯 Suggested change
@@
-    for (var bucket : charBuckets.values()) {
+    for (var key : charBuckets.keySet().stream().sorted().toList()) {
+      var bucket = charBuckets.get(key);
+      if (bucket == null) {
+        continue;
+      }
       for (var entry : bucket) {
@@
-    for (var bucket : charBuckets.values()) {
+    for (var key : charBuckets.keySet().stream().sorted().toList()) {
+      var bucket = charBuckets.get(key);
+      if (bucket == null) {
+        continue;
+      }
       for (var entry : bucket) {
@@
-      return Integer.compare(start.getCharacter(), otherStart.getCharacter());
+      var byCharacter = Integer.compare(start.getCharacter(), otherStart.getCharacter());
+      if (byCharacter != 0) {
+        return byCharacter;
+      }
+      var byUri = entry.uri().toString().compareTo(other.entry.uri().toString());
+      if (byUri != 0) {
+        return byUri;
+      }
+      return entry.name().compareTo(other.entry.name());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java`
around lines 207 - 223, The iteration over charBuckets in WorkspaceSymbolIndex
is non-deterministic because charBuckets is a ConcurrentHashMap and
Scored.compareTo can return 0 for different entries, so sort/normalize both
places: replace the raw for (var bucket : charBuckets.values()) loop with
iteration over a stable ordering (e.g., get a sorted list of
charBuckets.keySet() and iterate keys to retrieve buckets) so
traversal/insertion order is deterministic; and update Scored.compareTo (class
Scored) to add deterministic tie-breakers after existing comparisons (for
example compare a stable identifier from Scored.entry such as
entry.getFileUri(), then entry.range().start.line, then
entry.range().start.character, and finally a fallback like
Integer.compare(System.identityHashCode(entry), 0) only if needed) so compareTo
never returns 0 for distinct entries and limit-based truncation is stable.
Ensure any added entry accessors used in tie-breakers exist or are derived from
the Entry object referenced by Scored.entry().

@github-actions

github-actions Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Test Results

 3 324 files  + 12   3 324 suites  +12   1h 32m 12s ⏱️ + 23m 35s
 3 270 tests + 18   3 254 ✅ + 17  16 💤 +1  0 ❌ ±0 
19 620 runs  +108  19 524 ✅ +102  96 💤 +6  0 ❌ ±0 

Results for commit f4888ac. ± Comparison against base commit 85295f5.

This pull request removes 3 and adds 21 tests. Note that renamed tests count towards both.
com.github._1c_syntax.bsl.languageserver.providers.SymbolProviderTest ‑ getSymbolsQueryStringAllSymbols()
com.github._1c_syntax.bsl.languageserver.providers.SymbolProviderTest ‑ getSymbolsQueryStringErrorRegex()
com.github._1c_syntax.bsl.languageserver.providers.SymbolProviderTest ‑ getSymbolsQueryWithRegexSpecialCharsFallsBackToLiteralMatch()
com.github._1c_syntax.bsl.languageserver.providers.SymbolProviderTest ‑ getSymbolsEmptyQueryReturnsAllSymbols()
com.github._1c_syntax.bsl.languageserver.providers.SymbolProviderTest ‑ getSymbolsMapsIndexEntryWithSpecialChars()
com.github._1c_syntax.bsl.languageserver.providers.SymbolProviderTest ‑ getSymbolsQueryStringNoMatch()
com.github._1c_syntax.bsl.languageserver.types.index.WorkspaceSymbolIndexBenchmarkTest ‑ benchmarkSearch()
com.github._1c_syntax.bsl.languageserver.types.index.WorkspaceSymbolIndexTest ‑ cancelledCheckerInterruptsSearchEventually()
com.github._1c_syntax.bsl.languageserver.types.index.WorkspaceSymbolIndexTest ‑ clearOnLifecycleEventRemovesDocumentSymbols()
com.github._1c_syntax.bsl.languageserver.types.index.WorkspaceSymbolIndexTest ‑ clearRemovesSymbolFromAllWordStartKeys()
com.github._1c_syntax.bsl.languageserver.types.index.WorkspaceSymbolIndexTest ‑ doesNotFindArbitrarySubsequenceMatch()
com.github._1c_syntax.bsl.languageserver.types.index.WorkspaceSymbolIndexTest ‑ doesNotFindMiddleOfWordSubstring()
com.github._1c_syntax.bsl.languageserver.types.index.WorkspaceSymbolIndexTest ‑ emptyQueryReturnsAllIndexedSymbols()
…

♻️ This comment has been updated with latest results.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java (1)

213-219: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

This cancellation test still fails on the first check, not the periodic in-loop check path.

Because CancelChecker always throws, the assertion succeeds at the initial search cancellation check and does not validate loop-time cancellation behavior.

✅ Proposed fix
@@
-    CancelChecker cancelChecker = () -> {
-      throw new CancellationException();
-    };
+    var checks = new java.util.concurrent.atomic.AtomicInteger();
+    CancelChecker cancelChecker = () -> {
+      if (checks.incrementAndGet() > 1) {
+        throw new CancellationException();
+      }
+    };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java`
around lines 213 - 219, The test currently supplies a CancelChecker that always
throws, so the initial pre-loop cancellation check in index.search triggers and
the loop-path cancellation behavior isn't exercised; change the CancelChecker
used in WorkspaceSymbolIndexTest so its first invocation does not throw but a
subsequent invocation does (e.g., track invocations with an AtomicBoolean or
counter and throw a CancellationException only after the first call), then keep
the existing assertThatThrownBy(() -> index.search("Символ",
cancelChecker)).isInstanceOf(CancellationException.class) to verify the in-loop
cancellation path.
src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java (1)

172-174: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Group updates by key to avoid repeated list rewrites during clear/reindex.

This is still doing per-entry remove/merge against the same key, which causes repeated List.copyOf churn on large same-name groups. Group by lowerName and update each key once.

⚙️ Proposed fix
@@
-      for (var entry : removed) {
-        removeEntryFromKey(entry.lowerName(), removedSet);
-      }
+      var removedByKey = new java.util.HashMap<String, Set<Entry>>();
+      for (var entry : removed) {
+        removedByKey
+          .computeIfAbsent(entry.lowerName(), k -> Collections.newSetFromMap(new IdentityHashMap<>()))
+          .add(entry);
+      }
+      for (var e : removedByKey.entrySet()) {
+        removeEntryFromKey(e.getKey(), e.getValue());
+      }
@@
-      for (var entry : collected) {
-        trie.merge(entry.lowerName(), List.of(entry), WorkspaceSymbolIndex::concat);
-      }
+      var grouped = new java.util.HashMap<String, List<Entry>>();
+      for (var entry : collected) {
+        grouped.merge(entry.lowerName(), List.of(entry), WorkspaceSymbolIndex::concat);
+      }
+      grouped.forEach((key, entries) -> trie.merge(key, entries, WorkspaceSymbolIndex::concat));

Also applies to: 305-307

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java`
around lines 172 - 174, The loop that calls removeEntryFromKey for each entry
causes repeated list rewrites; instead, group the entries in removed by their
lowerName and call removeEntryFromKey once per key with the grouped set (i.e.,
build a Map<String, Set<Entry>> or Map<String, Set<...>> keyed by
entry.lowerName(), then iterate that map and call removeEntryFromKey(key,
groupedSet)); apply the same refactor to the other occurrence in
WorkspaceSymbolIndex where entries are iterated and removeEntryFromKey is called
so each key is updated only once.
🧹 Nitpick comments (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java (1)

55-83: ⚡ Quick win

Use a configurable benchmark workspace path instead of a machine-specific absolute path.

The hard-coded local path prevents anyone else from running this benchmark without editing source.

♻️ Proposed refactor
@@
 import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Test;
@@
-  private static final String PATH_TO_CONFIGURATION = "/home/nfedkin/git_tree/github/1c-syntax/ssl_3_2";
+  private static final String BENCH_PATH_PROPERTY = "workspaceSymbolIndex.bench.path";
@@
-    var workspaceUri = Absolute.uri(Path.of(PATH_TO_CONFIGURATION).toUri());
+    var pathToConfiguration = System.getProperty(BENCH_PATH_PROPERTY);
+    Assumptions.assumeTrue(pathToConfiguration != null && !pathToConfiguration.isBlank(),
+      () -> "Set -D" + BENCH_PATH_PROPERTY + "=/path/to/workspace before running benchmark");
+    var workspaceUri = Absolute.uri(Path.of(pathToConfiguration).toUri());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java`
around lines 55 - 83, Replace the machine-specific PATH_TO_CONFIGURATION
constant with a configurable lookup (e.g., read
System.getProperty("benchmark.workspace.path") or an environment variable with a
sensible project-relative default) and update benchmarkSearch to use that value
when building workspaceUri for serverContextProvider.addWorkspace; specifically,
remove the hard-coded "/home/..." string in the PATH_TO_CONFIGURATION
definition, implement a method or inline logic to resolve the path from
System.getProperty/System.getenv (fallback to a relative path inside the repo or
test-resources), and ensure benchmarkSearch uses the new configurable
constant/reader so the benchmark can run on other machines without source edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java`:
- Around line 83-90: createWorkspaceSymbol is exposing mutable objects from
WorkspaceSymbolIndex.Entry (entry.range() and entry.tags()) which can leak
mutations into the shared cache; to fix, build and use defensive copies: create
a new Range instance copied from entry.range() (copy its start/end Positions)
and pass that Range to the Location used in workspaceSymbol.setLocation, and
copy the tags with a new List (e.g. new ArrayList<>(entry.tags())) before
calling workspaceSymbol.setTags; also null-check entry.tags() (and
entry.range()) and handle accordingly so you never store a direct reference to
the entry's mutable objects.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java`:
- Around line 163-177: The removal and insertion of entries in indexedByUri are
happening outside the write lock while trie mutations are protected, causing
race conditions; modify the WorkspaceSymbolIndex methods that call
indexedByUri.remove (clear/removed path) and indexedByUri.put (index path) so
that both the map update and the subsequent trie mutations (calls to
removeEntryFromKey and trie modifications) occur inside the same
lock.writeLock() block: move the indexedByUri.remove and indexedByUri.put calls
to be performed after acquiring lock.writeLock(), wrap them in try { ... }
finally { lock.writeLock().unlock(); }, and keep any temporary sets/local copies
(e.g., removedSet) as before so trie operations still iterate over stable data
while holding the lock. Ensure no map/trie mutation happens outside the write
lock in both the clear/removeEntryFromKey and index paths.

---

Duplicate comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java`:
- Around line 172-174: The loop that calls removeEntryFromKey for each entry
causes repeated list rewrites; instead, group the entries in removed by their
lowerName and call removeEntryFromKey once per key with the grouped set (i.e.,
build a Map<String, Set<Entry>> or Map<String, Set<...>> keyed by
entry.lowerName(), then iterate that map and call removeEntryFromKey(key,
groupedSet)); apply the same refactor to the other occurrence in
WorkspaceSymbolIndex where entries are iterated and removeEntryFromKey is called
so each key is updated only once.

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java`:
- Around line 213-219: The test currently supplies a CancelChecker that always
throws, so the initial pre-loop cancellation check in index.search triggers and
the loop-path cancellation behavior isn't exercised; change the CancelChecker
used in WorkspaceSymbolIndexTest so its first invocation does not throw but a
subsequent invocation does (e.g., track invocations with an AtomicBoolean or
counter and throw a CancellationException only after the first call), then keep
the existing assertThatThrownBy(() -> index.search("Символ",
cancelChecker)).isInstanceOf(CancellationException.class) to verify the in-loop
cancellation path.

---

Nitpick comments:
In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java`:
- Around line 55-83: Replace the machine-specific PATH_TO_CONFIGURATION constant
with a configurable lookup (e.g., read
System.getProperty("benchmark.workspace.path") or an environment variable with a
sensible project-relative default) and update benchmarkSearch to use that value
when building workspaceUri for serverContextProvider.addWorkspace; specifically,
remove the hard-coded "/home/..." string in the PATH_TO_CONFIGURATION
definition, implement a method or inline logic to resolve the path from
System.getProperty/System.getenv (fallback to a relative path inside the repo or
test-resources), and ensure benchmarkSearch uses the new configurable
constant/reader so the benchmark can run on other machines without source edits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ab0fb654-f5f6-47f0-bbac-66c677495ad8

📥 Commits

Reviewing files that changed from the base of the PR and between 1cf0b65 and 74ed5a6.

📒 Files selected for processing (6)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderScriptVariantTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderScriptVariantTest.java

Comment on lines +83 to +90
private static WorkspaceSymbol createWorkspaceSymbol(WorkspaceSymbolIndex.Entry entry) {
var location = new Location(entry.uri().toString(), entry.range());

var workspaceSymbol = new WorkspaceSymbol();
workspaceSymbol.setName(symbol.getName());
workspaceSymbol.setKind(symbol.getSymbolKind());
workspaceSymbol.setName(entry.name());
workspaceSymbol.setKind(entry.kind());
workspaceSymbol.setLocation(Either.forLeft(location));
workspaceSymbol.setTags(symbol.getTags());
getContainerName(symbol, symbolEntry.scriptVariant()).ifPresent(workspaceSymbol::setContainerName);
workspaceSymbol.setTags(entry.tags());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Defensively copy Range and tags before exposing them.

WorkspaceSymbolIndex.Entry is now part of the shared in-memory cache, but Range and List<SymbolTag> are mutable. Reusing entry.range() and entry.tags() here lets mutations on the returned WorkspaceSymbol leak back into the index, which breaks the new immutability/thread-safety contract and can corrupt later search results.

Proposed fix
 import org.eclipse.lsp4j.Location;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.Range;
 import org.eclipse.lsp4j.WorkspaceSymbol;
 import org.eclipse.lsp4j.WorkspaceSymbolParams;
@@
   private static WorkspaceSymbol createWorkspaceSymbol(WorkspaceSymbolIndex.Entry entry) {
-    var location = new Location(entry.uri().toString(), entry.range());
+    var location = new Location(entry.uri().toString(), copyRange(entry.range()));
 
     var workspaceSymbol = new WorkspaceSymbol();
     workspaceSymbol.setName(entry.name());
     workspaceSymbol.setKind(entry.kind());
     workspaceSymbol.setLocation(Either.forLeft(location));
-    workspaceSymbol.setTags(entry.tags());
+    workspaceSymbol.setTags(List.copyOf(entry.tags()));
     if (!entry.containerName().isEmpty()) {
       workspaceSymbol.setContainerName(entry.containerName());
     }
 
     return workspaceSymbol;
   }
+
+  private static Range copyRange(Range range) {
+    return new Range(
+      new Position(range.getStart().getLine(), range.getStart().getCharacter()),
+      new Position(range.getEnd().getLine(), range.getEnd().getCharacter())
+    );
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static WorkspaceSymbol createWorkspaceSymbol(WorkspaceSymbolIndex.Entry entry) {
var location = new Location(entry.uri().toString(), entry.range());
var workspaceSymbol = new WorkspaceSymbol();
workspaceSymbol.setName(symbol.getName());
workspaceSymbol.setKind(symbol.getSymbolKind());
workspaceSymbol.setName(entry.name());
workspaceSymbol.setKind(entry.kind());
workspaceSymbol.setLocation(Either.forLeft(location));
workspaceSymbol.setTags(symbol.getTags());
getContainerName(symbol, symbolEntry.scriptVariant()).ifPresent(workspaceSymbol::setContainerName);
workspaceSymbol.setTags(entry.tags());
private static WorkspaceSymbol createWorkspaceSymbol(WorkspaceSymbolIndex.Entry entry) {
var location = new Location(entry.uri().toString(), copyRange(entry.range()));
var workspaceSymbol = new WorkspaceSymbol();
workspaceSymbol.setName(entry.name());
workspaceSymbol.setKind(entry.kind());
workspaceSymbol.setLocation(Either.forLeft(location));
workspaceSymbol.setTags(List.copyOf(entry.tags()));
if (!entry.containerName().isEmpty()) {
workspaceSymbol.setContainerName(entry.containerName());
}
return workspaceSymbol;
}
private static Range copyRange(Range range) {
return new Range(
new Position(range.getStart().getLine(), range.getStart().getCharacter()),
new Position(range.getEnd().getLine(), range.getEnd().getCharacter())
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java`
around lines 83 - 90, createWorkspaceSymbol is exposing mutable objects from
WorkspaceSymbolIndex.Entry (entry.range() and entry.tags()) which can leak
mutations into the shared cache; to fix, build and use defensive copies: create
a new Range instance copied from entry.range() (copy its start/end Positions)
and pass that Range to the Location used in workspaceSymbol.setLocation, and
copy the tags with a new List (e.g. new ArrayList<>(entry.tags())) before
calling workspaceSymbol.setTags; also null-check entry.tags() (and
entry.range()) and handle accordingly so you never store a direct reference to
the entry's mutable objects.

nixel2007 and others added 7 commits June 13, 2026 23:44
Инкрементальный per-URI индекс символов рабочей области для workspace/symbol.
На DocumentContextContentChangedEvent собирает поддерживаемые символы (методы,
конструкторы, модульные/глобальные переменные) в неизменяемые записи с уже
вычисленным containerName. Записи разложены по корзинам по первой букве
lowercase-имени, что избавляет от полного линейного скана на непустой запрос.

search() ранжирует совпадения (точное > префикс > подстрока > подпоследовательность,
tie-break: короче имя / раньше позиция) и усекает выдачу top-N через bounded
priority queue размером limit (память O(limit), потолок MAX_RESULTS=1000).
CancelChecker проверяется периодически. ConcurrentHashMap + неизменяемые снимки
корзин — безопасное чтение из нескольких потоков без долгих блокировок.

Co-Authored-By: Claude Fable 5 <[email protected]>
При массовом populateContext каждый документ после индексации немедленно
освобождает вторичные данные (ServerContextDocumentClearedEvent), и базовый
обработчик чистил бы индекс — глобальный workspace/symbol ничего бы не находил
до следующего инкрементального изменения. Записи индекса автономны (имя, kind,
range, теги, containerName, без ссылок на AST), поэтому handleDataCleared
переопределён в no-op. Безымянные символы не индексируются; search() проверяет
отмену сразу на входе.

Co-Authored-By: Claude Fable 5 <[email protected]>
getSymbols больше не обходит все документы и не усекает выдачу слепым limit:
поиск делегируется WorkspaceSymbolIndex.search(query, MAX_RESULTS, cancelChecker),
который возвращает ранжированный top-N из готовых записей, а провайдер лишь
маппит их в WorkspaceSymbol. ScriptVariant→containerName и фильтр поддерживаемых
символов переехали в индекс. Сопоставление стало буквальным fuzzy (подстрока/
подпоследовательность) вместо regex; тесты адаптированы под ранжированную выдачу,
а проверка ScriptVariant перенесена на индекс.

Co-Authored-By: Claude Fable 5 <[email protected]>
Поиск теперь возвращает все совпадения, отсортированные по релевантности,
без усечения top-N. Удалены параметр limit, константа MAX_RESULTS и
bounded priority queue; ранжирование по скору оставлено. SymbolProvider
зовёт search без лимита, тест лимита переделан в проверку «возвращаются
все совпадения, ранжированно».

Co-Authored-By: Claude Fable 5 <[email protected]>
WorkspaceSymbolIndexBenchmarkTest (@disabled) наполняет серверный контекст
конфигурацией ssl_3_2 и замеряет время search по набору запросов (пустой,
одна буква, короткий префикс, подпоследовательность, редкий префикс) с
прогревом и усреднением. Печатает число проиндексированных символов и
среднее/медиану по каждому запросу.

Co-Authored-By: Claude Fable 5 <[email protected]>
- S2583: убрать мёртвую проверку query == null (класс @NullMarked, единственный вызывающий уже подставляет "")
- S3776: разбить searchMatching на collectPrefixMatches/collectFuzzyMatches
- S135: свести добор fuzzy к одному continue (matchedKeys в расчёт скора)
- S881: инкремент счётчика прогресса вынесен в ScanProgress.advance()
- S109: веса скоринга вынесены в SCORE_EXACT/PREFIX/SUBSTRING/SUBSEQUENCE
- S1200: вынести record Entry в отдельный файл пакета types/index
- S5853: объединить assertThat(names) в одну цепочку AssertJ

Co-Authored-By: Claude Fable 5 <[email protected]>
@nixel2007
nixel2007 force-pushed the claude/workspacesymbol-index branch from 74ed5a6 to d4dc90f Compare June 13, 2026 21:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java (1)

84-93: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid leaking mutable cached objects into WorkspaceSymbol.

entry.range() and entry.tags() are passed by reference. Mutating the returned WorkspaceSymbol can feed back into shared indexed data and skew later results.

Proposed minimal fix
 import org.eclipse.lsp4j.Location;
+import org.eclipse.lsp4j.Position;
+import org.eclipse.lsp4j.Range;
@@
   private static WorkspaceSymbol createWorkspaceSymbol(Entry entry) {
-    var location = new Location(entry.uri().toString(), entry.range());
+    var location = new Location(entry.uri().toString(), copyRange(entry.range()));
@@
-    workspaceSymbol.setTags(entry.tags());
+    workspaceSymbol.setTags(List.copyOf(entry.tags()));
@@
     return workspaceSymbol;
   }
+
+  private static Range copyRange(Range range) {
+    return new Range(
+      new Position(range.getStart().getLine(), range.getStart().getCharacter()),
+      new Position(range.getEnd().getLine(), range.getEnd().getCharacter())
+    );
+  }
In Eclipse LSP4J, are `org.eclipse.lsp4j.Range` and `WorkspaceSymbol` field objects mutable, and is defensive copying recommended when mapping from shared cache objects?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java`
around lines 84 - 93, createWorkspaceSymbol is leaking mutable cached objects by
passing entry.range() and entry.tags() directly into the WorkspaceSymbol; to fix
it, defensively copy those objects before setting them: construct a new Range
(copying start/end positions from entry.range()) and use that new Range when
creating the Location for setLocation, and setTags with a new List copy (e.g.,
new ArrayList<>(entry.tags())) or an unmodifiable copy instead of entry.tags()
directly; keep the containerName check as-is and handle possible null tags/range
gracefully.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java`:
- Around line 84-93: createWorkspaceSymbol is leaking mutable cached objects by
passing entry.range() and entry.tags() directly into the WorkspaceSymbol; to fix
it, defensively copy those objects before setting them: construct a new Range
(copying start/end positions from entry.range()) and use that new Range when
creating the Location for setLocation, and setTags with a new List copy (e.g.,
new ArrayList<>(entry.tags())) or an unmodifiable copy instead of entry.tags()
directly; keep the containerName check as-is and handle possible null tags/range
gracefully.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a9eed58b-ce4e-4222-aa85-595e3811c5de

📥 Commits

Reviewing files that changed from the base of the PR and between 74ed5a6 and d4dc90f.

📒 Files selected for processing (7)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/Entry.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderScriptVariantTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java

…иксного fuzzy

Записи WorkspaceSymbolIndex кладутся в PatriciaTrie не только под полным
lowercase-именем, но и под суффиксами от начала каждого CamelCase-слова
(splitByCharacterTypeCamelCase, кириллица режется корректно): для
«ПровестиДокумент» ключи «провестидокумент» и «документ». Так prefixMap
ловит запрос «слово из середины» (например «Документ») через дерево как
word-start, а не только сканом-подпоследовательностью.

Скоринг: точное < префикс полного имени < префикс слова (word-start) <
подстрока < подпоследовательность. Поиск дедуплицирует запись по
идентичности с лучшим скором (одна запись лежит под несколькими ключами).
clear(uri) удаляет запись из ВСЕХ её ключей (пересчёт keysOf по имени).
fuzzy-добор и пустой запрос идут по уникальным записям indexedByUri, а не
по раздутому дереву; indexedByUri и trie держатся согласованными под одним
write-lock.

Ограничения (честно): word-start trie покрывает префикс полного имени и
префикс слова; произвольная подстрока внутри слова и подпоследовательность
вразброс по-прежнему добираются полным сканом (обязателен на каждый
запрос). По замерам на ssl_3_2 латентность поиска не падает (fuzzy-скан
доминирует), выигрыш — в качестве ранжирования mid-word запросов. Лимит
выдачи отложен.

Co-Authored-By: Claude Fable 5 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java (1)

286-302: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The cancellation test still does not exercise periodic in-loop checks.

The CancelChecker at lines 295-297 throws CancellationException immediately on the first checkCanceled() call. This prevents validating that index.search() performs periodic cancellation checks during iteration (not just an initial check before work begins). The test currently only confirms that CancellationException propagates, but does not verify that the search method calls checkCanceled() repeatedly while processing the 2000 symbols.

The suggested fix from the previous review (allowing the first N checks to pass by using an AtomicInteger counter) does not appear to be implemented in the current code.

✅ Suggested change to test periodic checks
+    var checks = new java.util.concurrent.atomic.AtomicInteger();
     CancelChecker cancelChecker = () -> {
-      throw new CancellationException();
+      if (checks.incrementAndGet() > 5) {
+        throw new CancellationException();
+      }
     };

As per coding guidelines, "Always run tests before submitting changes and maintain or improve test coverage using appropriate test frameworks (JUnit, AssertJ, Mockito)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java`
around lines 286 - 302, The CancelChecker lambda in the
cancelledCheckerInterruptsSearchEventually test method throws
CancellationException immediately, which only validates that the exception
propagates but does not verify that index.search() actually performs periodic
cancellation checks during iteration through the 2000 symbols. Replace the
immediate throw with an AtomicInteger counter that tracks calls to the cancel
checker. Allow the first several calls to pass (so the search begins processing)
and then throw CancellationException on a subsequent call to verify that the
search method truly calls checkCanceled() repeatedly during iteration, not just
once before starting work.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java`:
- Around line 286-302: The CancelChecker lambda in the
cancelledCheckerInterruptsSearchEventually test method throws
CancellationException immediately, which only validates that the exception
propagates but does not verify that index.search() actually performs periodic
cancellation checks during iteration through the 2000 symbols. Replace the
immediate throw with an AtomicInteger counter that tracks calls to the cancel
checker. Allow the first several calls to pass (so the search begins processing)
and then throw CancellationException on a subsequent call to verify that the
search method truly calls checkCanceled() repeatedly during iteration, not just
once before starting work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2f539c6b-968c-4d69-a733-9bdc586040b7

📥 Commits

Reviewing files that changed from the base of the PR and between d4dc90f and 59a9755.

📒 Files selected for processing (3)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java

…по замерам)

Реализован собственный посимвольный SubsequenceCharTrie над полными
lowercase-именами записей: рекурсивный обход по подпоследовательности с
отсечением по глубине поддерева и шарингом общих начал имён — как замена
плоскому скану для substring/subsequence-добора.

Замеры на двух реальных конфигурациях (изолированное сравнение
перечисления кандидатов, медиана, 200 итераций) показали, что обход
trie МЕДЛЕННЕЕ плоского скана:

  конфигурация A (~38k символов):
    "П"               скан 3.9 ms  / trie 18.7 ms
    "Пров"            скан 5.3 ms  / trie 16.9 ms
    "Документ"        скан 5.4 ms  / trie 13.7 ms
    "ПрвДок"          скан 5.5 ms  / trie 15.0 ms
    "ОбработкаПроведения" скан 5.4 ms / trie 7.8 ms

  конфигурация B (бОльшая, ~355k символов):
    "П"               скан 63.7 ms / trie 4774.6 ms

Причина структурна: для коротких/неселективных запросов отсечение по
глубине почти не срабатывает, а узлов посимвольного trie на порядок
больше числа уникальных записей; вдобавок char-trie примерно удваивает
память индекса (на конфигурации B обход упирался в heap). Плоский скан
уникальных записей — тесный цикл с ранним выходом — уже близок к
оптимуму для этого профиля.

Поэтому активным путём оставлен плоский скан; SubsequenceCharTrie с
юнит-тестом эквивалентности и сравнительным бенчмарком оставлены, чтобы
отрицательный результат был воспроизводим. Семантика и ранжирование
(точное/префикс/word-start/подстрока/подпоследовательность),
дедуп, потокобезопасность и отсутствие лимита сохранены.

Co-Authored-By: Claude Fable 5 <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java (1)

145-180: ⚡ Quick win

Validate that flat scan and trie produce the same result count.

The benchmark compares latency of two subsequence-matching implementations but does not verify they produce identical results. Line 160 uses countTrie for logging, but a mismatch between countFlatScan and countTrie would go undetected. Adding a simple assertion after warmup ensures both methods enumerate the same candidate set.

✅ Proposed validation
     for (var query : SUBSEQUENCE_QUERIES) {
       var lowerQuery = query.toLowerCase(Locale.ENGLISH);
 
       for (var i = 0; i < WARMUP_ITERATIONS; i++) {
         countFlatScan(entries, lowerQuery);
         countTrie(trie, lowerQuery);
       }
 
-      var matches = countTrie(trie, lowerQuery);
+      var flatMatches = countFlatScan(entries, lowerQuery);
+      var trieMatches = countTrie(trie, lowerQuery);
+      if (flatMatches != trieMatches) {
+        throw new AssertionError(String.format(
+          "Subsequence mismatch for query=\"%s\": flat=%d trie=%d",
+          query, flatMatches, trieMatches));
+      }
+      var matches = trieMatches;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java`
around lines 145 - 180, The benchmarkSubsequenceEnumeration method compares
performance between countFlatScan and countTrie but does not verify they produce
identical results. After the warmup loop completes (after the loop at line
151-154 that calls both countFlatScan and countTrie), add an assertion that
validates both methods return the same count for the current lowerQuery. Capture
the result from countFlatScan and compare it to the result from countTrie to
ensure the benchmark is comparing equivalent implementations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java`:
- Around line 145-180: The benchmarkSubsequenceEnumeration method compares
performance between countFlatScan and countTrie but does not verify they produce
identical results. After the warmup loop completes (after the loop at line
151-154 that calls both countFlatScan and countTrie), add an assertion that
validates both methods return the same count for the current lowerQuery. Capture
the result from countFlatScan and compare it to the result from countTrie to
ensure the benchmark is comparing equivalent implementations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d8d03150-0f67-47c5-9745-e47e246ddbf9

📥 Commits

Reviewing files that changed from the base of the PR and between 59a9755 and e07750b.

📒 Files selected for processing (5)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/SubsequenceCharTrie.java
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/SubsequenceCharTrieTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java

nixel2007 and others added 3 commits June 14, 2026 08:10
Вариант 2 (рекурсивный обход собственного посимвольного trie с отсечением)
по замерам оказался медленнее плоского скана, в активный путь не включён.
Удалён мёртвый код: класс SubsequenceCharTrie, его тест и сравнительная
часть бенчмарка. Активный путь — вариант 1: PatriciaTrie + word-start ключи
для префикса, плоский скан для подстроки/подпоследовательности.

Co-Authored-By: Claude Fable 5 <[email protected]>
Многословные (≥2 CamelCase-фрагмента) запросы вроде ПрДок обслуживаются
пересечением множеств записей по фрагментам через trie.prefixMap, без
полного fuzzy-скана: запись проходит, если у неё есть слово, начинающееся
с каждого фрагмента. Пересечение начинается с самого мелкого множества.

Однословные запросы сохраняют прежний путь (префикс + полный fuzzy-скан).

Ранжирование: фрагменты, назначаемые словам строго по возрастанию,
получают SCORE_MULTI_WORD_IN_ORDER, иначе SCORE_MULTI_WORD_UNORDERED
(совпадения не по порядку не отфильтровываются, лишь понижаются). Скоры
перенумерованы: EXACT(0) < PREFIX(1) < WORD_PREFIX(2) < MULTI_WORD_IN_ORDER(3)
< MULTI_WORD_UNORDERED(4) < SUBSTRING(5) < SUBSEQUENCE(6)+позиция.

Бенчмарк дополнен многословными запросами и assumeTrue на наличие корпусов.

Co-Authored-By: Claude Fable 5 <[email protected]>
Удалён последний линейный проход по всем записям индекса: методы
collectFuzzyMatches, fuzzyScore, subsequenceFirstIndex и константы
SCORE_SUBSTRING/SCORE_SUBSEQUENCE. Поиск теперь идёт исключительно по
PatriciaTrie: префикс полного имени и начала любого CamelCase-слова плюс
многословное пересечение по фрагментам. Полное совпадение имени —
максимальный ранг, совпадения по подсловам — ранг ниже.

Произвольная подстрока внутри слова и подпоследовательность вразброс
намеренно больше НЕ поддерживаются; тесты и javadoc приведены в
соответствие, добавлен кейс на отсутствие совпадения по середине слова.
Бенчмарк перемаркирован на чисто древесные пути.

Co-Authored-By: Claude Fable 5 <[email protected]>
@nixel2007
nixel2007 force-pushed the claude/workspacesymbol-index branch from a0ceb48 to f4888ac Compare June 14, 2026 07:28
nixel2007 and others added 2 commits June 14, 2026 10:50
…са одиночного слова

Меняет приоритет скоринга workspace/symbol: многословное in-order
camel-hump совпадение (SCORE_MULTI_WORD_IN_ORDER теперь 2) ранжируется
выше префикса начала одиночного слова из середины имени
(SCORE_WORD_PREFIX теперь 3). Новый порядок релевантности: точное имя →
префикс полного имени → многословное in-order → префикс одиночного слова
→ многословное не по порядку. Обновлены javadoc/комментарии и тесты.

Co-Authored-By: Claude Fable 5 <[email protected]>
… trie-only

Возвращён «грязный» линейный fuzzy-поиск (подстрока внутри слова и
разбросанная подпоследовательность), но не блокирующим search, а отдельным
методом WorkspaceSymbolIndex.searchFuzzyTail: он снимает снимок уникальных
записей под read-lock, отпускает блокировку и скорит их вне неё (записи
неизменяемы), исключая по идентичности уже отданные быстрым путём.

SymbolProvider при наличии partialResultToken шлёт быстрый древесный чанк
первым через $/progress, затем досылает fuzzy-хвост батчами по 200 и
возвращает пустой синхронный ответ (клиент конкатенирует прогресс-чанки;
ранг сохраняется порядком прибытия). Без токена/при пустом запросе/без
клиента поведение прежнее — только trie, без медленного скана.

Co-Authored-By: Claude Fable 5 <[email protected]>
var symbol = symbolEntry.symbol();
var location = new Location(uri.toString(), symbol.getRange());
Set<Entry> fastSet = Collections.newSetFromMap(new IdentityHashMap<>());
fastSet.addAll(fast);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fast set тоже имеет смысл отдавать партиал, если клиент это поддерживает. Чтобы не забивать канал связи. И проверять не отменили ли запрос.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Готово. Быстрый (древесный) набор теперь тоже уходит частичными результатами пачками, а не одним чанком: вынес общий хелпер streamInBatches(token, entries, cancelChecker) — режет на пачки по STREAM_BATCH_SIZE (200), cancelChecker.checkCanceled() вызывается перед каждой пачкой, и он применяется и к быстрому набору, и к fuzzy-хвосту (порядок сохранён: пачки быстрого набора уходят раньше хвоста). Канал не забиваем + отмена проверяется между пачками. (74e3e06)

Заодно: добавил глобальную LSC-настройку workspaceSymbol.fuzzySearch (off/synchronous, дефолт off) — она включает СИНХРОННЫЙ блокирующий fuzzy для клиентов без partial results; в partial-режиме fuzzy работает всегда и от настройки не зависит. Отражено в schema.json и ConfigurationFile.md.

nixel2007 and others added 3 commits June 14, 2026 11:37
- S5841: тест searchFuzzyTailExcludesEntriesFromExcludeSet больше не проходит
  вхолостую на возможном пустом списке — добавлен .isNotEmpty() и проверка
  оставшегося fuzzy-совпадения «ОбработкаНеокумента», которого нет в exclude.
- S109: магическое число 2 (порог многословного пути) вынесено в константу
  MIN_MULTI_WORD_FRAGMENTS.
- S5976: три почти одинаковых теста древесного поиска объединены в один
  @ParameterizedTest findsSymbolViaTrieQueryForms.
- S135/S3242: цикл searchFuzzyTail упрощён — отбор записи вынесен в helper
  fuzzyTailScore, убраны лишние continue; параметр exclude расширен до
  Collection<Entry> (используется только contains).

Co-Authored-By: Claude Fable 5 <[email protected]>
…нхронного fuzzy-поиска

Добавлена глобальная настройка workspaceSymbol.fuzzySearch (по умолчанию off)
по аналогии с capabilities.textDocumentSync.change: для клиентов без потоковой
выдачи частичных результатов значение synchronous включает блокирующий fuzzy-скан,
дописываемый в синхронный ответ. Потоковая выдача через partial results остаётся
автоматической и от настройки не зависит.

Также: быстрый набор стримится партиалами чанками (как и fuzzy-хвост) через общий
helper streamInBatches с проверкой отмены перед каждым чанком; исправлены битые
ссылки {@link ...searchFuzzyTail} (Set -> Collection) в SymbolProvider, ронявшие
build-javadoc.

Co-Authored-By: Claude Fable 5 <[email protected]>
…enum

Настройка управляет ТОЛЬКО синхронным режимом fuzzy-поиска (в потоковом
режиме fuzzy-хвост досылается всегда), поэтому моделируется булевым флагом
syncFuzzySearch (по умолчанию false), а не enum-видом off/synchronous.

Удалён мёртвый enum WorkspaceSymbolFuzzySearch; WorkspaceSymbolOptions
теперь хранит boolean; GlobalLanguageServerConfiguration копирует/сбрасывает
флаг; SymbolProvider читает isSyncFuzzySearch() на синхронном пути (потоковая
ветка не тронута). Обновлены schema.json, документация (RU/EN) и тесты.

Co-Authored-By: Claude Fable 5 <[email protected]>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant