redhat-developer / redhat-developer/lsp4ij

Feature Request: Distinguish explicit vs implicit feature invocations

Open
#1,617 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Java
Stars
344
Forks
113
Avg merge
5h 22m
Merged PRs (30d)
15

Description

Summary

Allow LSP client plugins to differentiate between explicit user-triggered actions ("Go To > LSP Reference(s)") and implicit framework-driven invocations (Ctrl+Click, Find Usages) when gating LSP features. Currently, both paths share the same gating mechanism, making it impossible to disable a feature for implicit integration while keeping the explicit "Go To" actions available.

Motivation

Use case

Consider a language server that supports textDocument/references for all languages, but the client plugin provides per-language user preferences (e.g., a "Navigation" toggle in settings). When a user disables Navigation for a specific language:

  • Expected: The implicit integration (Ctrl+Click contributing to Find Usages, GotoDeclarationHandler, etc.) should stop — the user doesn't want the LSP to participate in these automatic flows for this language.
  • Expected: The explicit "Go To > LSP Reference(s)" action should still work — the user deliberately chose this action from the context menu, signaling intent to use the language server.
  • Actual: Both paths are blocked because they both flow through getLanguageServers(file, isEnabled, isSupported), and the isEnabled() filter is applied uniformly.
Why this matters

Many language server clients support languages that also have first-class IDE plugins (e.g., Java in IntelliJ, Python in PyCharm). For these languages, users typically want:

  • Implicit integration disabled (to avoid conflicts with the native plugin's Ctrl+Click, Find Usages, etc.)
  • Explicit LSP actions available as a fallback (e.g., "Go To > LSP References" when the native plugin's results are insufficient)

Currently, there's no way to express this distinction.

Current Architecture

Two invocation paths

Implicit (framework-driven):

Ctrl+Click → LSPGotoDeclarationHandler → LSPDefinitionSupport.getDefinitions()
                                        → getLanguageServers(file,
                                            f -> f.getDefinitionFeature().isEnabled(file),
                                            f -> f.getDefinitionFeature().isSupported(file))

Explicit (user-triggered):

"Go To > LSP References" → LSPGoToReferenceAction.canSupportAction()
                          → canSupportFeature() → isSupported(file)  // visibility (OK)

                        → LSPGoToReferenceAction.actionPerformed()
                          → LSPReferenceSupport.getReferences()
                          → getLanguageServers(file,
                              f -> f.getReferencesFeature().isEnabled(file),
                              f -> f.getReferencesFeature().isSupported(file))
The problem

Both paths call getLanguageServers() with the same isEnabled() predicate. When isEnabled() returns false (because the user disabled the feature for this language):

  1. The explicit "Go To > LSP References" menu item is visible (because canSupportAction() checks isSupported(), not isEnabled())
  2. But clicking it produces no results (because getLanguageServers() filters out the server via isEnabled())

This is confusing UX — the action appears available but silently does nothing.

Affected features

This affects all features that have both explicit and implicit paths:

Feature Explicit Action Implicit Integration
References LSPGoToReferenceAction LSPUsageSearcher, LSPFindUsagesHandlerFactory
Declaration LSPGoToDeclarationAction LSPGotoDeclarationHandler
Implementation LSPGoToImplementationAction (via Find Usages)
Type Definition LSPGoToTypeDefinitionAction (via Find Usages)

Workaround

Until this is addressed, client plugins can work around the issue by always returning true from isEnabled() and moving the user-preference check into is*Supported(). However, this is suboptimal because isEnabled() serves as a pre-server-start optimization — returning true unconditionally may cause the server to be started for files where it shouldn't be.

Environment

  • LSP4IJ version: latest (as of July 2025)
  • Affected classes:
    • AbstractLSPDocumentFeature.isEnabled()
    • AbstractLSPDocumentFeatureSupport.getLanguageServers()
    • AbstractLSPGoToAction and all subclasses
    • LSPGotoDeclarationHandler
    • LSPFindUsagesHandlerFactory / LSPUsageSearcher

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing AbstractLSPDocumentFeatureSupport.getLanguageServers() and the explicit paths through AbstractLSPGoToAction, then compare them with LSPGotoDeclarationHandler and LSPFindUsagesHandlerFactory/LSPUsageSearcher. Define how explicit actions bypass implicit gating without changing visibility checks, cover the affected feature actions, and verify that disabled implicit integrations stop while explicit actions still return results.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
developer-experience, tooling
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.