Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,11 @@ public static TextRange getWordRangeAt(@NotNull Document document,
}
}

if (offset < 0) {
// No word at a negative offset; also avoids charAt(-1) in getRightOffsetOfPart
// (offset can be -1 here for an empty document via the clamp above).
return null;
}
int start = getLeftOffsetOfPart(document, offset);
int end = getRightOffsetOfPart(document, offset);
return (start < end) ? new TextRange(start, end) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

import java.util.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer;
Expand All @@ -59,7 +60,11 @@ public class LanguageServiceAccessor implements Disposable {

private final Project project;
private final SimpleModificationTracker modificationTracker = new SimpleModificationTracker();
private final Set<LanguageServerWrapper> startedServers = new HashSet<>();
// Thread-safe set: readers (hasAny, processLanguageServers, getStartedServers) iterate it without
// locking, so a plain HashSet would throw ConcurrentModificationException when a writer mutates it
// concurrently. newKeySet() gives weakly-consistent iteration; writers still synchronize on it to
// keep their check-then-add/remove operations atomic.
private final Set<LanguageServerWrapper> startedServers = ConcurrentHashMap.newKeySet();
private final LanguageServerDefinitionListener serverDefinitionListener = new LanguageServerDefinitionListener() {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
package com.redhat.devtools.lsp4ij.features.semanticTokens.viewProvider;

import com.intellij.lang.Language;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiReference;
import com.redhat.devtools.lsp4ij.LSPIJUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand Down Expand Up @@ -57,45 +60,70 @@ public boolean supportsIncrementalReparse(@NotNull Language rootLanguage) {
@Nullable
private PsiElement getSemanticTokenElement(int offset) {
LSPSemanticToken semanticToken = isEnabled() ? getSemanticToken(offset) : null;
// Skip the file-level fallback stub returned when semantic tokens haven't loaded yet
// Only a concrete (non file-level) token carries the reference/declaration info needed by
// hover and go-to-declaration. The file-level stub spans the whole file, so it's handled by
// the narrow-element fallback below instead.
return (semanticToken != null && !semanticToken.isFileLevel()) ? semanticToken.getElement() : null;
}

/**
* Filters out whole-file elements returned by super.findElementAt() for structureless files.
* Returns the element to expose at {@code offset} when no concrete semantic token is available.
* <p>
* TextMate uses an {@code EmptyLexer} and a trivial parser, so its PSI tree consists of a single
* leaf element spanning the entire file. Returning that element as the "element at offset" would
* cause callers (e.g., {@code UsagePreviewPanel.getNameElementTextRange}) to treat the whole file
* as a single named element and highlight everything. Return {@code null} instead so callers fall
* back to their own range computation (e.g., the usage element's own text range).
* TextMate/plain-text files are structureless: {@code super.findElementAt()} returns a single leaf
* element spanning the ENTIRE file (empty lexer + trivial parser), and the file-level stub token
* does the same. Neither can be exposed verbatim:
* <ul>
* <li>Returning the whole-file element makes {@code UsagePreviewPanel#getNameElementTextRange}
* highlight the entire file in Find Usages.</li>
* <li>Returning {@code null} makes {@code EditorMouseHoverPopupManager#createContext} skip the
* documentation popup (so {@code textDocument/hover} is never sent) and leaves
* {@code GotoDeclarationHandler} without a source element (so go-to-declaration never runs).</li>
* </ul>
* So we synthesize a NON-null element with the word range at the offset (or {@code null} on
* whitespace/punctuation, where hover/navigation shouldn't trigger anyway).
*/
@Nullable
private PsiElement filterStructurelessSuperElement(@Nullable PsiElement superElement) {
if (superElement == null) return null;
private PsiElement narrowElementAt(int offset, @Nullable PsiElement superElement) {
PsiFile psiFile = getPsi(getBaseLanguage());
if (psiFile != null && superElement.getTextRange().equals(psiFile.getTextRange())) {
// A real (sub-file) super element is fine to return as-is.
if (superElement != null && (psiFile == null || !superElement.getTextRange().equals(psiFile.getTextRange()))) {
return superElement;
}
if (psiFile == null) {
return null;
}
Document document = LSPIJUtils.getDocument(psiFile.getVirtualFile());
if (document == null) {
return null;
}
TextRange wordRange = LSPIJUtils.getWordRangeAt(document, psiFile, offset);
if (wordRange == null) {
return null;
}
return superElement;
// Build a synthetic (unknown-type) semantic-token element scoped to the word at the offset.
// Using a real LSPSemanticTokenPsiElement (rather than a whole-file stub or a bare
// LSPPsiElement) keeps it consistent with the platform symbol model and LSP4IJ's own
// documentation provider, so the hover popup is built while Find Usages highlights only the
// word — its range always contains the offset, so DeclarationOrReference#rangeWithOffset holds.
return new LSPSemanticToken(psiFile, wordRange, null, null).getElement();
}

@Override
public PsiElement findElementAt(int offset) {
PsiElement element = getSemanticTokenElement(offset);
return element != null ? element : filterStructurelessSuperElement(super.findElementAt(offset));
return element != null ? element : narrowElementAt(offset, super.findElementAt(offset));
}

@Override
public PsiElement findElementAt(int offset, @NotNull Class<? extends Language> lang) {
PsiElement element = getSemanticTokenElement(offset);
return element != null ? element : filterStructurelessSuperElement(super.findElementAt(offset, lang));
return element != null ? element : narrowElementAt(offset, super.findElementAt(offset, lang));
}

@Override
public PsiElement findElementAt(int offset, @NotNull Language language) {
PsiElement element = getSemanticTokenElement(offset);
return element != null ? element : filterStructurelessSuperElement(super.findElementAt(offset, language));
return element != null ? element : narrowElementAt(offset, super.findElementAt(offset, language));
}

@Override
Expand Down
Loading