From 44c4c18a1debaa9f76646699f79acb45a50b2bf1 Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Sat, 7 Dec 2024 12:20:46 -0600 Subject: [PATCH 01/14] Issue 670 - LSP rename refactorings now collect external references to that identifier before performing the actual rename, and then on successful rename, any references are updated regarding the new name. --- .../rename/LSPRenameRefactoringDialog.java | 130 +++++++++++++++++- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java index 3fb370fd8..3ab37d1f6 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java @@ -13,25 +13,43 @@ *******************************************************************************/ package com.redhat.devtools.lsp4ij.features.rename; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileTypes.FileTypes; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.resolve.reference.PsiReferenceUtil; +import com.intellij.psi.search.PsiSearchHelper; +import com.intellij.psi.search.UsageSearchContext; import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.ui.NameSuggestionsField; import com.intellij.refactoring.ui.RefactoringDialog; +import com.intellij.util.containers.ContainerUtil; import com.redhat.devtools.lsp4ij.LSPFileSupport; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LanguageServerBundle; +import com.redhat.devtools.lsp4ij.features.LSPPsiElement; import com.redhat.devtools.lsp4ij.features.refactoring.WorkspaceEditData; import com.redhat.devtools.lsp4ij.internal.CancellationUtil; import com.redhat.devtools.lsp4ij.internal.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.swing.*; -import java.util.List; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; @@ -47,6 +65,8 @@ */ class LSPRenameRefactoringDialog extends RefactoringDialog { + private static final Logger LOGGER = LoggerFactory.getLogger(LSPRenameRefactoringDialog.class); + @NotNull private final LSPRenameParams renameParams; @@ -143,6 +163,10 @@ protected boolean areButtonsValid() { static void doRename(@NotNull LSPRenameParams renameParams, @NotNull PsiFile psiFile, @NotNull Editor editor) { + // Before having the language server perform the rename, see if there are any external references that should + // also be updated upon successful completion by the language server + int offset = editor.getCaretModel().getOffset(); + Set externalReferences = getExternalReferences(psiFile, offset); CompletableFuture> future = LSPFileSupport.getSupport(psiFile) .getRenameSupport() @@ -152,7 +176,8 @@ static void doRename(@NotNull LSPRenameParams renameParams, // The 'rename' is stopped: // - if user change the editor content // - if it cancels the Task - String title = LanguageServerBundle.message("lsp.refactor.rename.progress.title", psiFile.getVirtualFile().getName(), renameParams.getNewName()); + String newName = renameParams.getNewName(); + String title = LanguageServerBundle.message("lsp.refactor.rename.progress.title", psiFile.getVirtualFile().getName(), newName); waitUntilDoneAsync(future, title, psiFile); future.handle((workspaceEdits, error) -> { @@ -174,8 +199,19 @@ static void doRename(@NotNull LSPRenameParams renameParams, LSPRenameHandler.showErrorHint(editor, LanguageServerBundle.message("lsp.refactor.rename.cannot.be.renamed.error")); } else { // Apply the rename from the LSP WorkspaceEdit list - WriteCommandAction - .runWriteCommandAction(psiFile.getProject(), () -> workspaceEdits.forEach(workspaceEditData -> LSPIJUtils.applyWorkspaceEdit(workspaceEditData.edit()))); + WriteCommandAction.runWriteCommandAction(psiFile.getProject(), () -> { + workspaceEdits.forEach(workspaceEditData -> LSPIJUtils.applyWorkspaceEdit(workspaceEditData.edit())); + + // Update any found external references with the new name + externalReferences.forEach(externalReference -> { + // Don't let a single failed external reference keep us from updating other references + try { + externalReference.handleElementRename(newName); + } catch (Exception e) { + LOGGER.warn("External reference rename failed.", e); + } + }); + }); } return workspaceEdits; }); @@ -192,4 +228,90 @@ protected void dispose() { super.dispose(); } + @NotNull + private static Set getExternalReferences(@NotNull PsiFile file, int offset) { + Set externalReferences = new LinkedHashSet<>(); + + Document document = LSPIJUtils.getDocument(file); + TextRange wordTextRange = document != null ? LSPIJUtils.getWordRangeAt(document, file, offset) : null; + if (wordTextRange != null) { + LSPPsiElement wordElement = new LSPPsiElement(file, wordTextRange); + String wordText = wordElement.getText(); + if (StringUtil.isNotEmpty(wordText)) { + // When testing, just collect references on the current thread + if (ApplicationManager.getApplication().isUnitTestMode()) { + ContainerUtil.addAllNotNull(externalReferences, collectExternalReferences(file, wordText, wordTextRange, null)); + } else { + // This should happen on a progress indicator since it's performing a textual search of project + // sources, and it must be modal as we need the results synchronously + Project project = file.getProject(); + ProgressManager.getInstance().run(new Task.Modal(project, "Finding References to '" + wordText + "'", true) { + @Override + public void run(@NotNull ProgressIndicator progressIndicator) { + progressIndicator.setIndeterminate(true); + ContainerUtil.addAllNotNull(externalReferences, collectExternalReferences(file, wordText, wordTextRange, progressIndicator)); + } + }); + } + } + } + + return externalReferences; + } + + @NotNull + private static Collection collectExternalReferences(@NotNull PsiFile file, + @NotNull String wordText, + @NotNull TextRange wordTextRange, + @Nullable ProgressIndicator progressIndicator) { + Map externalReferencesByKey = new LinkedHashMap<>(); + + PsiSearchHelper.getInstance(file.getProject()).processElementsWithWord( + (element, offsetInElement) -> { + PsiReference originalReference = element.findReferenceAt(offsetInElement); + List references = originalReference != null ? + PsiReferenceUtil.unwrapMultiReference(originalReference) : + Collections.emptyList(); + for (PsiReference reference : references) { + // Deduplicate using a unique key with reference type, file, and text range + String referenceKey = getReferenceKey(reference); + if (referenceKey != null) { + // Only add references we haven't added previously + if (!externalReferencesByKey.containsKey(referenceKey)) { + PsiElement targetElement = reference.resolve(); + PsiFile targetFile = targetElement != null ? targetElement.getContainingFile() : null; + TextRange targetTextRange = targetFile != null ? targetElement.getTextRange() : null; + if ((targetFile != null) && Objects.equals(file, targetFile) && + (targetTextRange != null) && Objects.equals(wordTextRange, targetTextRange)) { + externalReferencesByKey.put(referenceKey, reference); + } + } + } + } + if (progressIndicator != null) { + progressIndicator.checkCanceled(); + } + return true; + }, + file.getUseScope(), + wordText, + UsageSearchContext.ANY, + // TODO: This should use the client-config caseSensitive setting for the file; what's + // the best way to get that from here? + true + ); + + return externalReferencesByKey.values(); + } + + @Nullable + private static String getReferenceKey(@NotNull PsiReference reference) { + PsiElement sourceElement = reference.getElement(); + PsiFile sourceFile = sourceElement.getContainingFile(); + VirtualFile sourceVirtualFile = sourceFile != null ? sourceFile.getVirtualFile() : null; + if (sourceVirtualFile != null) { + return reference.getClass().getName() + "::" + sourceVirtualFile.getPath() + "::" + reference.getAbsoluteRange(); + } + return null; + } } From 7a64bb7a40f8c737aaffef17eec484598395ced6 Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Sat, 7 Dec 2024 14:43:47 -0600 Subject: [PATCH 02/14] Issue 670 - Derived case-sensitivity for the language server(s) as well as can be. If language servers involved in the rename refactoring have mixed case-sensitivity, searching and matching is performed in a case-sensitive manner. As it says in the comment, in that (likely unusual) situation, it's much better not to have changed something due to a case-sensitivity mismatch than to change things that should not have been changed. --- .../rename/LSPRenameRefactoringDialog.java | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java index 3ab37d1f6..45290ce31 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java @@ -39,6 +39,10 @@ import com.redhat.devtools.lsp4ij.LSPFileSupport; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LanguageServerBundle; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; +import com.redhat.devtools.lsp4ij.client.features.LSPCompletionFeature; +import com.redhat.devtools.lsp4ij.client.features.LSPRenameFeature; import com.redhat.devtools.lsp4ij.features.LSPPsiElement; import com.redhat.devtools.lsp4ij.features.refactoring.WorkspaceEditData; import com.redhat.devtools.lsp4ij.internal.CancellationUtil; @@ -165,8 +169,7 @@ static void doRename(@NotNull LSPRenameParams renameParams, @NotNull Editor editor) { // Before having the language server perform the rename, see if there are any external references that should // also be updated upon successful completion by the language server - int offset = editor.getCaretModel().getOffset(); - Set externalReferences = getExternalReferences(psiFile, offset); + Set externalReferences = getExternalReferences(renameParams, psiFile, editor); CompletableFuture> future = LSPFileSupport.getSupport(psiFile) .getRenameSupport() @@ -229,10 +232,13 @@ protected void dispose() { } @NotNull - private static Set getExternalReferences(@NotNull PsiFile file, int offset) { + private static Set getExternalReferences(@NotNull LSPRenameParams renameParams, + @NotNull PsiFile file, + @NotNull Editor editor) { Set externalReferences = new LinkedHashSet<>(); Document document = LSPIJUtils.getDocument(file); + int offset = editor.getCaretModel().getOffset(); TextRange wordTextRange = document != null ? LSPIJUtils.getWordRangeAt(document, file, offset) : null; if (wordTextRange != null) { LSPPsiElement wordElement = new LSPPsiElement(file, wordTextRange); @@ -240,7 +246,7 @@ private static Set getExternalReferences(@NotNull PsiFile file, in if (StringUtil.isNotEmpty(wordText)) { // When testing, just collect references on the current thread if (ApplicationManager.getApplication().isUnitTestMode()) { - ContainerUtil.addAllNotNull(externalReferences, collectExternalReferences(file, wordText, wordTextRange, null)); + ContainerUtil.addAllNotNull(externalReferences, collectExternalReferences(renameParams, file, wordText, wordTextRange, null)); } else { // This should happen on a progress indicator since it's performing a textual search of project // sources, and it must be modal as we need the results synchronously @@ -249,7 +255,7 @@ private static Set getExternalReferences(@NotNull PsiFile file, in @Override public void run(@NotNull ProgressIndicator progressIndicator) { progressIndicator.setIndeterminate(true); - ContainerUtil.addAllNotNull(externalReferences, collectExternalReferences(file, wordText, wordTextRange, progressIndicator)); + ContainerUtil.addAllNotNull(externalReferences, collectExternalReferences(renameParams, file, wordText, wordTextRange, progressIndicator)); } }); } @@ -260,12 +266,16 @@ public void run(@NotNull ProgressIndicator progressIndicator) { } @NotNull - private static Collection collectExternalReferences(@NotNull PsiFile file, + private static Collection collectExternalReferences(@NotNull LSPRenameParams renameParams, + @NotNull PsiFile file, @NotNull String wordText, @NotNull TextRange wordTextRange, @Nullable ProgressIndicator progressIndicator) { Map externalReferencesByKey = new LinkedHashMap<>(); + // Determine whether or not to search/match in a case-sensitive manner based on client configuration + boolean caseSensitive = isCaseSensitive(renameParams, file); + PsiSearchHelper.getInstance(file.getProject()).processElementsWithWord( (element, offsetInElement) -> { PsiReference originalReference = element.findReferenceAt(offsetInElement); @@ -281,8 +291,13 @@ private static Collection collectExternalReferences(@NotNull PsiFi PsiElement targetElement = reference.resolve(); PsiFile targetFile = targetElement != null ? targetElement.getContainingFile() : null; TextRange targetTextRange = targetFile != null ? targetElement.getTextRange() : null; + String targetText = reference.getCanonicalText(); + // Files match if ((targetFile != null) && Objects.equals(file, targetFile) && - (targetTextRange != null) && Objects.equals(wordTextRange, targetTextRange)) { + // Text ranges match + (targetTextRange != null) && Objects.equals(wordTextRange, targetTextRange) && + // Text matches according to case-sensitivity + (caseSensitive ? wordText.equals(targetText) : wordText.equalsIgnoreCase(targetText))) { externalReferencesByKey.put(referenceKey, reference); } } @@ -296,14 +311,35 @@ private static Collection collectExternalReferences(@NotNull PsiFi file.getUseScope(), wordText, UsageSearchContext.ANY, - // TODO: This should use the client-config caseSensitive setting for the file; what's - // the best way to get that from here? - true + caseSensitive ); return externalReferencesByKey.values(); } + private static boolean isCaseSensitive(@NotNull LSPRenameParams renameParams, + @NotNull PsiFile psiFile) { + List languageServers = renameParams.getLanguageServers(); + if (!ContainerUtil.isEmpty(languageServers)) { + // If any supporting language server is case-sensitive, the search must be case-sensitive; it's better to + // miss changing things that should have been changed than to change things that should not + for (LanguageServerItem languageServer : languageServers) { + LSPClientFeatures clientFeatures = languageServer.getClientFeatures(); + if (clientFeatures != null) { + LSPRenameFeature renameFeature = clientFeatures.getRenameFeature(); + if (renameFeature.isRenameSupported(psiFile)) { + LSPCompletionFeature completionFeature = clientFeatures.getCompletionFeature(); + if (completionFeature.isCaseSensitive(psiFile)) { + return true; + } + } + } + } + } + + return false; + } + @Nullable private static String getReferenceKey(@NotNull PsiReference reference) { PsiElement sourceElement = reference.getElement(); From c2ceb972315999b6a7cc8357430a6364a04fcc3e Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 09:14:40 -0600 Subject: [PATCH 03/14] Moved the notion of case-sensitivity to be independent of any specific feature. This commit also includes an improvement for how bundled files are refreshed in VFS so that the latest-and-greatest contents are always used. Currently this is used specifically for bundled JSON schema files, but it should work for any bundled file that's needed by an IDE feature. --- docs/LSPApi.md | 11 ++++++- .../redhat/devtools/lsp4ij/LSPIJUtils.java | 17 ++++++++++ .../client/features/LSPClientFeatures.java | 12 +++++++ .../client/features/LSPCompletionFeature.java | 15 ++------- .../rename/LSPRenameRefactoringDialog.java | 8 ++--- .../ClientConfigurationSettings.java | 16 ++-------- .../launching/UserDefinedClientFeatures.java | 9 +++++- .../UserDefinedCompletionFeature.java | 31 ------------------- .../AbstractLSPJsonSchemaFileProvider.java | 8 ++++- .../jsonSchema/clientSettings.schema.json | 17 +++------- .../templates/clojure-lsp/clientSettings.json | 4 +-- .../templates/gopls/clientSettings.json | 4 +-- .../templates/jdtls/clientSettings.json | 4 +-- .../templates/lemminx/clientSettings.json | 4 +-- .../templates/metals/clientSettings.json | 4 +-- .../rust-analyzer/clientSettings.json | 4 +-- .../sourcekit-lsp/clientSettings.json | 4 +-- .../clientSettings.json | 4 +-- .../clientSettings.json | 4 +-- .../clientSettings.json | 4 +-- 20 files changed, 76 insertions(+), 108 deletions(-) delete mode 100644 src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedCompletionFeature.java diff --git a/docs/LSPApi.md b/docs/LSPApi.md index 0eb3787fb..af10b6905 100644 --- a/docs/LSPApi.md +++ b/docs/LSPApi.md @@ -2,6 +2,7 @@ The [LSPClientFeatures](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java) API allows you to customize the behavior of LSP features, including: +- [Client-only features](#client-only-features) - [LSP codeAction feature](#lsp-codeAction-feature) - [LSP codeLens feature](#lsp-codeLens-feature) - [LSP color feature](#lsp-color-feature) @@ -163,6 +164,14 @@ public class MyLSPCodeLensFeature extends LSPCodeLensFeature { } ``` +## Client-only Features + +Client-only features are used to provide information to LSP4IJ that is not available from language servers but is required for proper integration of language server features into the IDE's corresponding features. + +| API | Description | Default Behaviour | +|---------------------------------------|--------------------------------------------------------------------------------------|----------------------------| +| boolean isCaseSensitive(PsiFile file) | Determines whether or not the language grammar for the given file is case-sensitive. | `false` (case-insensitive) | + ## LSP CodeAction Feature | API | Description | Default Behaviour | @@ -228,7 +237,7 @@ public class MyLSPCodeLensFeature extends LSPCodeLensFeature { | boolean isStrikeout(CompletionItem item) | Returns true if the IntelliJ lookup is strike out and false otherwise. | use `item.getDeprecated()` or `item.getTags().contains(CompletionItemTag.Deprecated)` | | String getTailText(CompletionItem item) | Returns the IntelliJ lookup tail text from the given LSP completion item and null otherwise. | `item.getLabelDetails().getDetail()` | | boolean isItemTextBold(CompletionItem item) | Returns the IntelliJ lookup item text bold from the given LSP completion item and null otherwise. | `item.getKind() == CompletionItemKind.Keyword` | -| boolean isCaseSensitive(PsiFile file) | Determines whether or not completions should be offered in a case-sensitive manner. | Case-insensitive. | +| ## LSP Declaration Feature diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java index bb992d064..3ca13e2f3 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java @@ -1239,4 +1239,21 @@ public static String getProjectUri(Project project) { .map(l -> new Location(l.getTargetUri(), l.getTargetSelectionRange() != null ? l.getTargetSelectionRange() : l.getTargetRange())) .toList(); } + + /** + * Forces the provided file that is bundled with LSP4IJ to refresh in VFS so that the latest version is used. + * + * @param bundledFile a file that is contained with the LSP4IJ distribution + */ + public static void refreshBundledFile(@NotNull VirtualFile bundledFile) { + // Refresh asynchronously then synchronously because otherwise bundled virtual files won't refresh properly + bundledFile.refresh( + true, + bundledFile.isDirectory(), + () -> bundledFile.refresh( + false, + bundledFile.isDirectory() + ) + ); + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java index 5cea09ef6..40a7d09c3 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java @@ -13,6 +13,7 @@ import com.intellij.openapi.Disposable; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPRequestConstants; import com.redhat.devtools.lsp4ij.LanguageServerWrapper; import com.redhat.devtools.lsp4ij.ServerStatus; @@ -929,6 +930,17 @@ public boolean isEnabled(@NotNull VirtualFile file) { return true; } + /** + * Determines whether or not the language grammar for the file is case-sensitive. + * + * @param file the file + * @return true if the file's language grammar is case-sensitive; otherwise false + */ + public boolean isCaseSensitive(@NotNull PsiFile file) { + // Default to case-insensitive + return false; + } + /** * Set the language server wrapper. * diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java index fb7e01afa..ce6bea948 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPCompletionFeature.java @@ -229,8 +229,8 @@ public void addLookupItem(@NotNull LSPCompletionContext context, @NotNull LookupElement lookupItem, int priority, @NotNull CompletionItem item) { - // Determine whether or not completions should be case-sensitive - boolean caseSensitive = isCaseSensitive(context.getParameters().getOriginalFile()); + // Determine whether or not completions in this language should be case-sensitive + boolean caseSensitive = getClientFeatures().isCaseSensitive(context.getParameters().getOriginalFile()); var prioritizedLookupItem = PrioritizedLookupElement.withPriority(lookupItem, priority); @@ -293,15 +293,4 @@ public void setServerCapabilities(@Nullable ServerCapabilities serverCapabilitie completionCapabilityRegistry.setServerCapabilities(serverCapabilities); } } - - /** - * Determines whether or not completions for the file should be offered in a case-sensitive manner. - * - * @param file the file - * @return true if completions should be offered in a case-sensitive manner; otherwise false - */ - public boolean isCaseSensitive(@NotNull PsiFile file) { - // Default to case-insensitive - return false; - } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java index 45290ce31..15911ceab 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java @@ -41,7 +41,6 @@ import com.redhat.devtools.lsp4ij.LanguageServerBundle; import com.redhat.devtools.lsp4ij.LanguageServerItem; import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; -import com.redhat.devtools.lsp4ij.client.features.LSPCompletionFeature; import com.redhat.devtools.lsp4ij.client.features.LSPRenameFeature; import com.redhat.devtools.lsp4ij.features.LSPPsiElement; import com.redhat.devtools.lsp4ij.features.refactoring.WorkspaceEditData; @@ -327,11 +326,8 @@ private static boolean isCaseSensitive(@NotNull LSPRenameParams renameParams, LSPClientFeatures clientFeatures = languageServer.getClientFeatures(); if (clientFeatures != null) { LSPRenameFeature renameFeature = clientFeatures.getRenameFeature(); - if (renameFeature.isRenameSupported(psiFile)) { - LSPCompletionFeature completionFeature = clientFeatures.getCompletionFeature(); - if (completionFeature.isCaseSensitive(psiFile)) { - return true; - } + if (renameFeature.isRenameSupported(psiFile) && clientFeatures.isCaseSensitive(psiFile)) { + return true; } } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/ClientConfigurationSettings.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/ClientConfigurationSettings.java index 45a126bfe..f3e9b15f7 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/ClientConfigurationSettings.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/ClientConfigurationSettings.java @@ -19,30 +19,20 @@ * Client-side settings for a user-defined language server configuration. */ public class ClientConfigurationSettings { - /** - * Client-side code completion settings. - */ - public static class ClientConfigurationCompletionSettings { - /** - * Whether or not completions should be offered as case-sensitive. Defaults to false. - */ - public boolean caseSensitive = false; - } - /** * Client-side code workspace symbol settings. */ public static class ClientConfigurationWorkspaceSymbolSettings { /** - * Whether or not completions should be offered as case-sensitive. Defaults to false. + * Whether or not the language server can support the IDE's Go To Class action efficiently. Defaults to false. */ public boolean supportsGotoClass = false; } /** - * Client-side code completion settings + * Whether or not the language grammar is case-sensitive. Defaults to false. */ - public @NotNull ClientConfigurationCompletionSettings completions = new ClientConfigurationCompletionSettings(); + public boolean caseSensitive = false; /** * Client-side code workspace symbol settings diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedClientFeatures.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedClientFeatures.java index df6fe13f1..093294da3 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedClientFeatures.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedClientFeatures.java @@ -13,7 +13,9 @@ *******************************************************************************/ package com.redhat.devtools.lsp4ij.server.definition.launching; +import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; +import org.jetbrains.annotations.NotNull; /** * Adds client-side configuration features. @@ -24,7 +26,12 @@ public UserDefinedClientFeatures() { super(); // Use the extended feature implementations - setCompletionFeature(new UserDefinedCompletionFeature()); setWorkspaceSymbolFeature(new UserDefinedWorkspaceSymbolFeature()); } + + public boolean isCaseSensitive(@NotNull PsiFile file) { + UserDefinedLanguageServerDefinition serverDefinition = (UserDefinedLanguageServerDefinition) getServerDefinition(); + ClientConfigurationSettings clientConfiguration = serverDefinition.getLanguageServerClientConfiguration(); + return (clientConfiguration != null) && clientConfiguration.caseSensitive; + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedCompletionFeature.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedCompletionFeature.java deleted file mode 100644 index 370466eb7..000000000 --- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/launching/UserDefinedCompletionFeature.java +++ /dev/null @@ -1,31 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2024 Red Hat Inc. and others. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License v. 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 - * which is available at https://www.apache.org/licenses/LICENSE-2.0. - * - * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 - * - * Contributors: - * Red Hat Inc. - initial API and implementation - *******************************************************************************/ -package com.redhat.devtools.lsp4ij.server.definition.launching; - -import com.intellij.psi.PsiFile; -import com.redhat.devtools.lsp4ij.client.features.LSPCompletionFeature; -import org.jetbrains.annotations.NotNull; - -/** - * Adds client-side completion configuration features. - */ -public class UserDefinedCompletionFeature extends LSPCompletionFeature { - - @Override - public boolean isCaseSensitive(@NotNull PsiFile file) { - UserDefinedLanguageServerDefinition serverDefinition = (UserDefinedLanguageServerDefinition) getClientFeatures().getServerDefinition(); - ClientConfigurationSettings clientConfiguration = serverDefinition.getLanguageServerClientConfiguration(); - return clientConfiguration != null ? clientConfiguration.completions.caseSensitive : super.isCaseSensitive(file); - } -} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java index 131f4e2fd..920efe831 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java @@ -20,8 +20,11 @@ import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; import com.jetbrains.jsonSchema.extension.SchemaType; import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; +import com.redhat.devtools.lsp4ij.LSPIJUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.net.URL; @@ -29,6 +32,9 @@ * Abstract base class for JSON schema file providers that are based on JSON schema files bundled in the plugin distribution. */ abstract class AbstractLSPJsonSchemaFileProvider implements JsonSchemaFileProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLSPJsonSchemaFileProvider.class); + private final String jsonSchemaPath; private final String jsonFilename; private VirtualFile jsonSchemaFile = null; @@ -47,7 +53,7 @@ public final VirtualFile getSchemaFile() { jsonSchemaFile = jsonSchemaFileUrl != null ? VirtualFileManager.getInstance().findFileByUrl(jsonSchemaFileUrl) : null; // Make sure that the IDE is using the absolute latest version of the JSON schema if (jsonSchemaFile != null) { - jsonSchemaFile.refresh(true, false); + LSPIJUtils.refreshBundledFile(jsonSchemaFile); } } return jsonSchemaFile; diff --git a/src/main/resources/jsonSchema/clientSettings.schema.json b/src/main/resources/jsonSchema/clientSettings.schema.json index 27867ad94..5c9a234f6 100644 --- a/src/main/resources/jsonSchema/clientSettings.schema.json +++ b/src/main/resources/jsonSchema/clientSettings.schema.json @@ -5,18 +5,11 @@ "type": "object", "additionalProperties": false, "properties": { - "completions": { - "type": "object", - "title": "Client-side completion configuration", - "additionalProperties": false, - "properties": { - "caseSensitive": { - "type": "boolean", - "title": "Completion case-sensitivity", - "description": "Whether or not completions should be offered as case-sensitive.", - "default": false - } - } + "caseSensitive": { + "type": "boolean", + "title": "Language grammar case-sensitivity (LSP4IJ)", + "description": "Whether or not the language grammar is case-sensitive.", + "default": false }, "workspaceSymbol": { "type": "object", diff --git a/src/main/resources/templates/clojure-lsp/clientSettings.json b/src/main/resources/templates/clojure-lsp/clientSettings.json index b9ae8989b..2a717ac8d 100644 --- a/src/main/resources/templates/clojure-lsp/clientSettings.json +++ b/src/main/resources/templates/clojure-lsp/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": true } diff --git a/src/main/resources/templates/gopls/clientSettings.json b/src/main/resources/templates/gopls/clientSettings.json index b9ae8989b..2a717ac8d 100644 --- a/src/main/resources/templates/gopls/clientSettings.json +++ b/src/main/resources/templates/gopls/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": true } diff --git a/src/main/resources/templates/jdtls/clientSettings.json b/src/main/resources/templates/jdtls/clientSettings.json index b9ae8989b..2a717ac8d 100644 --- a/src/main/resources/templates/jdtls/clientSettings.json +++ b/src/main/resources/templates/jdtls/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": true } diff --git a/src/main/resources/templates/lemminx/clientSettings.json b/src/main/resources/templates/lemminx/clientSettings.json index 0cac35366..b3239a2d2 100644 --- a/src/main/resources/templates/lemminx/clientSettings.json +++ b/src/main/resources/templates/lemminx/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": false } diff --git a/src/main/resources/templates/metals/clientSettings.json b/src/main/resources/templates/metals/clientSettings.json index b9ae8989b..2a717ac8d 100644 --- a/src/main/resources/templates/metals/clientSettings.json +++ b/src/main/resources/templates/metals/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": true } diff --git a/src/main/resources/templates/rust-analyzer/clientSettings.json b/src/main/resources/templates/rust-analyzer/clientSettings.json index b9ae8989b..2a717ac8d 100644 --- a/src/main/resources/templates/rust-analyzer/clientSettings.json +++ b/src/main/resources/templates/rust-analyzer/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": true } diff --git a/src/main/resources/templates/sourcekit-lsp/clientSettings.json b/src/main/resources/templates/sourcekit-lsp/clientSettings.json index b9ae8989b..2a717ac8d 100644 --- a/src/main/resources/templates/sourcekit-lsp/clientSettings.json +++ b/src/main/resources/templates/sourcekit-lsp/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": true } diff --git a/src/main/resources/templates/typescript-language-server/clientSettings.json b/src/main/resources/templates/typescript-language-server/clientSettings.json index b9ae8989b..2a717ac8d 100644 --- a/src/main/resources/templates/typescript-language-server/clientSettings.json +++ b/src/main/resources/templates/typescript-language-server/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": true } diff --git a/src/main/resources/templates/vscode-css-language-server/clientSettings.json b/src/main/resources/templates/vscode-css-language-server/clientSettings.json index 0cac35366..b3239a2d2 100644 --- a/src/main/resources/templates/vscode-css-language-server/clientSettings.json +++ b/src/main/resources/templates/vscode-css-language-server/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": true - }, + "caseSensitive": true, "workspaceSymbol": { "supportsGotoClass": false } diff --git a/src/main/resources/templates/vscode-html-language-server/clientSettings.json b/src/main/resources/templates/vscode-html-language-server/clientSettings.json index e97cd6519..00d4fb8df 100644 --- a/src/main/resources/templates/vscode-html-language-server/clientSettings.json +++ b/src/main/resources/templates/vscode-html-language-server/clientSettings.json @@ -1,7 +1,5 @@ { - "completions": { - "caseSensitive": false - }, + "caseSensitive": false, "workspaceSymbol": { "supportsGotoClass": false } From 8c0f81c74d71ce2c06541e9cf6689015633119f2 Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 09:24:44 -0600 Subject: [PATCH 04/14] Spit up the composite check for a valid reference into distinct staged checks based on PR review feedback. --- .../rename/LSPRenameRefactoringDialog.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java index 15911ceab..55b3645f8 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java @@ -289,15 +289,17 @@ private static Collection collectExternalReferences(@NotNull LSPRe if (!externalReferencesByKey.containsKey(referenceKey)) { PsiElement targetElement = reference.resolve(); PsiFile targetFile = targetElement != null ? targetElement.getContainingFile() : null; - TextRange targetTextRange = targetFile != null ? targetElement.getTextRange() : null; - String targetText = reference.getCanonicalText(); // Files match - if ((targetFile != null) && Objects.equals(file, targetFile) && - // Text ranges match - (targetTextRange != null) && Objects.equals(wordTextRange, targetTextRange) && + if ((targetFile != null) && Objects.equals(file, targetFile)) { + // Text ranges match + TextRange targetTextRange = targetElement.getTextRange(); + if ((targetTextRange != null) && Objects.equals(wordTextRange, targetTextRange)) { // Text matches according to case-sensitivity - (caseSensitive ? wordText.equals(targetText) : wordText.equalsIgnoreCase(targetText))) { - externalReferencesByKey.put(referenceKey, reference); + String targetText = reference.getCanonicalText(); + if (caseSensitive ? wordText.equals(targetText) : wordText.equalsIgnoreCase(targetText)) { + externalReferencesByKey.put(referenceKey, reference); + } + } } } } From 9e364989683612f0cf9b31e4e07eb6af4ba92309 Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 09:32:42 -0600 Subject: [PATCH 05/14] Removed unintended change that was being used to test VFS refresh. --- src/main/resources/jsonSchema/clientSettings.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/jsonSchema/clientSettings.schema.json b/src/main/resources/jsonSchema/clientSettings.schema.json index 5c9a234f6..907db4f3f 100644 --- a/src/main/resources/jsonSchema/clientSettings.schema.json +++ b/src/main/resources/jsonSchema/clientSettings.schema.json @@ -7,7 +7,7 @@ "properties": { "caseSensitive": { "type": "boolean", - "title": "Language grammar case-sensitivity (LSP4IJ)", + "title": "Language grammar case-sensitivity", "description": "Whether or not the language grammar is case-sensitive.", "default": false }, From 42170d3c46da9b6fe44501cc7d06e76b5cbf041b Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 11:27:31 -0600 Subject: [PATCH 06/14] Issue 670 - Find Usages now also includes external references, so the logic to find those references is extracted into the common class LSPExternalReferencesFinder. --- gradle.properties | 5 +- .../lsp4ij/LanguageServiceAccessor.java | 20 +++ .../rename/LSPRenameRefactoringDialog.java | 149 +++------------- .../usages/LSPExternalReferencesFinder.java | 168 ++++++++++++++++++ .../lsp4ij/usages/LSPUsageSearcher.java | 11 ++ 5 files changed, 227 insertions(+), 126 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java diff --git a/gradle.properties b/gradle.properties index 03789f1ee..e9516658b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,10 +10,11 @@ pluginSinceBuild=232 #pluginUntilBuild=233.* # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension platformType=IC -platformVersion=2023.2 +platformVersion=2024.3 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 -platformPlugins=com.redhat.devtools.intellij.telemetry:1.2.1.62, textmate, properties +#platformPlugins=com.redhat.devtools.intellij.telemetry:1.2.1.62, textmate, properties, com.intellij.modules.json, com.illuminatedcloud2.intellij:2.3.3.9 +platformPlugins=com.redhat.devtools.intellij.telemetry:1.2.1.62, textmate, properties, com.intellij.modules.json # Gradle Releases -> https://github.com/gradle/gradle/releases gradleVersion=8.5 channel=nightly diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java index 93e40a922..591a75616 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java @@ -423,6 +423,26 @@ public CompletableFuture> getAsyncMatched() { } } + /** + * Returns the language server wrappers for the specified file synchronously. + * + * @param file the file + * @return the language server wrappers that apply to the file + */ + @ApiStatus.Internal + @NotNull + public Set getMatchedLanguageServerWrappersSync(@NotNull VirtualFile file) { + Set languageServerWrappers = new LinkedHashSet<>(); + MatchedLanguageServerDefinitions matchedLanguageServerDefinitions = getMatchedLanguageServerDefinitions(file, project, false); + if (matchedLanguageServerDefinitions != null) { + Set languageServerDefinitions = matchedLanguageServerDefinitions.getMatched(); + for (LanguageServerDefinition languageServerDefinition : languageServerDefinitions) { + languageServerWrappers.add(new LanguageServerWrapper(project, languageServerDefinition)); + } + } + return languageServerWrappers; + } + /** * Returns the matched language server definitions for the given file. * diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java index 55b3645f8..4b53df1b9 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/rename/LSPRenameRefactoringDialog.java @@ -15,7 +15,6 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.WriteCommandAction; -import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileTypes.FileTypes; import com.intellij.openapi.progress.ProgressIndicator; @@ -23,36 +22,27 @@ import com.intellij.openapi.progress.Task; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; -import com.intellij.openapi.util.TextRange; -import com.intellij.openapi.util.text.StringUtil; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiReference; -import com.intellij.psi.impl.source.resolve.reference.PsiReferenceUtil; -import com.intellij.psi.search.PsiSearchHelper; -import com.intellij.psi.search.UsageSearchContext; import com.intellij.refactoring.RefactoringBundle; import com.intellij.refactoring.ui.NameSuggestionsField; import com.intellij.refactoring.ui.RefactoringDialog; -import com.intellij.util.containers.ContainerUtil; import com.redhat.devtools.lsp4ij.LSPFileSupport; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LanguageServerBundle; -import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; -import com.redhat.devtools.lsp4ij.client.features.LSPRenameFeature; -import com.redhat.devtools.lsp4ij.features.LSPPsiElement; import com.redhat.devtools.lsp4ij.features.refactoring.WorkspaceEditData; import com.redhat.devtools.lsp4ij.internal.CancellationUtil; import com.redhat.devtools.lsp4ij.internal.StringUtils; +import com.redhat.devtools.lsp4ij.usages.LSPExternalReferencesFinder; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.swing.*; -import java.util.*; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutionException; @@ -168,7 +158,8 @@ static void doRename(@NotNull LSPRenameParams renameParams, @NotNull Editor editor) { // Before having the language server perform the rename, see if there are any external references that should // also be updated upon successful completion by the language server - Set externalReferences = getExternalReferences(renameParams, psiFile, editor); + int offset = editor.getCaretModel().getOffset(); + Set externalReferences = getExternalReferences(psiFile, offset); CompletableFuture> future = LSPFileSupport.getSupport(psiFile) .getRenameSupport() @@ -231,121 +222,31 @@ protected void dispose() { } @NotNull - private static Set getExternalReferences(@NotNull LSPRenameParams renameParams, - @NotNull PsiFile file, - @NotNull Editor editor) { + private static Set getExternalReferences(@NotNull PsiFile file, int offset) { Set externalReferences = new LinkedHashSet<>(); - Document document = LSPIJUtils.getDocument(file); - int offset = editor.getCaretModel().getOffset(); - TextRange wordTextRange = document != null ? LSPIJUtils.getWordRangeAt(document, file, offset) : null; - if (wordTextRange != null) { - LSPPsiElement wordElement = new LSPPsiElement(file, wordTextRange); - String wordText = wordElement.getText(); - if (StringUtil.isNotEmpty(wordText)) { - // When testing, just collect references on the current thread - if (ApplicationManager.getApplication().isUnitTestMode()) { - ContainerUtil.addAllNotNull(externalReferences, collectExternalReferences(renameParams, file, wordText, wordTextRange, null)); - } else { - // This should happen on a progress indicator since it's performing a textual search of project - // sources, and it must be modal as we need the results synchronously - Project project = file.getProject(); - ProgressManager.getInstance().run(new Task.Modal(project, "Finding References to '" + wordText + "'", true) { - @Override - public void run(@NotNull ProgressIndicator progressIndicator) { - progressIndicator.setIndeterminate(true); - ContainerUtil.addAllNotNull(externalReferences, collectExternalReferences(renameParams, file, wordText, wordTextRange, progressIndicator)); - } + // When testing, just collect references on the current thread + if (ApplicationManager.getApplication().isUnitTestMode()) { + LSPExternalReferencesFinder.processExternalReferences(file, offset, reference -> { + externalReferences.add(reference); + return true; + }); + } else { + // This should happen on a progress indicator since it's performing a textual search of project + // sources, and it must be modal as we need the results synchronously + Project project = file.getProject(); + ProgressManager.getInstance().run(new Task.Modal(project, "Finding External References", true) { + @Override + public void run(@NotNull ProgressIndicator progressIndicator) { + progressIndicator.setIndeterminate(true); + LSPExternalReferencesFinder.processExternalReferences(file, offset, reference -> { + externalReferences.add(reference); + return true; }); } - } + }); } return externalReferences; } - - @NotNull - private static Collection collectExternalReferences(@NotNull LSPRenameParams renameParams, - @NotNull PsiFile file, - @NotNull String wordText, - @NotNull TextRange wordTextRange, - @Nullable ProgressIndicator progressIndicator) { - Map externalReferencesByKey = new LinkedHashMap<>(); - - // Determine whether or not to search/match in a case-sensitive manner based on client configuration - boolean caseSensitive = isCaseSensitive(renameParams, file); - - PsiSearchHelper.getInstance(file.getProject()).processElementsWithWord( - (element, offsetInElement) -> { - PsiReference originalReference = element.findReferenceAt(offsetInElement); - List references = originalReference != null ? - PsiReferenceUtil.unwrapMultiReference(originalReference) : - Collections.emptyList(); - for (PsiReference reference : references) { - // Deduplicate using a unique key with reference type, file, and text range - String referenceKey = getReferenceKey(reference); - if (referenceKey != null) { - // Only add references we haven't added previously - if (!externalReferencesByKey.containsKey(referenceKey)) { - PsiElement targetElement = reference.resolve(); - PsiFile targetFile = targetElement != null ? targetElement.getContainingFile() : null; - // Files match - if ((targetFile != null) && Objects.equals(file, targetFile)) { - // Text ranges match - TextRange targetTextRange = targetElement.getTextRange(); - if ((targetTextRange != null) && Objects.equals(wordTextRange, targetTextRange)) { - // Text matches according to case-sensitivity - String targetText = reference.getCanonicalText(); - if (caseSensitive ? wordText.equals(targetText) : wordText.equalsIgnoreCase(targetText)) { - externalReferencesByKey.put(referenceKey, reference); - } - } - } - } - } - } - if (progressIndicator != null) { - progressIndicator.checkCanceled(); - } - return true; - }, - file.getUseScope(), - wordText, - UsageSearchContext.ANY, - caseSensitive - ); - - return externalReferencesByKey.values(); - } - - private static boolean isCaseSensitive(@NotNull LSPRenameParams renameParams, - @NotNull PsiFile psiFile) { - List languageServers = renameParams.getLanguageServers(); - if (!ContainerUtil.isEmpty(languageServers)) { - // If any supporting language server is case-sensitive, the search must be case-sensitive; it's better to - // miss changing things that should have been changed than to change things that should not - for (LanguageServerItem languageServer : languageServers) { - LSPClientFeatures clientFeatures = languageServer.getClientFeatures(); - if (clientFeatures != null) { - LSPRenameFeature renameFeature = clientFeatures.getRenameFeature(); - if (renameFeature.isRenameSupported(psiFile) && clientFeatures.isCaseSensitive(psiFile)) { - return true; - } - } - } - } - - return false; - } - - @Nullable - private static String getReferenceKey(@NotNull PsiReference reference) { - PsiElement sourceElement = reference.getElement(); - PsiFile sourceFile = sourceElement.getContainingFile(); - VirtualFile sourceVirtualFile = sourceFile != null ? sourceFile.getVirtualFile() : null; - if (sourceVirtualFile != null) { - return reference.getClass().getName() + "::" + sourceVirtualFile.getPath() + "::" + reference.getAbsoluteRange(); - } - return null; - } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java new file mode 100644 index 000000000..a37282d72 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java @@ -0,0 +1,168 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat Inc. and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 + * which is available at https://www.apache.org/licenses/LICENSE-2.0. + * + * SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 + * + * Contributors: + * Red Hat Inc. - initial API and implementation + *******************************************************************************/ +package com.redhat.devtools.lsp4ij.usages; + +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiReference; +import com.intellij.psi.impl.source.resolve.reference.PsiReferenceUtil; +import com.intellij.psi.search.PsiSearchHelper; +import com.intellij.psi.search.UsageSearchContext; +import com.intellij.util.Processor; +import com.intellij.util.containers.ContainerUtil; +import com.redhat.devtools.lsp4ij.LSPIJUtils; +import com.redhat.devtools.lsp4ij.LanguageServerWrapper; +import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; +import com.redhat.devtools.lsp4ij.features.LSPPsiElement; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * Utility class that helps to process/find external references to LSP4IJ-based (pseudo-)elements. + */ +public class LSPExternalReferencesFinder { + + /** + * Processes all external references for the LSP4IJ element at the offset in the specified file. + * + * @param file the file for which the element at the specified offset should be processed for external references + * @param offset the offset of the element in the file + * @param processor the external reference processor + */ + public static void processExternalReferences(@NotNull PsiFile file, + int offset, + @NotNull Processor processor) { + VirtualFile virtualFile = file.getVirtualFile(); + if (virtualFile != null) { + Document document = LSPIJUtils.getDocument(virtualFile); + TextRange wordTextRange = document != null ? LSPIJUtils.getWordRangeAt(document, file, offset) : null; + if (wordTextRange != null) { + LSPPsiElement wordElement = new LSPPsiElement(file, wordTextRange); + String wordText = wordElement.getText(); + if (StringUtil.isNotEmpty(wordText)) { + processExternalReferences( + file, + wordText, + wordTextRange, + ProgressManager.getInstance().getProgressIndicator(), + processor + ); + } + } + } + } + + private static void processExternalReferences(@NotNull PsiFile file, + @NotNull String wordText, + @NotNull TextRange wordTextRange, + @Nullable ProgressIndicator progressIndicator, + @NotNull Processor processor) { + VirtualFile virtualFile = file.getVirtualFile(); + if (virtualFile == null) { + return; + } + + Project project = file.getProject(); + Set languageServerWrappers = LanguageServiceAccessor.getInstance(project).getMatchedLanguageServerWrappersSync(virtualFile); + if (ContainerUtil.isEmpty(languageServerWrappers)) { + return; + } + + // Determine whether or not to search/match in a case-sensitive manner based on client configuration + boolean caseSensitive = isCaseSensitive(languageServerWrappers, file); + + if (progressIndicator != null) { + progressIndicator.setText("Finding usages of '" + wordText + "'"); + } + + Set externalReferenceKeys = new HashSet<>(); + PsiSearchHelper.getInstance(file.getProject()).processElementsWithWord( + (element, offsetInElement) -> { + PsiReference originalReference = element.findReferenceAt(offsetInElement); + List references = originalReference != null ? + PsiReferenceUtil.unwrapMultiReference(originalReference) : + Collections.emptyList(); + for (PsiReference reference : references) { + // Deduplicate using a unique key with reference type, file, and text range + String referenceKey = getReferenceKey(reference); + if (referenceKey != null) { + // Only add references we haven't added previously + if (!externalReferenceKeys.contains(referenceKey)) { + PsiElement targetElement = reference.resolve(); + PsiFile targetFile = targetElement != null ? targetElement.getContainingFile() : null; + // Files match + if ((targetFile != null) && Objects.equals(file, targetFile)) { + // Text ranges match + TextRange targetTextRange = targetElement.getTextRange(); + if ((targetTextRange != null) && Objects.equals(wordTextRange, targetTextRange)) { + // Text matches according to case-sensitivity + String targetText = reference.getCanonicalText(); + if (caseSensitive ? wordText.equals(targetText) : wordText.equalsIgnoreCase(targetText)) { + if (!processor.process(reference)) { + return false; + } + externalReferenceKeys.add(referenceKey); + } + } + } + } + } + } + if (progressIndicator != null) { + progressIndicator.checkCanceled(); + } + return true; + }, + ReadAction.compute(file::getUseScope), + wordText, + UsageSearchContext.ANY, + caseSensitive + ); + } + + private static boolean isCaseSensitive(Set languageServerWrappers, @NotNull PsiFile psiFile) { + // If any supporting language server is case-sensitive, the search must be case-sensitive; it's better to + // miss changing things that should have been changed than to change things that should not + for (LanguageServerWrapper languageServerWrapper : languageServerWrappers) { + LSPClientFeatures clientFeatures = languageServerWrapper.getClientFeatures(); + if ((clientFeatures != null) && clientFeatures.isCaseSensitive(psiFile)) { + return true; + } + } + + return false; + } + + @Nullable + private static String getReferenceKey(@NotNull PsiReference reference) { + PsiElement sourceElement = reference.getElement(); + PsiFile sourceFile = sourceElement.getContainingFile(); + VirtualFile sourceVirtualFile = sourceFile != null ? sourceFile.getVirtualFile() : null; + if (sourceVirtualFile != null) { + return reference.getClass().getName() + "::" + sourceVirtualFile.getPath() + "::" + reference.getAbsoluteRange(); + } + return null; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java index 7aa60facf..ac704a61d 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java @@ -15,6 +15,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -43,6 +44,7 @@ *
  • Type Definitions
  • *
  • References
  • *
  • Implementations
  • + *
  • External References
  • * */ public class LSPUsageSearcher extends CustomUsageSearcher { @@ -92,6 +94,15 @@ public void processElementUsages(@NotNull PsiElement element, @NotNull Processor } catch (Exception e) { LOGGER.error("Error while collection LSP Usages", e); } + + // For completeness' sake, also collect external usages to LSP (pseudo-)elements + int offset = ReadAction.compute(() -> { + Editor editor = LSPIJUtils.editorForElement(element); + return editor != null ? editor.getCaretModel().getOffset() : -1; + }); + if (offset > -1) { + LSPExternalReferencesFinder.processExternalReferences(file, offset, reference -> processor.process(new UsageInfo2UsageAdapter(new UsageInfo(reference)))); + } } @Nullable From 364e9f105a3a4ef1fb51f9ce567bf89096bf61ab Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 11:28:59 -0600 Subject: [PATCH 07/14] This should not have been included in the previous commit. --- gradle.properties | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index e9516658b..03789f1ee 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,11 +10,10 @@ pluginSinceBuild=232 #pluginUntilBuild=233.* # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension platformType=IC -platformVersion=2024.3 +platformVersion=2023.2 # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 -#platformPlugins=com.redhat.devtools.intellij.telemetry:1.2.1.62, textmate, properties, com.intellij.modules.json, com.illuminatedcloud2.intellij:2.3.3.9 -platformPlugins=com.redhat.devtools.intellij.telemetry:1.2.1.62, textmate, properties, com.intellij.modules.json +platformPlugins=com.redhat.devtools.intellij.telemetry:1.2.1.62, textmate, properties # Gradle Releases -> https://github.com/gradle/gradle/releases gradleVersion=8.5 channel=nightly From d7d468c299e48140642774a0ba5ecb60fd68968d Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 11:33:03 -0600 Subject: [PATCH 08/14] Removed this unused logger. --- .../jsonSchema/AbstractLSPJsonSchemaFileProvider.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java index 920efe831..9e47d0681 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java @@ -23,8 +23,6 @@ import com.redhat.devtools.lsp4ij.LSPIJUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.net.URL; @@ -33,8 +31,6 @@ */ abstract class AbstractLSPJsonSchemaFileProvider implements JsonSchemaFileProvider { - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLSPJsonSchemaFileProvider.class); - private final String jsonSchemaPath; private final String jsonFilename; private VirtualFile jsonSchemaFile = null; From b5105e77034fc57284eda9e09c49a390f56d08dd Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 11:34:30 -0600 Subject: [PATCH 09/14] Made this a proper pure utility class. --- .../devtools/lsp4ij/usages/LSPExternalReferencesFinder.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java index a37282d72..dd983817d 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java @@ -42,7 +42,11 @@ /** * Utility class that helps to process/find external references to LSP4IJ-based (pseudo-)elements. */ -public class LSPExternalReferencesFinder { +public final class LSPExternalReferencesFinder { + + private LSPExternalReferencesFinder() { + // Pure utility class + } /** * Processes all external references for the LSP4IJ element at the offset in the specified file. From 4c064357b08c4796493afab6458b3dfd0fae7fdc Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 11:36:12 -0600 Subject: [PATCH 10/14] The @NotNull annotation was lost in the signature change refactoring. --- .../devtools/lsp4ij/usages/LSPExternalReferencesFinder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java index dd983817d..274251d49 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java @@ -146,7 +146,7 @@ private static void processExternalReferences(@NotNull PsiFile file, ); } - private static boolean isCaseSensitive(Set languageServerWrappers, @NotNull PsiFile psiFile) { + private static boolean isCaseSensitive(@NotNull Set languageServerWrappers, @NotNull PsiFile psiFile) { // If any supporting language server is case-sensitive, the search must be case-sensitive; it's better to // miss changing things that should have been changed than to change things that should not for (LanguageServerWrapper languageServerWrapper : languageServerWrappers) { From c34b31b2ed6d8dcd274b90230c10dfba2b0b3c77 Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 12:11:06 -0600 Subject: [PATCH 11/14] Changed the way we get the text offset when searching for external usages. --- .../redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java index ac704a61d..2f4f54366 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java @@ -15,7 +15,6 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -96,13 +95,7 @@ public void processElementUsages(@NotNull PsiElement element, @NotNull Processor } // For completeness' sake, also collect external usages to LSP (pseudo-)elements - int offset = ReadAction.compute(() -> { - Editor editor = LSPIJUtils.editorForElement(element); - return editor != null ? editor.getCaretModel().getOffset() : -1; - }); - if (offset > -1) { - LSPExternalReferencesFinder.processExternalReferences(file, offset, reference -> processor.process(new UsageInfo2UsageAdapter(new UsageInfo(reference)))); - } + LSPExternalReferencesFinder.processExternalReferences(file, element.getTextOffset(), reference -> processor.process(new UsageInfo2UsageAdapter(new UsageInfo(reference)))); } @Nullable From 6b72fa0166d4eeba63846b0e4ccd2613de1e6142 Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Mon, 9 Dec 2024 12:17:56 -0600 Subject: [PATCH 12/14] ProcessCanceledException should not be handled/logged explicitly. It should be propagated. --- .../com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java index 2f4f54366..f37899059 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPUsageSearcher.java @@ -15,6 +15,7 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.editor.Document; +import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; @@ -90,6 +91,8 @@ public void processElementUsages(@NotNull PsiElement element, @NotNull Processor } } } + } catch (ProcessCanceledException pce) { + throw pce; } catch (Exception e) { LOGGER.error("Error while collection LSP Usages", e); } From 7e6e64a10f34057deafdbff33992517fa66931c2 Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Tue, 10 Dec 2024 08:41:48 -0600 Subject: [PATCH 13/14] Removed refreshBundleFile() as Angelo has another approach and the presence of that method is causing merge conflicts. --- .../com/redhat/devtools/lsp4ij/LSPIJUtils.java | 17 ----------------- .../AbstractLSPJsonSchemaFileProvider.java | 4 +--- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java index 3ca13e2f3..bb992d064 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java @@ -1239,21 +1239,4 @@ public static String getProjectUri(Project project) { .map(l -> new Location(l.getTargetUri(), l.getTargetSelectionRange() != null ? l.getTargetSelectionRange() : l.getTargetRange())) .toList(); } - - /** - * Forces the provided file that is bundled with LSP4IJ to refresh in VFS so that the latest version is used. - * - * @param bundledFile a file that is contained with the LSP4IJ distribution - */ - public static void refreshBundledFile(@NotNull VirtualFile bundledFile) { - // Refresh asynchronously then synchronously because otherwise bundled virtual files won't refresh properly - bundledFile.refresh( - true, - bundledFile.isDirectory(), - () -> bundledFile.refresh( - false, - bundledFile.isDirectory() - ) - ); - } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java index 9e47d0681..131f4e2fd 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/settings/jsonSchema/AbstractLSPJsonSchemaFileProvider.java @@ -20,7 +20,6 @@ import com.jetbrains.jsonSchema.extension.JsonSchemaFileProvider; import com.jetbrains.jsonSchema.extension.SchemaType; import com.jetbrains.jsonSchema.impl.JsonSchemaVersion; -import com.redhat.devtools.lsp4ij.LSPIJUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,7 +29,6 @@ * Abstract base class for JSON schema file providers that are based on JSON schema files bundled in the plugin distribution. */ abstract class AbstractLSPJsonSchemaFileProvider implements JsonSchemaFileProvider { - private final String jsonSchemaPath; private final String jsonFilename; private VirtualFile jsonSchemaFile = null; @@ -49,7 +47,7 @@ public final VirtualFile getSchemaFile() { jsonSchemaFile = jsonSchemaFileUrl != null ? VirtualFileManager.getInstance().findFileByUrl(jsonSchemaFileUrl) : null; // Make sure that the IDE is using the absolute latest version of the JSON schema if (jsonSchemaFile != null) { - LSPIJUtils.refreshBundledFile(jsonSchemaFile); + jsonSchemaFile.refresh(true, false); } } return jsonSchemaFile; From 1a856bac45f0a63d649c07a5d00ac987decf0e9c Mon Sep 17 00:00:00 2001 From: Scott Wells Date: Tue, 10 Dec 2024 09:04:24 -0600 Subject: [PATCH 14/14] Implemented PR review feedback suggestions. --- docs/LSPApi.md | 10 +------ .../lsp4ij/LanguageServerWrapper.java | 2 ++ .../lsp4ij/LanguageServiceAccessor.java | 20 ------------- .../usages/LSPExternalReferencesFinder.java | 28 +++---------------- 4 files changed, 7 insertions(+), 53 deletions(-) diff --git a/docs/LSPApi.md b/docs/LSPApi.md index af10b6905..69fc1d088 100644 --- a/docs/LSPApi.md +++ b/docs/LSPApi.md @@ -2,7 +2,6 @@ The [LSPClientFeatures](https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/client/features/LSPClientFeatures.java) API allows you to customize the behavior of LSP features, including: -- [Client-only features](#client-only-features) - [LSP codeAction feature](#lsp-codeAction-feature) - [LSP codeLens feature](#lsp-codeLens-feature) - [LSP color feature](#lsp-color-feature) @@ -61,6 +60,7 @@ public class MyLanguageServerFactory implements LanguageServerFactory { | boolean keepServerAlive() | Returns `true` if the server is kept alive even if all files associated with the language server are closed and `false` otherwise. | `false` | | boolean canStopServerByUser() | Returns `true` if the user can stop the language server in LSP console from the context menu and `false` otherwise. | `true` | | boolean isEnabled(VirtualFile) | Returns `true` if the language server is enabled for the given file and `false` otherwise. | `true` | +| boolean isCaseSensitive(PsiFile file) | Returns `true` if the language grammar for the given file is case-sensitive and `false` otherwise. | `false` | ```java package my.language.server; @@ -164,14 +164,6 @@ public class MyLSPCodeLensFeature extends LSPCodeLensFeature { } ``` -## Client-only Features - -Client-only features are used to provide information to LSP4IJ that is not available from language servers but is required for proper integration of language server features into the IDE's corresponding features. - -| API | Description | Default Behaviour | -|---------------------------------------|--------------------------------------------------------------------------------------|----------------------------| -| boolean isCaseSensitive(PsiFile file) | Determines whether or not the language grammar for the given file is case-sensitive. | `false` (case-insensitive) | - ## LSP CodeAction Feature | API | Description | Default Behaviour | diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java index 7f7bb30cf..8a073afa3 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerWrapper.java @@ -1169,6 +1169,7 @@ public boolean isDidRenameFilesSupported(@NotNull PsiFile file) { return fileOperationsManager.canDidRenameFiles(LSPIJUtils.toUri(file), file.isDirectory()); } + @NotNull public LSPClientFeatures getClientFeatures() { if (clientFeatures == null) { clientFeatures = getOrCreateClientFeatures(); @@ -1176,6 +1177,7 @@ public LSPClientFeatures getClientFeatures() { return clientFeatures; } + @NotNull private synchronized LSPClientFeatures getOrCreateClientFeatures() { if (clientFeatures != null) { return clientFeatures; diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java index 591a75616..93e40a922 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServiceAccessor.java @@ -423,26 +423,6 @@ public CompletableFuture> getAsyncMatched() { } } - /** - * Returns the language server wrappers for the specified file synchronously. - * - * @param file the file - * @return the language server wrappers that apply to the file - */ - @ApiStatus.Internal - @NotNull - public Set getMatchedLanguageServerWrappersSync(@NotNull VirtualFile file) { - Set languageServerWrappers = new LinkedHashSet<>(); - MatchedLanguageServerDefinitions matchedLanguageServerDefinitions = getMatchedLanguageServerDefinitions(file, project, false); - if (matchedLanguageServerDefinitions != null) { - Set languageServerDefinitions = matchedLanguageServerDefinitions.getMatched(); - for (LanguageServerDefinition languageServerDefinition : languageServerDefinitions) { - languageServerWrappers.add(new LanguageServerWrapper(project, languageServerDefinition)); - } - } - return languageServerWrappers; - } - /** * Returns the matched language server definitions for the given file. * diff --git a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java index 274251d49..5766d361f 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/usages/LSPExternalReferencesFinder.java @@ -28,11 +28,8 @@ import com.intellij.psi.search.PsiSearchHelper; import com.intellij.psi.search.UsageSearchContext; import com.intellij.util.Processor; -import com.intellij.util.containers.ContainerUtil; import com.redhat.devtools.lsp4ij.LSPIJUtils; -import com.redhat.devtools.lsp4ij.LanguageServerWrapper; import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; -import com.redhat.devtools.lsp4ij.client.features.LSPClientFeatures; import com.redhat.devtools.lsp4ij.features.LSPPsiElement; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -88,21 +85,17 @@ private static void processExternalReferences(@NotNull PsiFile file, return; } - Project project = file.getProject(); - Set languageServerWrappers = LanguageServiceAccessor.getInstance(project).getMatchedLanguageServerWrappersSync(virtualFile); - if (ContainerUtil.isEmpty(languageServerWrappers)) { - return; - } - // Determine whether or not to search/match in a case-sensitive manner based on client configuration - boolean caseSensitive = isCaseSensitive(languageServerWrappers, file); + Project project = file.getProject(); + boolean caseSensitive = LanguageServiceAccessor.getInstance(project) + .hasAny(file.getVirtualFile(), ls -> ls.getClientFeatures().isCaseSensitive(file)); if (progressIndicator != null) { progressIndicator.setText("Finding usages of '" + wordText + "'"); } Set externalReferenceKeys = new HashSet<>(); - PsiSearchHelper.getInstance(file.getProject()).processElementsWithWord( + PsiSearchHelper.getInstance(project).processElementsWithWord( (element, offsetInElement) -> { PsiReference originalReference = element.findReferenceAt(offsetInElement); List references = originalReference != null ? @@ -146,19 +139,6 @@ private static void processExternalReferences(@NotNull PsiFile file, ); } - private static boolean isCaseSensitive(@NotNull Set languageServerWrappers, @NotNull PsiFile psiFile) { - // If any supporting language server is case-sensitive, the search must be case-sensitive; it's better to - // miss changing things that should have been changed than to change things that should not - for (LanguageServerWrapper languageServerWrapper : languageServerWrappers) { - LSPClientFeatures clientFeatures = languageServerWrapper.getClientFeatures(); - if ((clientFeatures != null) && clientFeatures.isCaseSensitive(psiFile)) { - return true; - } - } - - return false; - } - @Nullable private static String getReferenceKey(@NotNull PsiReference reference) { PsiElement sourceElement = reference.getElement();