feat: символьный индекс для workspace/symbol (ранжированный поиск)#4105
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughIntroduces 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. ChangesWorkspace Symbol Indexing Refactor
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderScriptVariantTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java
| 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; | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
| 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(); | ||
| } |
There was a problem hiding this comment.
🧩 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().
Test Results 3 324 files + 12 3 324 suites +12 1h 32m 12s ⏱️ + 23m 35s 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.♻️ This comment has been updated with latest results. |
6d7d339 to
74ed5a6
Compare
There was a problem hiding this comment.
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 winThis cancellation test still fails on the first check, not the periodic in-loop check path.
Because
CancelCheckeralways throws, the assertion succeeds at the initialsearchcancellation 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 winGroup 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.copyOfchurn on large same-name groups. Group bylowerNameand 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 winUse 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
📒 Files selected for processing (6)
src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderScriptVariantTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.javasrc/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
| 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()); |
There was a problem hiding this comment.
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.
| 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.
Инкрементальный 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]>
…ons-collections4) 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]>
74ed5a6 to
d4dc90f
Compare
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.java (1)
84-93:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAvoid leaking mutable cached objects into
WorkspaceSymbol.
entry.range()andentry.tags()are passed by reference. Mutating the returnedWorkspaceSymbolcan 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
📒 Files selected for processing (7)
src/main/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProvider.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/Entry.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderScriptVariantTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/providers/SymbolProviderTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.javasrc/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]>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexTest.java (1)
286-302:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winThe cancellation test still does not exercise periodic in-loop checks.
The CancelChecker at lines 295-297 throws
CancellationExceptionimmediately on the firstcheckCanceled()call. This prevents validating thatindex.search()performs periodic cancellation checks during iteration (not just an initial check before work begins). The test currently only confirms thatCancellationExceptionpropagates, but does not verify that the search method callscheckCanceled()repeatedly while processing the 2000 symbols.The suggested fix from the previous review (allowing the first N checks to pass by using an
AtomicIntegercounter) 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
📒 Files selected for processing (3)
src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.javasrc/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]>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.java (1)
145-180: ⚡ Quick winValidate 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
countTriefor logging, but a mismatch betweencountFlatScanandcountTriewould 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
📒 Files selected for processing (5)
src/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/SubsequenceCharTrie.javasrc/main/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndex.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/SubsequenceCharTrieTest.javasrc/test/java/com/github/_1c_syntax/bsl/languageserver/types/index/WorkspaceSymbolIndexBenchmarkTest.javasrc/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
Вариант 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]>
a0ceb48 to
f4888ac
Compare
…са одиночного слова Меняет приоритет скоринга 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); |
There was a problem hiding this comment.
Fast set тоже имеет смысл отдавать партиал, если клиент это поддерживает. Чтобы не забивать канал связи. И проверять не отменили ли запрос.
There was a problem hiding this comment.
Готово. Быстрый (древесный) набор теперь тоже уходит частичными результатами пачками, а не одним чанком: вынес общий хелпер 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.
- 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]>
|



Проблема
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-start2> подстрока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-добор и пустой запрос идут по уникальным записям, поэтому раздувание дерева их стоимость не увеличивает.Что покрывает / не покрывает (честно)
Исследованный и отклонённый вариант 2 (subsequence-обход trie)
Пробовали заменить плоский скан для подпоследовательности на рекурсивный обход собственного посимвольного trie с отсечением по глубине. По замерам на двух реальных конфигурациях вариант 2 оказался медленнее плоского скана (×3…×75) и примерно удваивал память индекса, поэтому отклонён. Оставлен вариант 1:
PatriciaTrie+ word-start ключи для префикса, плоский скан для подстроки/подпоследовательности. Мёртвый код варианта 2 удалён из ветки.🤖 Generated with Claude Code