PY-90092 lsp-tools: inline lsp plugin into community

IJ-MR-207996

GitOrigin-RevId: 8a8619c679d0991f0a3c8a1cf3c425e3d1eaa4c4
This commit is contained in:
Morgan Bartholomew
2026-07-24 16:16:38 +00:00
committed by intellij-monorepo-bot
parent febef8303a
commit 8f33ac4c5c
141 changed files with 8283 additions and 1 deletions
+7
View File
@@ -1794,6 +1794,8 @@
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.helpers.iml" filepath="$PROJECT_DIR$/python/intellij.python.helpers.iml" />
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.helpers.tests.iml" filepath="$PROJECT_DIR$/python/intellij.python.helpers.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/python/IntelliLang-python/intellij.python.langInjection.iml" filepath="$PROJECT_DIR$/python/IntelliLang-python/intellij.python.langInjection.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-lsp-core/intellij.python.lsp.core.iml" filepath="$PROJECT_DIR$/python/python-lsp-core/intellij.python.lsp.core.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-lsp-impl/intellij.python.lsp.impl.iml" filepath="$PROJECT_DIR$/python/python-lsp-impl/intellij.python.lsp.impl.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-markdown/intellij.python.markdown.iml" filepath="$PROJECT_DIR$/python/python-markdown/intellij.python.markdown.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-markdown/intellij.python.markdown.tests.iml" filepath="$PROJECT_DIR$/python/python-markdown/intellij.python.markdown.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.ml.features/intellij.python.ml.features.iml" filepath="$PROJECT_DIR$/python/intellij.python.ml.features/intellij.python.ml.features.iml" />
@@ -1808,10 +1810,13 @@
<module fileurl="file://$PROJECT_DIR$/python/intellij.python.pydev.iml" filepath="$PROJECT_DIR$/python/intellij.python.pydev.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-pyproject/intellij.python.pyproject.iml" filepath="$PROJECT_DIR$/python/python-pyproject/intellij.python.pyproject.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-pyproject/intellij.python.pyproject.tests.iml" filepath="$PROJECT_DIR$/python/python-pyproject/intellij.python.pyproject.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-pyrefly/intellij.python.pyrefly.iml" filepath="$PROJECT_DIR$/python/python-pyrefly/intellij.python.pyrefly.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-pyright/intellij.python.pyright.iml" filepath="$PROJECT_DIR$/python/python-pyright/intellij.python.pyright.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-pytools/intellij.python.pytools.iml" filepath="$PROJECT_DIR$/python/python-pytools/intellij.python.pytools.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-pytools/tests/intellij.python.pytools.tests.iml" filepath="$PROJECT_DIR$/python/python-pytools/tests/intellij.python.pytools.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-pytools-ui/intellij.python.pytools.ui.iml" filepath="$PROJECT_DIR$/python/python-pytools-ui/intellij.python.pytools.ui.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-requirements/intellij.python.requirements.iml" filepath="$PROJECT_DIR$/python/python-requirements/intellij.python.requirements.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-ruff/intellij.python.ruff.iml" filepath="$PROJECT_DIR$/python/python-ruff/intellij.python.ruff.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-sdk/intellij.python.sdk.iml" filepath="$PROJECT_DIR$/python/python-sdk/intellij.python.sdk.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-sdk/intellij.python.sdk.tests.iml" filepath="$PROJECT_DIR$/python/python-sdk/intellij.python.sdk.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-sdk-ui/intellij.python.sdk.ui.iml" filepath="$PROJECT_DIR$/python/python-sdk-ui/intellij.python.sdk.ui.iml" />
@@ -1832,6 +1837,8 @@
<module fileurl="file://$PROJECT_DIR$/python/python-test-env/plain/intellij.python.test.env.plain.iml" filepath="$PROJECT_DIR$/python/python-test-env/plain/intellij.python.test.env.plain.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-test-env/uv/intellij.python.test.env.uv.iml" filepath="$PROJECT_DIR$/python/python-test-env/uv/intellij.python.test.env.uv.iml" />
<module fileurl="file://$PROJECT_DIR$/python/tools/intellij.python.tools.iml" filepath="$PROJECT_DIR$/python/tools/intellij.python.tools.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-ty/intellij.python.ty.iml" filepath="$PROJECT_DIR$/python/python-ty/intellij.python.ty.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-type-engine/intellij.python.typeEngine.iml" filepath="$PROJECT_DIR$/python/python-type-engine/intellij.python.typeEngine.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-uv/backend/intellij.python.uv.backend.iml" filepath="$PROJECT_DIR$/python/python-uv/backend/intellij.python.uv.backend.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-uv/common/intellij.python.uv.common.iml" filepath="$PROJECT_DIR$/python/python-uv/common/intellij.python.uv.common.iml" />
<module fileurl="file://$PROJECT_DIR$/python/python-uv/tests/intellij.python.uv.tests.iml" filepath="$PROJECT_DIR$/python/python-uv/tests/intellij.python.uv.tests.iml" />
+7
View File
@@ -1593,6 +1593,8 @@ python/python-external-index
python/python-features-trainer
python/python-grazie
python/python-hatch
python/python-lsp-core
python/python-lsp-impl
python/python-markdown
python/python-packaging
python/python-parser
@@ -1604,11 +1606,14 @@ python/python-process-output/frontend
python/python-psi-api
python/python-psi-impl
python/python-pyproject
python/python-pyrefly
python/python-pyright
python/python-pytools
python/python-pytools-ui
python/python-pytools/tests
python/python-requirements
python/python-restructuredtext
python/python-ruff
python/python-sdk
python/python-sdk-configurator/backend
python/python-sdk-configurator/common
@@ -1623,6 +1628,8 @@ python/python-test-env/junit4
python/python-test-env/junit5
python/python-test-env/plain
python/python-test-env/uv
python/python-ty
python/python-type-engine
python/python-uv/backend
python/python-uv/common
python/python-uv/tests
@@ -1 +1,22 @@
{ "exclusions": [] }
{
"exclusions": [
{
"inspection": "SplitModeApiUsage",
"file": "community/python/python-type-engine/src/com/intellij/python/typeEngine/PyTypeEngineStatusBarWidget.kt",
"line": 49,
"reason": "PY-90092: Python LSP/type-engine tools were moved from PyCharm Pro into PyCharm Community. The type engine status bar widget is monolith-only UI that has not been split into frontend/backend yet."
},
{
"inspection": "SplitModeApiUsage",
"file": "community/python/python-type-engine/src/com/intellij/python/typeEngine/PyTypeEngineStatusBarWidget.kt",
"line": 132,
"reason": "PY-90092: JBPopupFactory usage in the monolith-only Python type engine status bar widget (not yet split for remote dev)."
},
{
"inspection": "SplitModeXmlApiUsage",
"file": "community/python/python-type-engine/resources/intellij.python.typeEngine.xml",
"line": 42,
"reason": "PY-90092: statusBarWidgetFactory registration for the monolith-only Python type engine widget (not yet split for remote dev)."
}
]
}
+2
View File
@@ -1409,6 +1409,8 @@ jvm_library(
"//plugins/toml/core:core_test_lib",
"//python/python-poetry/backend",
"//python/python-poetry/backend:backend_test_lib",
"//python/python-lsp-core:lsp-core",
"//python/python-lsp-core:lsp-core_test_lib",
],
)
### auto-generated section `build intellij.python.community.tests` end
@@ -141,6 +141,7 @@
<orderEntry type="module" module-name="intellij.libraries.junit4" scope="TEST" />
<orderEntry type="module" module-name="intellij.toml.core" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.community.impl.poetry.backend" scope="TEST" />
<orderEntry type="module" module-name="intellij.python.lsp.core" scope="TEST" />
</component>
<component name="TestModuleProperties" production-module="intellij.python.community.impl" />
</module>
+21
View File
@@ -116,6 +116,12 @@
- name: lib/modules/intellij.python.langInjection.jar
contentModules:
- name: intellij.python.langInjection
- name: lib/modules/intellij.python.lsp.core.jar
contentModules:
- name: intellij.python.lsp.core
- name: lib/modules/intellij.python.lsp.impl.jar
contentModules:
- name: intellij.python.lsp.impl
- name: lib/modules/intellij.python.markdown.jar
contentModules:
- name: intellij.python.markdown
@@ -161,6 +167,12 @@
- name: lib/modules/intellij.python.pyproject.jar
contentModules:
- name: intellij.python.pyproject
- name: lib/modules/intellij.python.pyrefly.jar
contentModules:
- name: intellij.python.pyrefly
- name: lib/modules/intellij.python.pyright.jar
contentModules:
- name: intellij.python.pyright
- name: lib/modules/intellij.python.pytools.jar
contentModules:
- name: intellij.python.pytools
@@ -170,6 +182,9 @@
- name: lib/modules/intellij.python.requirements.jar
contentModules:
- name: intellij.python.requirements
- name: lib/modules/intellij.python.ruff.jar
contentModules:
- name: intellij.python.ruff
- name: lib/modules/intellij.python.sdk.jar
contentModules:
- name: intellij.python.sdk
@@ -200,6 +215,12 @@
- name: lib/modules/intellij.python.terminal.shared.jar
contentModules:
- name: intellij.python.terminal.shared
- name: lib/modules/intellij.python.ty.jar
contentModules:
- name: intellij.python.ty
- name: lib/modules/intellij.python.typeEngine.jar
contentModules:
- name: intellij.python.typeEngine
- name: lib/modules/intellij.python.uv.backend.jar
contentModules:
- name: intellij.python.uv.backend
@@ -94,6 +94,13 @@ The Python plug-in provides smart editing for Python scripts. The feature set of
<module name="intellij.python.terminal.frontend"/>
<module name="intellij.python.pytools.ui"/>
<module name="intellij.python.black"/>
<module name="intellij.python.lsp.core"/>
<module name="intellij.python.lsp.impl"/>
<module name="intellij.python.typeEngine"/>
<module name="intellij.python.ty"/>
<module name="intellij.python.ruff"/>
<module name="intellij.python.pyrefly"/>
<module name="intellij.python.pyright"/>
</content>
<content>
<module name="intellij.libraries.tuweni.toml"/>
+106
View File
@@ -0,0 +1,106 @@
load("@rules_jvm//:jvm.bzl", "jvm_library")
### auto-generated section `build intellij.python.lsp.core` start
jvm_library(
name = "lsp-core",
srcs = glob(
[
"src/**/*.kt",
"src/**/*.java",
"src/**/*.form",
],
allow_empty = True,
),
module_name = "intellij.python.lsp.core",
resource_strip_prefix = "resources",
resources = glob(
["resources/**/*"],
),
visibility = ["//visibility:public"],
exports = [
# do not sort,
"@lib//:eclipse-lsp4j",
"@lib//:eclipse-lsp4j-jsonrpc",
],
deps = [
# do not sort,
"@lib//:kotlin-stdlib",
"//libraries/guava",
"@lib//:eclipse-lsp4j",
"@lib//:eclipse-lsp4j-jsonrpc",
"//platform/core-api:core",
"//platform/core-ui",
"//platform/editor-ui-api:editor-ui",
"//platform/lang-impl",
"//platform/platform-impl:ide-impl",
"//platform/lsp",
"//platform/projectModel-api:projectModel",
"//platform/analysis-api:analysis",
"//platform/lang-api:lang",
"//python:python-community-impl",
"//python/python-core-impl:community-core-impl",
"//python/python-sdk:sdk",
"//platform/observable",
"//platform/ide-core",
"//platform/statistics",
"//platform/core-impl",
"//platform/util:util-ui",
"//python/python-pytools-ui:pytools-ui",
"//python/python-pytools:pytools",
"//python/python-exec-service:community-execService",
"//platform/lsp-impl",
],
)
jvm_library(
name = "lsp-core_test_lib",
testonly = True,
srcs = glob(
[],
allow_empty = True,
),
module_name = "intellij.python.lsp.core",
visibility = ["//visibility:public"],
exports = [
# do not sort,
"@lib//:eclipse-lsp4j",
"@lib//:eclipse-lsp4j-jsonrpc",
],
runtime_deps = [
# do not sort,
":lsp-core",
"//libraries/guava:guava_test_lib",
"//platform/core-api:core_test_lib",
"//platform/core-ui:core-ui_test_lib",
"//platform/editor-ui-api:editor-ui_test_lib",
"//platform/lang-impl:lang-impl_test_lib",
"//platform/platform-impl:ide-impl_test_lib",
"//platform/lsp:lsp_test_lib",
"//platform/projectModel-api:projectModel_test_lib",
"//platform/analysis-api:analysis_test_lib",
"//platform/lang-api:lang_test_lib",
"//python:python-community-impl_test_lib",
"//python/python-core-impl:community-core-impl_test_lib",
"//python/python-sdk:sdk_test_lib",
"//platform/observable:observable_test_lib",
"//platform/ide-core:ide-core_test_lib",
"//platform/statistics:statistics_test_lib",
"//platform/core-impl:core-impl_test_lib",
"//platform/util:util-ui_test_lib",
"//python/python-pytools-ui:pytools-ui_test_lib",
"//python/python-pytools:pytools_test_lib",
"//python/python-exec-service:community-execService_test_lib",
"//platform/lsp-impl:lsp-impl_test_lib",
],
)
### auto-generated section `build intellij.python.lsp.core` end
### auto-generated section `iml intellij.python.lsp.core` start
exports_files(
[
"intellij.python.lsp.core.iml",
],
visibility = ["//visibility:public"],
)
### auto-generated section `iml intellij.python.lsp.core` end
+2
View File
@@ -0,0 +1,2 @@
owner 'PyCharm Codeinsight'
+101
View File
@@ -0,0 +1,101 @@
## Updated: 2025-12-26
Today we have major LSP providers for Python type checking:
- [pyrefly](https://github.com/facebook/pyrefly)
- [ty](https://github.com/astral-sh/ty)
# Overview
## Pyrefly
Pyrefly is Meta’s new Rust-based Python type checker, replacing Pyre — Meta’s previous Python type checker written in OCaml.
The expectation is that Pyrefly should be faster, more portable, and more capable compared to Pyre.
One key thing the Pyrefly team made very clear this year is that they want to be truly open source. Pyre was also technically open source, but it was more of a “we built this for our needs, but here’s the source code if you want it”. In contrast, one of the foundational goals of Pyrefly is to be more engaged with the needs of the open-source community.
## ty
ty is also a Rust-based Python type checker currently under development by Astral, the team behind uv and ruff. The project was formerly known as Red-Knot, but now has its official name: ty. Compared to Meta, Astral has been much quieter about its announcement: just a soft launch on GitHub, a brief 30-minute presentation, and a couple of blog articles and podcasts here and there.
## Comparison
Both pyrefly and ty are written in Rust, both are incremental (albeit implemented slightly differently: see details below), and both are powered under the hood by Ruff for AST parsing. Also, both have first-class support for command-line type checking and LSP/IDE integration.
However, other than the fact that they are both fast Python type checkers, that’s where the similarities end.
The primary difference lies in their goals. Pyrefly tries to be as aggressive as possible when typing — inferring as much as possible so that even code with absolutely no explicit types can have some amount of typing guarantees.
ty, on the other hand, follows a different mantra: the gradual guarantee. The principal idea is that in a well-typed program, removing a type annotation should not cause a type error. In other words: you shouldn’t need to add new types to working code to resolve type errors.
Example with difference:
```python
def foo(imp: Any):
return str(imp)
a = foo(123)
# ✅ pyrefly | Revealed type: str
# ➖ ty | Revealed type: `Unknown`
# ➖ mypy | Revealed type is "Any"
# ✅ pyright | Type of "a" is "str"
reveal_type(a)
# ✅ pyrefly | ERROR: `+` is not supported between `str` and `Literal[1]`
# ➖ ty | < No Error >
# ➖ mypy | < No Error >
# ✅ pyright | ERROR: Operator "+" not supported for types "str" and "Literal[1]"
a + 1
```
## Experiments
I decided to test and implement type inference based on pyrefly. To resolve types I use the following algorithm:
- Filter PyTypedElements to determine whether they should be sent to LSP type evaluation or can be resolved instantly. For instance, for PyClass and PyStringLiteral we don't need to send them to LSP because it's sufficient to resolve the type from the builtin type provider.
- I created a special cache for each file that persists between document modifications and stores calculated types there.
- The first 10 requests are sent to LSP directly without batching because we have completion that should run really fast. Please note that our limitation is to not use completion from LSP and instead use builtin with LSP type provider. After the first 10 requests, I collect all filtered types from the file and send them to LSP as a batch request.
- LSP returns types as strings with type hints. We use special code that converts these strings to PyTypes. The code is not ideal and some types cannot be converted from string to PyType.
## Test Environment
For testing I used a project from https://packages.jetbrains.team/files/p/pyqa/pycharm-test-data/pandasExample.zip and measured how much time it takes to infer types for all PyTypedElements in the file.
I tested both the builtin type provider and the pyrefly type provider. I ran the task in single-threaded mode (one by one) and in multithreaded mode with 8 parallel threads.
After the first run, I modified the file by adding `a=2\n` to the end and checked types again.
## Results
| LSP Provider | First Run (ms) | Second Run (ms) |
|---------------------|----------------|-----------------|
| builtin (single) | 4040 | 2400 |
| pyrefly (single) | 4010 | 1200 |
| builtin (8 thread) | 2200 | 1200 |
| pyrefly (8 threads) | 3051 | 608 |
The following durations are for single-threaded Pyrefly:
| Operation | Total Duration First Run (ms) | Second Run (ms) |
|-----------------------|--------------------------------|-----------------|
| LSP Request | 3000 | 1789 |
| String Type -> PyType | 557 | 166 |
| Total | 4010 | 1200 |
Please note that in my test I had 2 LSP requests because I inferred types for pandas_example.py and the file pandas/typing.py was processed by the String PyType to PyType resolver.
# Tested Hypotheses:
- Sending LSP requests without batching (1 type per request): This has significant overhead and is ~10x slower compared to batching.
- Sending all LSP requests in a single batch: This is faster overall, but results in very long response times for large files. For instance, a 3k-line file has a completion response time of 3 seconds. Not all files need types for all PSI elements, so storing them all creates significant memory and time overhead, especially for libraries.
- Sending LSP requests both individually and in batch simultaneously: There is some internal blocking, and the response for a single type can take longer than the batch response.
# Risks
- The rest of our codeinsight was created and tested using our current type inference engine. Replacing this could cause issues for anything that relies on typing. These could include refactorings, resolve, inspections. To mitigate this, we should make our codeinsight tests (most of which are unit tests) are compatible with external type inference.
- Memory consumption: The WebStorm team is dealing with a heavy TS server. We should be prepared for the same data to be cached twice: once inside the LSP and again inside the IDE.
- We should invest in converting LSP string type representations into PyType instances. Ideally, we wouldn't use our old type engine.
# Distribution of external type inference binaries
- We have a fork of Ty at the JB Account on GitHub. This fork contains an action that builds the distribution of the patched Ty.
- Pyrefly has already accepted our provideType() method. Therefore, we can install Pyrefly using the Python package manager.
# Helpful Links:
- Test project for validating type resolution on large pandas file: https://packages.jetbrains.team/files/p/pyqa/pycharm-test-data/pandasExample.zip
- My MR branch: https://code.jetbrains.team/p/ij/repositories/ultimate/reviews/186102/files?repo=ultimate
- My MR to pyrefly: https://github.com/facebook/pyrefly/pull/1918
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.libraries.guava" />
<orderEntry type="library" exported="" name="eclipse.lsp4j" level="project" />
<orderEntry type="library" exported="" name="eclipse.lsp4j.jsonrpc" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.lsp" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="intellij.python.community.impl" />
<orderEntry type="module" module-name="intellij.python.community.core.impl" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.platform.ide.observable" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.platform.core.impl" />
<orderEntry type="module" module-name="intellij.platform.util.ui" />
<orderEntry type="module" module-name="intellij.python.pytools.ui" />
<orderEntry type="module" module-name="intellij.python.pytools" />
<orderEntry type="module" module-name="intellij.python.community.execService" />
<orderEntry type="module" module-name="intellij.platform.lsp.impl" />
</component>
</module>
@@ -0,0 +1,30 @@
<idea-plugin visibility="internal">
<dependencies>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.python.community.execService"/>
<module name="intellij.python.community.impl"/>
<module name="intellij.python.pytools"/>
<module name="intellij.python.pytools.ui"/>
<!-- endregion -->
</dependencies>
<extensionPoints>
<extensionPoint qualifiedName="com.intellij.python.lsp.typeEngineConfigurable"
interface="com.intellij.python.lsp.core.typeEngine.PyTypeEngineProvider"
dynamic="true"/>
<extensionPoint qualifiedName="com.intellij.python.lsp.notebookLanguageResolver"
interface="com.intellij.python.lsp.core.NotebookLanguageResolver"
dynamic="true"/>
</extensionPoints>
<extensions defaultExtensionNs="com.intellij">
<python.lsp.typeEngineConfigurable implementation="com.intellij.python.lsp.core.typeEngine.BuiltInPyTypeEngineProvider"/>
<registryKey key="python.lsp.type.engine.notebooks"
defaultValue="false"
description="Enable Python LSP-backed type engines in notebooks"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.python.lsp.core.typeEngine.PyTypeEngineUsageCollector"/>
<projectSettings service="com.intellij.python.lsp.core.typeEngine.PyTypeEngineProjectSettings"/>
<notificationGroup id="Python LSP Tools" displayType="STICKY_BALLOON" isLogByDefault="true" bundle="messages.PyBundle"
key="notification.group.python.lsp.tools"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,10 @@
comment.multimodule.not.warning=You can configure PyCharm type engine only for a single-module project now.
display.name=Type Engine
engine.label=Type engine:
project.default=Project default
pycharm.description=A smart IDE type engine optimized for real world Python, including Django templates, SQLAlchemy models, and more
pyrefly.description=Pyrefly provides fast and accurate type checking, with support for Django and Pydantic
ty.description=Currently in Beta, ty provides fast and accurate type checking designed to work well with untyped code. Supports advanced type analysis features.
open.settings=Type Engine Settings\u2026
pyrefly.package.install.description=If Pyrefly is not already installed, the latest version will be installed automatically after you apply these settings.
@@ -0,0 +1,25 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.lsp.core
import com.intellij.lang.Language
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.vfs.VirtualFile
import org.jetbrains.annotations.ApiStatus
/**
* Resolves the kernel language of a notebook file without forcing python-lsp-core to
* depend on any concrete notebook framework. Integration modules register implementations.
*/
@ApiStatus.Internal
interface NotebookLanguageResolver {
/**
* Returns the kernel language of [file] if this resolver recognises it as a
* notebook, or `null` otherwise.
*/
fun resolveLanguage(file: VirtualFile): Language?
companion object {
val EP_NAME: ExtensionPointName<NotebookLanguageResolver> =
ExtensionPointName.create("com.intellij.python.lsp.notebookLanguageResolver")
}
}
@@ -0,0 +1,18 @@
package com.intellij.python.lsp.core
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
object PyLspCoreBundle {
private val myDynamicBundle = DynamicBundle(this::class.java, BUNDLE)
@NonNls
const val BUNDLE: String = "messages.PyLspCoreBundle"
@Nls
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String {
return myDynamicBundle.getMessage(key, *params)
}
}
@@ -0,0 +1,53 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.lsp.core
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.jetbrains.python.PythonFileType
import com.jetbrains.python.PythonLanguage
import com.jetbrains.python.pyi.PyiFileType
import org.jetbrains.annotations.ApiStatus
/**
* Predicate shared by [PyLspToolDescriptor.isSupportedFile] and
* [PyLspToolIntegrationProvider.fileOpened].
*
* Accepts:
* - regular Python sources ([PythonFileType])
* - Python stub files ([PyiFileType])
* - when [notebookSupported] is `true`, notebooks whose kernel language resolves
* to Python via [NotebookLanguageResolver].
*
* [notebookSupported] is a per-descriptor knob: a python LSP tool that can serve
* notebooks opts in by overriding its `supportsNotebooks` property to `true`.
*/
@ApiStatus.Internal
fun isPythonFile(file: VirtualFile, notebookSupported: Boolean = false): Boolean =
isPythonFile(
file,
notebookSupported,
NotebookLanguageResolver.EP_NAME.extensionList,
)
/**
* Test helper: pass explicit [NotebookLanguageResolver] implementations instead of relying on the EP.
*/
@ApiStatus.Internal
fun isPythonFile(
file: VirtualFile,
notebookSupported: Boolean,
resolvers: List<NotebookLanguageResolver>,
): Boolean {
if (isPythonSourceFile(file)) return true
if (!notebookSupported) return false
if (!Registry.`is`("jupyter.lsp.enabled", false)) return false
return isPythonNotebookLanguage(file, resolvers)
}
@ApiStatus.Internal
fun isPythonSourceFile(file: VirtualFile): Boolean =
file.fileType is PythonFileType || file.fileType is PyiFileType
@ApiStatus.Internal
fun isPythonNotebookLanguage(file: VirtualFile, resolvers: List<NotebookLanguageResolver>): Boolean =
resolvers.any { it.resolveLanguage(file)?.isKindOf(PythonLanguage.INSTANCE) == true }
@@ -0,0 +1,427 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.lsp.core
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.intention.CustomizableIntentionAction
import com.intellij.codeInsight.intention.FileModifier
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.codeInsight.intention.IntentionActionWithOptions
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo
import com.intellij.codeInspection.util.IntentionName
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.modcommand.ModCommandAction
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.EffectType
import com.intellij.openapi.editor.markup.TextAttributes
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.progress.runBlockingMaybeCancellable
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.openapi.util.text.buildChildren
import com.intellij.openapi.util.text.buildHtml
import com.intellij.openapi.util.text.plus
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.lsp.api.LspClient
import com.intellij.platform.lsp.api.LspClientDescriptor
import com.intellij.platform.lsp.api.LspClientManager
import com.intellij.platform.lsp.api.LspIntegrationProvider
import com.intellij.platform.lsp.api.LspIntegrationProvider.LspClientStarter
import com.intellij.platform.lsp.api.LspServerListener
import com.intellij.platform.lsp.api.customization.LspCodeLensCustomizer
import com.intellij.platform.lsp.api.customization.LspCodeLensDisabled
import com.intellij.platform.lsp.api.customization.LspCompletionCustomizer
import com.intellij.platform.lsp.api.customization.LspCompletionSupport
import com.intellij.platform.lsp.api.customization.LspCustomization
import com.intellij.platform.lsp.api.customization.LspDiagnosticsCustomizer
import com.intellij.platform.lsp.api.customization.LspDiagnosticsDisabled
import com.intellij.platform.lsp.api.customization.LspDiagnosticsSupport
import com.intellij.platform.lsp.api.customization.LspHoverCustomizer
import com.intellij.platform.lsp.api.customization.LspHoverDisabled
import com.intellij.platform.lsp.api.customization.LspHoverSupport
import com.intellij.platform.lsp.api.customization.LspInlayHintSupport
import com.intellij.platform.lsp.api.customization.LspOptimizeImportsCustomizer
import com.intellij.platform.lsp.api.customization.LspOptimizeImportsDisabled
import com.intellij.platform.lsp.api.lsWidget.LspClientWidgetItem
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.python.community.execService.asGeneralCommandLine
import com.intellij.python.pytools.PyTool
import com.intellij.python.pytools.getExecutableWithBaseArgs
import com.intellij.python.pytools.isEnabledOn
import com.intellij.python.pytools.lsp.PyLspTool
import com.intellij.python.pytools.lsp.PyLspToolSettings
import com.intellij.python.pytools.ui.configuration.PyExternalToolsConfigurable
import com.intellij.python.pytools.ui.getInstalledToolPackage
import com.intellij.ui.JBColor
import com.jetbrains.python.onFailure
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.sdk.ModuleOrProject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.eclipse.lsp4j.Diagnostic
import org.eclipse.lsp4j.DiagnosticSeverity
import org.eclipse.lsp4j.ExecuteCommandParams
import org.eclipse.lsp4j.InitializeResult
import org.jetbrains.annotations.Nls
import java.util.Collections
import javax.swing.Icon
abstract class PyLspToolIntegrationProvider : LspIntegrationProvider {
private val listenerConnectedForProjects: MutableSet<Project> = Collections.synchronizedSet(HashSet<Project>())
override fun fileOpened(project: Project, file: VirtualFile, clientStarter: LspClientStarter) {
// Find the module for this file
val module = ModuleUtilCore.findModuleForFile(file, project) ?: return
val moduleOrProject = ModuleOrProject.ModuleAndProject(module)
val descriptor = getDescriptor(module)
if (!descriptor.isSupportedFile(file))
return
if (!descriptor.pyTool.isEnabledOn(project))
return
descriptor.supportProvider = this
if (listenerConnectedForProjects.add(project)) {
val listenerDisposable = Disposer.newDisposable("Python LSP package listener")
Disposer.register(project, listenerDisposable)
Disposer.register(listenerDisposable) {
listenerConnectedForProjects.remove(project)
}
subscribeOnChanges(descriptor.pyTool, project, listenerDisposable)
}
runBlockingMaybeCancellable {
descriptor.pyTool.getExecutableWithBaseArgs(moduleOrProject, descriptor.executableName)
}.onFailure { return }
clientStarter.ensureClientStarted(descriptor)
}
override fun createWidgetItem(lspClient: LspClient, currentFile: VirtualFile?): LspClientWidgetItem? {
return object : LspClientWidgetItem(
lspClient = lspClient,
currentFile = currentFile,
icon = getIcon(lspClient),
settingsPageClass = PyExternalToolsConfigurable::class.java
) {
override val itemLabel: @NlsSafe String
get() = presentableName(lspClient) + versionPostfix + rootPostfix
}
}
abstract fun getDescriptor(module: Module): PyLspToolDescriptor
fun getIcon(lspClient: LspClient): Icon = (lspClient.descriptor as PyLspToolDescriptor).pyTool.icon
fun presentableName(lspClient: LspClient): @NlsSafe String = lspClient.initializeResult?.serverInfo?.name
?: lspClient.descriptor.presentableName
protected open fun subscribeOnChanges(pyTool: PyTool, project: Project, parentDisposable: Disposable) {
project.messageBus.connect(parentDisposable)
.subscribe(PythonPackageManager.PACKAGE_MANAGEMENT_TOPIC, LspPackageListener(pyTool, project))
}
inner class LspPackageListener(val pyTool: PyTool, val project: Project) : PythonPackageManagementListener {
var wasThisToolInstalled: Boolean? = null
override fun packagesChanged(sdk: Sdk) {
val lspServerManager = LspClientManager.getInstance(project)
val manager = PythonPackageManager.forSdk(project, sdk)
val isInstalled = manager.getInstalledToolPackage(pyTool) != null
if (isInstalled != wasThisToolInstalled) {
val providerClass = this@PyLspToolIntegrationProvider::class.java
if (isInstalled) {
lspServerManager.startClientsIfNeeded(providerClass)
}
else {
lspServerManager.stopClients(providerClass)
}
}
wasThisToolInstalled = isInstalled
}
}
}
/**
* Base class for Python LSP tool descriptors that can be either module-scoped or project-scoped.
*
* When a [module] is provided, the LSP server will only serve files within that module's content roots.
* When [module] is null, it falls back to project-wide behavior using all project base directories.
*/
abstract class PyLspToolDescriptor(
val module: Module,
val pyTool: PyLspTool<*>,
) : LspClientDescriptor(module.project, pyTool.presentableName, *ModuleRootManager.getInstance(module).contentRoots) {
/**
* Whether this python LSP tool can serve Jupyter notebooks. Defaults to `true` because all current Python LSP tools support notebooks.
*/
open val supportsNotebooks: Boolean get() = true
override fun isSupportedFile(file: VirtualFile): Boolean =
isPythonFile(file, notebookSupported = supportsNotebooks) && isFileInModule(file)
protected fun isFileInModule(file: VirtualFile): Boolean =
ModuleUtilCore.findModuleForFile(file, project) == module
abstract val toolConfig: PyLspToolSettings
open val executableName: String get() = pyTool.packageName.name
abstract fun lspArguments(): List<String>
override fun createCommandLine(): GeneralCommandLine {
val moduleOrProject = ModuleOrProject.ModuleAndProject(module)
val executable = runBlockingMaybeCancellable {
pyTool.getExecutableWithBaseArgs(moduleOrProject, executableName)
}
val (binary, baseArgs) = executable.getOr { throw ExecutionException(it.error.message) }
val cmd = binary.asGeneralCommandLine().getOr { throw ExecutionException(it.error.message) }
.withParameters(*baseArgs.toTypedArray(), *lspArguments().toTypedArray())
return cmd
}
lateinit var supportProvider: PyLspToolIntegrationProvider
private val registeredActionIds = mutableListOf<String>()
override val lspServerListener: PyLspToolDescriptorLspServerListener = PyLspToolDescriptorLspServerListener()
open inner class PyLspToolDescriptorLspServerListener : LspServerListener {
override fun serverInitialized(params: InitializeResult) {
val actionManager = ActionManager.getInstance()
val commandProvider = params.capabilities.executeCommandProvider
if (commandProvider == null) return
// workaround for IJPL-196574
commandProvider.commands.forEach { command ->
val actionId = "LSP.Command.$presentableName.$command"
if (actionManager.getAction(actionId) != null) {
// workaround for PY-86023
actionManager.unregisterAction(actionId)
}
if (command in commandDescriptions && commandDescriptions[command] == null) return@forEach
val text = "${params.serverInfo.name}: " + (commandDescriptions[command] ?: command)
val action = object : AnAction(text) {
override fun actionPerformed(e: AnActionEvent) {
val lspServerManager = LspClientManager.getInstance(project)
lspServerManager
.getClients(supportProvider::class.java)
.firstOrNull()
?.let { server ->
project.service<PyLspService>().cs.launch {
server.sendRequest {
it.workspaceService.executeCommand(ExecuteCommandParams(command, null))
}
}
}
}
}
actionManager.registerAction(actionId, action)
registeredActionIds.add(actionId)
}
}
override fun serverStopped(shutdownNormally: Boolean) {
val actionManager = ActionManager.getInstance()
registeredActionIds.forEach { actionId ->
actionManager.unregisterAction(actionId)
}
registeredActionIds.clear()
}
}
// workaround for IJPL-196574
open val commandDescriptions: Map<String, String?> = emptyMap()
override val lspCustomization: PyLspToolCustomization = PyLspToolCustomization(toolConfig, pyTool, project)
}
open class PyLspToolCustomization(
val toolConfig: PyLspToolSettings,
private val pyTool: PyTool,
private val project: Project,
) : LspCustomization() {
val Diagnostic.presentableCode: String?
get() = code?.get()?.toString()?.let { codeCustomizer(it) }
open fun quickFixMessage(text: @IntentionName String): @IntentionName String = text
open fun quickFixOptions(diagnostic: Diagnostic): List<IntentionAction> = emptyList()
override val completionCustomizer: LspCompletionCustomizer = object : LspCompletionSupport() {
override fun shouldRunCodeCompletion(parameters: CompletionParameters): Boolean =
pyTool.isEnabledOn(project) && toolConfig.completions == true
}
protected open val diagnosticsSupport: PyLspToolDiagnosticsSupport = PyLspToolDiagnosticsSupport()
// instead of using `shouldAskServerForDiagnostics` we also want to avoid `publishDiagnostics`
final override val diagnosticsCustomizer: LspDiagnosticsCustomizer
get() = if (pyTool.isEnabledOn(project) && toolConfig.inspections) diagnosticsSupport else LspDiagnosticsDisabled
override val optimizeImportsCustomizer: LspOptimizeImportsCustomizer = LspOptimizeImportsDisabled
override val inlayHintCustomizer: LspInlayHintSupport = object : LspInlayHintSupport() {
override fun shouldAskServerForInlayHints(file: VirtualFile): Boolean =
pyTool.isEnabledOn(project) && toolConfig.inlayHints == true
}
override val hoverCustomizer: LspHoverCustomizer
get() = if (pyTool.isEnabledOn(project) && toolConfig.documentation == true) LspHoverSupport() else LspHoverDisabled
override val codeLensCustomizer: LspCodeLensCustomizer = LspCodeLensDisabled
open inner class PyLspToolDiagnosticsSupport : LspDiagnosticsSupport() {
override fun getMessage(diagnostic: Diagnostic): String {
val messageLines = super.getMessage(diagnostic).split("\n")
// source is nullable
val firstLine =
messageLines[0] + " ${diagnostic.source.orEmpty()}" + diagnostic.presentableCode?.let { "($it)" }.orEmpty() // NON-NLS
if (messageLines.size == 1) return firstLine
return firstLine + "\n" + messageLines.drop(1).joinToString("\n", prefix = "\n") // NON-NLS
}
override fun getTooltip(diagnostic: Diagnostic): String {
val code = diagnostic.presentableCode
val source = diagnostic.source
val style = "color: gray"
// workaround for IJPL-196845
val codeLink = when {
diagnostic.codeDescription?.href != null && code != null -> HtmlChunk.link(diagnostic.codeDescription?.href!!, code)
code != null -> HtmlChunk.span(style).buildChildren { append(HtmlChunk.text(code)) }
else -> null
}
val suffix = when {
source != null && codeLink != null && diagnostic.codeDescription?.href != null -> {
// "source(code)" with link
HtmlChunk.span(style).buildChildren {
append(HtmlChunk.text("$source("))
} +
codeLink +
HtmlChunk.span(style).buildChildren {
append(HtmlChunk.text(")"))
}
}
source != null && code != null -> {
// "source(code)" without link
HtmlChunk.span(style).buildChildren {
append(HtmlChunk.text("$source($code)"))
}
}
source != null -> {
// "source"
HtmlChunk.span(style).buildChildren {
append(HtmlChunk.text(source))
}
}
else -> {
// "code" or null (empty)
codeLink
}
}
val tooltip = super.getTooltip(diagnostic)
// workaround for leading spaces getting removed
val fixed = tooltip.replace(Regex("\n +")) { m ->
val spaces = m.value.substring(1)
"\n" + "\u00A0".repeat(spaces.length)
}
// workaround for IJPL-196664
return buildHtml {
append(HtmlChunk.text(fixed))
if (suffix != null) {
append(HtmlChunk.nbsp() + suffix)
}
}
}
// workaround for IJPL-196573
override fun getHighlightSeverity(diagnostic: Diagnostic): HighlightSeverity? =
when (diagnostic.severity) {
DiagnosticSeverity.Hint -> HighlightSeverity.INFORMATION
else -> super.getHighlightSeverity(diagnostic)
}
// workaround for IJPL-196573
override fun getEnforcedTextAttributes(diagnostic: Diagnostic): TextAttributes? =
when (diagnostic.severity) {
DiagnosticSeverity.Information -> TextAttributes().apply {
effectType = EffectType.WAVE_UNDERSCORE
effectColor = JBColor.BLUE
}
DiagnosticSeverity.Hint -> TextAttributes().apply {
effectType = EffectType.BOLD_DOTTED_LINE
effectColor = JBColor.GRAY
}
else -> null
}
}
open inner class PyLspAction(val diagnostic: Diagnostic, val action: IntentionAction) : IntentionAction by action,
CustomizableIntentionAction,
IntentionActionWithOptions {
override fun getText(): @IntentionName String {
return quickFixMessage(action.text)
}
override fun isShowSubmenu(): Boolean {
return true
}
override fun getOptions(): List<IntentionAction> {
return quickFixOptions(diagnostic)
}
override fun asIntention(): IntentionAction {
return this
}
override fun getCombiningPolicy(): IntentionActionWithOptions.CombiningPolicy {
// ideally this would be `InspectionOptionsOnly`, but at this time it not investigated
return IntentionActionWithOptions.CombiningPolicy.IntentionOptionsOnly
}
//<editor-fold defaultstate="collapsed" desc="default method explicit delegates">
override fun generatePreview(project: Project, editor: Editor, psiFile: PsiFile): IntentionPreviewInfo {
return action.generatePreview(project, editor, psiFile)
}
override fun asModCommandAction(): ModCommandAction? {
return action.asModCommandAction()
}
override fun getElementToMakeWritable(currentFile: PsiFile): PsiElement? {
return action.getElementToMakeWritable(currentFile)
}
override fun getFileModifierForPreview(target: PsiFile): FileModifier? {
return action.getFileModifierForPreview(target)
}
override fun isDumbAware(): Boolean {
return action.isDumbAware
}
//</editor-fold>
}
open fun codeCustomizer(@Nls code: String): @Nls String = code
}
@Service(Service.Level.PROJECT)
class PyLspService(val cs: CoroutineScope)
@@ -0,0 +1,11 @@
package com.intellij.python.lsp.core.listener
import com.intellij.util.messages.Topic
interface PyLspListener {
fun onTypeSettingsChange()
companion object {
val TOPIC: Topic<PyLspListener> = Topic.create("Topic.PyLspListener", PyLspListener::class.java)
}
}
@@ -0,0 +1,135 @@
package com.intellij.python.lsp.core.type
import com.jetbrains.python.psi.PyBinaryExpression
import com.jetbrains.python.psi.PyCallExpression
import com.jetbrains.python.psi.PyCapturePattern
import com.jetbrains.python.psi.PyClassPattern
import com.jetbrains.python.psi.PyDecorator
import com.jetbrains.python.psi.PyDictLiteralExpression
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyLambdaExpression
import com.jetbrains.python.psi.PyListLiteralExpression
import com.jetbrains.python.psi.PyLiteralPattern
import com.jetbrains.python.psi.PyMappingPattern
import com.jetbrains.python.psi.PyNamedParameter
import com.jetbrains.python.psi.PyPrefixExpression
import com.jetbrains.python.psi.PyRecursiveElementVisitor
import com.jetbrains.python.psi.PyReferenceExpression
import com.jetbrains.python.psi.PySequencePattern
import com.jetbrains.python.psi.PySetCompExpression
import com.jetbrains.python.psi.PySetLiteralExpression
import com.jetbrains.python.psi.PySubscriptionExpression
import com.jetbrains.python.psi.PyTargetExpression
import com.jetbrains.python.psi.PyTypedElement
import com.jetbrains.python.psi.PyWildcardPattern
import com.jetbrains.python.psi.PyYieldExpression
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
class LspCollectSupportedTypesVisitor: PyRecursiveElementVisitor() {
val result: MutableList<PyTypedElement> = mutableListOf<PyTypedElement>()
override fun visitPyMappingPattern(node: PyMappingPattern) {
result += node
super.visitPyMappingPattern(node)
}
override fun visitPySequencePattern(node: PySequencePattern) {
result += node
super.visitPySequencePattern(node)
}
override fun visitPyBinaryExpression(node: PyBinaryExpression) {
result += node
super.visitPyBinaryExpression(node)
}
override fun visitPyCallExpression(node: PyCallExpression) {
result += node
super.visitPyCallExpression(node)
}
override fun visitPyCapturePattern(node: PyCapturePattern) {
result += node
super.visitPyCapturePattern(node)
}
override fun visitPyClassPattern(node: PyClassPattern) {
result += node
super.visitPyClassPattern(node)
}
override fun visitPyDecorator(node: PyDecorator) {
result += node
super.visitPyDecorator(node)
}
override fun visitPyDictLiteralExpression(node: PyDictLiteralExpression) {
result += node
super.visitPyDictLiteralExpression(node)
}
override fun visitPyFunction(node: PyFunction) {
result += node
super.visitPyFunction(node)
}
override fun visitPyLambdaExpression(node: PyLambdaExpression) {
result += node
super.visitPyLambdaExpression(node)
}
override fun visitPyListLiteralExpression(node: PyListLiteralExpression) {
result += node
super.visitPyListLiteralExpression(node)
}
override fun visitPyLiteralPattern(node: PyLiteralPattern) {
result += node
super.visitPyLiteralPattern(node)
}
override fun visitPyNamedParameter(node: PyNamedParameter) {
result += node
super.visitPyNamedParameter(node)
}
override fun visitPyPrefixExpression(node: PyPrefixExpression) {
result += node
super.visitPyPrefixExpression(node)
}
override fun visitPyReferenceExpression(node: PyReferenceExpression) {
result += node
super.visitPyReferenceExpression(node)
}
override fun visitPySetCompExpression(node: PySetCompExpression) {
result += node
super.visitPySetCompExpression(node)
}
override fun visitPySetLiteralExpression(node: PySetLiteralExpression) {
result += node
super.visitPySetLiteralExpression(node)
}
override fun visitPySubscriptionExpression(node: PySubscriptionExpression) {
result += node
super.visitPySubscriptionExpression(node)
}
override fun visitPyTargetExpression(node: PyTargetExpression) {
result += node
super.visitPyTargetExpression(node)
}
override fun visitWildcardPattern(node: PyWildcardPattern) {
result += node
super.visitWildcardPattern(node)
}
override fun visitPyYieldExpression(node: PyYieldExpression) {
result += node
super.visitPyYieldExpression(node)
}
}
@@ -0,0 +1,123 @@
package com.intellij.python.lsp.core.type
import com.jetbrains.python.psi.PyBinaryExpression
import com.jetbrains.python.psi.PyCallExpression
import com.jetbrains.python.psi.PyCapturePattern
import com.jetbrains.python.psi.PyClassPattern
import com.jetbrains.python.psi.PyDecorator
import com.jetbrains.python.psi.PyDictLiteralExpression
import com.jetbrains.python.psi.PyElementVisitor
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyGeneratorExpression
import com.jetbrains.python.psi.PyLambdaExpression
import com.jetbrains.python.psi.PyListLiteralExpression
import com.jetbrains.python.psi.PyLiteralPattern
import com.jetbrains.python.psi.PyMappingPattern
import com.jetbrains.python.psi.PyNamedParameter
import com.jetbrains.python.psi.PyPrefixExpression
import com.jetbrains.python.psi.PyReferenceExpression
import com.jetbrains.python.psi.PySequencePattern
import com.jetbrains.python.psi.PySetCompExpression
import com.jetbrains.python.psi.PySetLiteralExpression
import com.jetbrains.python.psi.PySubscriptionExpression
import com.jetbrains.python.psi.PyTargetExpression
import com.jetbrains.python.psi.PyTupleExpression
import com.jetbrains.python.psi.PyWildcardPattern
import com.jetbrains.python.psi.PyYieldExpression
class LspIsSupportedTypesVisitor : PyElementVisitor() {
var isSupported = false
private set
override fun visitPyMappingPattern(node: PyMappingPattern) {
isSupported = true
}
override fun visitPySequencePattern(node: PySequencePattern) {
isSupported = true
}
override fun visitPyBinaryExpression(node: PyBinaryExpression) {
isSupported = true
}
override fun visitPyCallExpression(node: PyCallExpression) {
isSupported = true
}
override fun visitPyCapturePattern(node: PyCapturePattern) {
isSupported = true
}
override fun visitPyClassPattern(node: PyClassPattern) {
isSupported = true
}
override fun visitPyDecorator(node: PyDecorator) {
isSupported = true
}
override fun visitPyDictLiteralExpression(node: PyDictLiteralExpression) {
isSupported = true
}
override fun visitPyFunction(node: PyFunction) {
isSupported = true
}
override fun visitPyLambdaExpression(node: PyLambdaExpression) {
isSupported = true
}
override fun visitPyListLiteralExpression(node: PyListLiteralExpression) {
isSupported = true
}
override fun visitPyLiteralPattern(node: PyLiteralPattern) {
isSupported = true
}
override fun visitPyNamedParameter(node: PyNamedParameter) {
isSupported = true
}
override fun visitPyTupleExpression(node: PyTupleExpression) {
isSupported = true
}
override fun visitPyPrefixExpression(node: PyPrefixExpression) {
isSupported = true
}
override fun visitPyReferenceExpression(node: PyReferenceExpression) {
isSupported = true
}
override fun visitPySetCompExpression(node: PySetCompExpression) {
isSupported = true
}
override fun visitPySetLiteralExpression(node: PySetLiteralExpression) {
isSupported = true
}
override fun visitPySubscriptionExpression(node: PySubscriptionExpression) {
isSupported = true
}
override fun visitPyTargetExpression(node: PyTargetExpression) {
isSupported = true
}
override fun visitWildcardPattern(node: PyWildcardPattern) {
isSupported = true
}
override fun visitPyYieldExpression(node: PyYieldExpression) {
isSupported = true
}
override fun visitPyGeneratorExpression(node: PyGeneratorExpression) {
isSupported = true
}
}
@@ -0,0 +1,19 @@
package com.intellij.python.lsp.core.type
import com.jetbrains.python.psi.PyExpressionCodeFragment
import com.jetbrains.python.psi.PyTypedElement
import com.jetbrains.python.psi.types.engine.PyTypeEngine
import org.jetbrains.annotations.ApiStatus
@ApiStatus.Internal
interface LspPyTypeResolver : PyTypeEngine {
override fun isSupportedForResolve(pyTypedElement: PyTypedElement): Boolean {
val realFile = pyTypedElement.containingFile?.originalFile ?: return false
if (realFile is PyExpressionCodeFragment)
return false
val isSupportedTypesVisitor = LspIsSupportedTypesVisitor()
pyTypedElement.accept(isSupportedTypesVisitor)
return isSupportedTypesVisitor.isSupported
}
}
@@ -0,0 +1,110 @@
package com.intellij.python.lsp.core.type
import com.google.common.util.concurrent.Striped
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.Ref
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.jetbrains.python.psi.PyTypedElement
import com.jetbrains.python.psi.types.PyType
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Unmodifiable
import java.util.WeakHashMap
import java.util.concurrent.ConcurrentSkipListMap
import java.util.concurrent.ConcurrentSkipListSet
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.withLock
@ApiStatus.Internal
abstract class LspTypeEvalContext(val psiFile: PsiFile) {
/**
* Context should be available only for the same timestamp of the file.
*/
private val psiFileTimestamp: Long = psiFile.modificationStamp
private val stringTypeLocks = Striped.lock(64)
private val requestedTypesCount = AtomicInteger(0)
private val isFullLoaded: AtomicBoolean = AtomicBoolean(false)
private val cacheElementToStringType: WeakHashMap<PyTypedElement, String?> = WeakHashMap()
private val cacheStringTypeToType: ConcurrentSkipListMap<String, Ref<PyType?>> = ConcurrentSkipListMap()
private val unresolvedSet: ConcurrentSkipListSet<String> = ConcurrentSkipListSet()
protected abstract fun requestTypes(pyTypedElements: @Unmodifiable Collection<PyTypedElement>): List<String?>?
open fun provideType(element: PyTypedElement, isUserInitiated: Boolean): Ref<PyType?>? {
val stringType = cacheElementToStringType[element] ?: resolveTypeByLsp(element, isUserInitiated)
if (stringType == null)
return null
return stringTypeLocks.get(stringType).withLock {
if (stringType in unresolvedSet) {
return@withLock null
}
val cachedResolved = cacheStringTypeToType[stringType]
if (cachedResolved != null) {
return@withLock cachedResolved
}
val resolvedType = resolveStringType(element, stringType)
if (resolvedType != null) {
cacheStringTypeToType[stringType] = resolvedType
}
else {
unresolvedSet.add(stringType)
}
return@withLock resolvedType
}
}
protected abstract fun resolveStringType(element: PyTypedElement, stringType: String): Ref<PyType?>?
private fun resolveTypeByLsp(element: PyTypedElement, isUserInitiated: Boolean): String? =
if (!isUserInitiated && requestedTypesCount.incrementAndGet() >= FULL_LOAD_THRESHOLD && !ApplicationManager.getApplication().isDispatchThread) {
synchronized(this@LspTypeEvalContext) {
loadAllTypes(element)
}
cacheElementToStringType[element]
}
else {
synchronized(element) {
loadSingleType(element)
}
}
private fun loadAllTypes(element: PyTypedElement) {
if (isFullLoaded.get())
return
val requestedTypes = collectElementsForCalculation(element) - cacheElementToStringType.keys
if (requestedTypes.isEmpty()) return
val contents = requestTypes(requestedTypes)
if (contents == null) {
thisLogger().warn("Failed to load all types for ${psiFile.name} Current stamp ${psiFile.modificationStamp} calculating: ${psiFileTimestamp}")
return
}
cacheElementToStringType.putAll(requestedTypes zip contents)
isFullLoaded.set(true)
}
private fun loadSingleType(typedElement: PyTypedElement): String? {
val types = requestTypes(listOf(typedElement))
if (types.isNullOrEmpty()) return null
val resolvedType = types.single() ?: ""
cacheElementToStringType[typedElement] = resolvedType
return resolvedType
}
companion object {
private const val FULL_LOAD_THRESHOLD: Int = 10
fun collectElementsForCalculation(element: PsiElement): List<PyTypedElement> {
val psiFile = element.containingFile ?: return emptyList()
val visitor = LspCollectSupportedTypesVisitor()
psiFile.acceptChildren(visitor)
return visitor.result
}
}
}
@@ -0,0 +1,36 @@
package com.intellij.python.lsp.core.type
import com.intellij.openapi.util.Ref
import com.intellij.psi.impl.source.resolve.FileContextUtil
import com.intellij.psi.util.QualifiedName
import com.jetbrains.python.codeInsight.typeRepresentation.psi.PyTypeRepresentationFile
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.psi.PyTypedElement
import com.jetbrains.python.psi.impl.PyBuiltinCache
import com.jetbrains.python.psi.types.PyType
import com.jetbrains.python.psi.types.TypeEvalContext
object PyStringTypeResolver {
fun resolvePyType(anchor: PyTypedElement, stringType: String): Ref<PyType?>? {
if (stringType.isBlank())
return null
val codeInsightFallback = TypeEvalContext.externalContext(anchor.project)
val qualifiedName = QualifiedName.fromDottedString(stringType)
// Shortcut for builtins classes
if (qualifiedName.matchesPrefix(QualifiedName.fromComponents("builtins"))) {
val builtinClassName = qualifiedName.removeHead(1)
if (builtinClassName.componentCount == 1) {
val builtinClass = PyBuiltinCache.getInstance(anchor).getObjectType(builtinClassName.toString())
if (builtinClass != null) {
return Ref(builtinClass)
}
}
}
val file = FileContextUtil.getContextFile(anchor) ?: return null
val typeExpression = PyTypeRepresentationFile(stringType, file).type ?: return null
return PyTypingTypeProvider.getType(typeExpression, codeInsightFallback, true)
}
}
@@ -0,0 +1,24 @@
package com.intellij.python.lsp.core.typeEngine
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.project.Project
import com.intellij.python.lsp.core.PyLspCoreBundle
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.RowsRange
internal class BuiltInPyTypeEngineProvider : PyTypeEngineProvider {
override val pyTypeEngineType: PyTypeEngineType
get() = PyTypeEngineType.PYCHARM
override fun updateLspServers(project: Project) {}
override fun isSupported(project: Project): Boolean = true
override fun Panel.createConfigurableContent(project: Project, propertyGraph: PropertyGraph): RowsRange {
return rowsRange {
row {
comment(PyLspCoreBundle.message("pycharm.description"))
}
}
}
}
@@ -0,0 +1,75 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.lsp.core.typeEngine
import com.intellij.icons.AllIcons
import com.intellij.openapi.components.service
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.observable.util.transform
import com.intellij.openapi.options.SearchableConfigurable
import com.intellij.openapi.options.UiDslUnnamedConfigurable
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.python.lsp.core.PyLspCoreBundle
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.toMutableProperty
import com.intellij.ui.dsl.builder.toNullableProperty
import com.jetbrains.python.psi.types.PyTypeEngineSettingsModificationTracker
class PyTypeEngineConfigurable(
private val project: Project,
) : UiDslUnnamedConfigurable.Simple(), SearchableConfigurable {
private val settings: PyTypeEngineProjectSettings
get() = project.service<PyTypeEngineProjectSettings>()
private val propertyGraph = PropertyGraph()
private val selectedTypeEngine = propertyGraph.property(PyTypeEngineType.PYCHARM)
private val availableOptions: List<PyTypeEngineType>
get() = PyTypeEngineProvider.getSupportedTypes(project)
private var previousTypeEngine: PyTypeEngineType = settings.typeEngine
init {
val initialOption = settings.typeEngine
selectedTypeEngine.set(initialOption)
}
override fun getId(): String = "pycharm.type.engine"
override fun getDisplayName(): String = PyLspCoreBundle.message("display.name")
override fun getDisplayNameFast(): String = PyLspCoreBundle.message("display.name")
override fun getHelpTopic(): String = "reference.settings.python.type.engine"
override fun Panel.createContent() {
val isSingleModule = project.modules.size == 1
if (!isSingleModule) {
row {
icon(AllIcons.General.Information).commentRight(PyLspCoreBundle.message("comment.multimodule.not.warning"))
}
return
}
row(PyLspCoreBundle.message("engine.label")) {
segmentedButton(availableOptions) { text = it.displayName }
.bind(settings::typeEngine.toMutableProperty().toNullableProperty())
.whenItemSelected { selectedTypeEngine.set(it) }
}
PyTypeEngineProvider.EP.extensionsIfPointIsRegistered.filter { it.isSupported(project) }.forEach { provider ->
provider.apply {
val isVisible = selectedTypeEngine.transform { it == provider.pyTypeEngineType }
createConfigurableContent(project, propertyGraph).visibleIf(isVisible)
}
}
onApply {
val newEngine = settings.typeEngine
if (newEngine != previousTypeEngine) {
PyTypeEngineUsageCollector.logEngineChanged(project, newEngine)
previousTypeEngine = newEngine
}
// Invalidate TypeEvalContext cache so that editor errors reflect the new type engine settings
PyTypeEngineSettingsModificationTracker.getInstance(project).incModificationCount()
}
}
}
@@ -0,0 +1,39 @@
package com.intellij.python.lsp.core.typeEngine
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.python.lsp.core.listener.PyLspListener
import com.intellij.util.xmlb.XmlSerializerUtil
@Service(Service.Level.PROJECT)
@State(name = "PyTypeEngineProjectSettings", storages = [Storage("pyTypeEngine.xml")])
class PyTypeEngineProjectSettings(val project: Project) : PersistentStateComponent<PyTypeEngineSettingsState> {
private var state: PyTypeEngineSettingsState = PyTypeEngineSettingsState()
var typeEngine: PyTypeEngineType
get() = state.typeEngine
set(value) {
if (value == state.typeEngine) return
state.typeEngine = value
project.messageBus.syncPublisher(PyLspListener.TOPIC).onTypeSettingsChange()
}
override fun getState(): PyTypeEngineSettingsState = state
override fun loadState(state: PyTypeEngineSettingsState): Unit = XmlSerializerUtil.copyBean(state, this.state)
companion object {
fun getInstance(project: Project): PyTypeEngineProjectSettings = project.service<PyTypeEngineProjectSettings>()
}
}
data class PyTypeEngineSettingsState(
var typeEngine: PyTypeEngineType = PyTypeEngineType.PYCHARM,
)
@@ -0,0 +1,34 @@
package com.intellij.python.lsp.core.typeEngine
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.project.Project
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.RowsRange
interface PyTypeEngineProvider {
val pyTypeEngineType: PyTypeEngineType
fun isSupported(project: Project): Boolean
fun updateLspServers(project: Project)
fun Panel.createConfigurableContent(project: Project, propertyGraph: PropertyGraph): RowsRange
companion object {
internal val EP = ExtensionPointName.create<PyTypeEngineProvider>("com.intellij.python.lsp.typeEngineConfigurable")
fun getSupportedTypes(project: Project): List<PyTypeEngineType> {
return EP.extensionsIfPointIsRegistered.mapNotNull {
if (!it.isSupported(project)) return@mapNotNull null
return@mapNotNull it.pyTypeEngineType
}
}
fun updateLspServers(project: Project) {
EP.extensionsIfPointIsRegistered.forEach {
it.updateLspServers(project)
}
}
}
}
@@ -0,0 +1,10 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.lsp.core.typeEngine
import org.jetbrains.annotations.Nls
enum class PyTypeEngineType(val displayName: @Nls String, val packageName: String) {
PYCHARM("Built-in", ""),
PYREFLY("Pyrefly", "pyrefly"),
TY("ty", "ty");
}
@@ -0,0 +1,44 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.lsp.core.typeEngine
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.openapi.project.Project
object PyTypeEngineUsageCollector : CounterUsagesCollector() {
private val GROUP = EventLogGroup("python.type.engine", 2)
// Event fields
private val TYPE_ENGINE = EventFields.Enum<PyTypeEngineType>("type_engine")
// Events
private val ENGINE_CHANGED = GROUP.registerVarargEvent(
"engine.changed",
TYPE_ENGINE,
)
private val SETTINGS_OPENED = GROUP.registerEvent("settings.opened")
private val STATUS_BAR_WIDGET_CLICKED = GROUP.registerEvent("statusbar.widget.clicked")
override fun getGroup(): EventLogGroup = GROUP
fun logEngineChanged(
project: Project?,
newEngine: PyTypeEngineType,
) {
ENGINE_CHANGED.log(
project,
TYPE_ENGINE.with(newEngine),
)
}
fun logSettingsOpened(project: Project?) {
SETTINGS_OPENED.log(project)
}
fun logStatusBarWidgetClicked(project: Project?) {
STATUS_BAR_WIDGET_CLICKED.log(project)
}
}
@@ -0,0 +1,21 @@
package com.intellij.python.lsp.core.typeEngine
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.util.registry.Registry
import com.jetbrains.python.sdk.isReadOnly
import com.jetbrains.python.sdk.pythonSdk
import com.jetbrains.python.statistics.InterpreterTarget
import com.jetbrains.python.statistics.executionType
object PyTypeEngineUtils {
fun isExternalTypeEngineSupported(project: Project): Boolean {
if (!Registry.`is`("pycharm.type.engine", true))
return false
val module = project.modules.singleOrNull() ?: return false
val pythonSdk = module.pythonSdk ?: return false
return !pythonSdk.isReadOnly && pythonSdk.executionType == InterpreterTarget.LOCAL
}
}
@@ -0,0 +1,13 @@
package com.intellij.python.lsp.core.utils
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.SimpleModificationTracker
@Service(Service.Level.PROJECT)
class PyLspServerModificationTracker: SimpleModificationTracker() {
companion object {
fun getInstance(project: Project): PyLspServerModificationTracker = project.service()
}
}
+82
View File
@@ -0,0 +1,82 @@
load("@rules_jvm//:jvm.bzl", "jvm_library")
### auto-generated section `build intellij.python.lsp.impl` start
jvm_library(
name = "lsp-impl",
srcs = glob(
[
"src/**/*.kt",
"src/**/*.java",
"src/**/*.form",
],
allow_empty = True,
),
module_name = "intellij.python.lsp.impl",
resource_strip_prefix = "resources",
resources = glob(
["resources/**/*"],
),
visibility = ["//visibility:public"],
deps = [
# do not sort,
"@lib//:kotlin-stdlib",
"//platform/core-api:core",
"//platform/platform-api:ide",
"//platform/ide-core",
"//platform/platform-impl:ide-impl",
"//platform/editor-ui-api:editor-ui",
"//platform/projectModel-api:projectModel",
"//platform/lsp",
"//python/python-pytools:pytools",
"//python:python-community-impl",
"//python/python-sdk:sdk",
"//python/python-lsp-core:lsp-core",
"//python/python-ruff:ruff",
"//python/python-pyright:pyright",
"//python/python-pyrefly:pyrefly",
"//python/python-ty:ty",
"//python/python-pytools-ui:pytools-ui",
],
)
jvm_library(
name = "lsp-impl_test_lib",
testonly = True,
srcs = glob(
[],
allow_empty = True,
),
module_name = "intellij.python.lsp.impl",
visibility = ["//visibility:public"],
runtime_deps = [
# do not sort,
":lsp-impl",
"//platform/core-api:core_test_lib",
"//platform/platform-api:ide_test_lib",
"//platform/ide-core:ide-core_test_lib",
"//platform/platform-impl:ide-impl_test_lib",
"//platform/editor-ui-api:editor-ui_test_lib",
"//platform/projectModel-api:projectModel_test_lib",
"//platform/lsp:lsp_test_lib",
"//python/python-pytools:pytools_test_lib",
"//python:python-community-impl_test_lib",
"//python/python-sdk:sdk_test_lib",
"//python/python-lsp-core:lsp-core_test_lib",
"//python/python-ruff:ruff_test_lib",
"//python/python-pyright:pyright_test_lib",
"//python/python-pyrefly:pyrefly_test_lib",
"//python/python-ty:ty_test_lib",
"//python/python-pytools-ui:pytools-ui_test_lib",
],
)
### auto-generated section `build intellij.python.lsp.impl` end
### auto-generated section `iml intellij.python.lsp.impl` start
exports_files(
[
"intellij.python.lsp.impl.iml",
],
visibility = ["//visibility:public"],
)
### auto-generated section `iml intellij.python.lsp.impl` end
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.lsp" />
<orderEntry type="module" module-name="intellij.python.pytools" />
<orderEntry type="module" module-name="intellij.python.community.impl" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.python.lsp.core" />
<orderEntry type="module" module-name="intellij.python.ruff" />
<orderEntry type="module" module-name="intellij.python.pyright" />
<orderEntry type="module" module-name="intellij.python.pyrefly" />
<orderEntry type="module" module-name="intellij.python.ty" />
<orderEntry type="module" module-name="intellij.python.pytools.ui" />
</component>
</module>
@@ -0,0 +1,19 @@
<idea-plugin>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<module name="intellij.python.community.impl"/>
<module name="intellij.python.lsp.core"/>
<module name="intellij.python.pyrefly"/>
<module name="intellij.python.pyright"/>
<module name="intellij.python.pytools"/>
<module name="intellij.python.pytools.ui"/>
<module name="intellij.python.ruff"/>
<module name="intellij.python.sdk"/>
<module name="intellij.python.ty"/>
</dependencies>
<!-- endregion -->
<extensions defaultExtensionNs="com.intellij">
<postStartupActivity implementation="com.intellij.python.lsp.impl.PyLspToolAdvertiserStartupActivity"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,199 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.lsp.impl
import com.intellij.ide.util.PropertiesComponent
import com.intellij.notification.NotificationAction
import com.intellij.notification.NotificationGroupManager
import com.intellij.notification.NotificationType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.modules
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.python.pyright.BasedpyrightPyTool
import com.intellij.python.pyright.PyrightPyTool
import com.intellij.python.pytools.PyTool
import com.intellij.python.pytools.PyToolsState
import com.intellij.python.pytools.lsp.PyLspTool
import com.intellij.python.pytools.ui.configuration.PyExternalToolsConfigurable
import com.intellij.python.ruff.RuffPyTool
import com.intellij.python.ty.TyPyTool
import com.jetbrains.python.PyBundle
import com.jetbrains.python.packaging.common.PythonPackageManagementListener
import com.jetbrains.python.packaging.management.PythonPackageManager
import com.jetbrains.python.sdk.findPythonSdk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.annotations.ApiStatus
import kotlin.time.Duration.Companion.seconds
private const val NOTIFICATION_GROUP_ID = "Python LSP Tools"
private const val DONT_ASK_PROPERTY_PREFIX = "python.lsp.tool.dont.ask."
private fun lspTools(): List<PyLspTool<*>> = listOf(
RuffPyTool.getInstance(),
//PyreflyPyTool.getInstance(),
BasedpyrightPyTool.getInstance(),
PyrightPyTool.getInstance(),
TyPyTool.getInstance(),
)
/**
* Service that detects LSP tools installed in the user's virtual environment
* and shows a balloon notification offering to enable them.
*
* Also listens for package installation events to suggest enabling tools when installed.
*/
@ApiStatus.Internal
@Service(Service.Level.PROJECT)
class PyLspToolAdvertiserService(private val project: Project, private val cs: CoroutineScope) {
private val shownForTools = mutableSetOf<String>()
init {
// Subscribe to package management events to detect when LSP tools are installed
project.messageBus.connect(cs).subscribe(
PythonPackageManager.PACKAGE_MANAGEMENT_TOPIC,
object : PythonPackageManagementListener {
override fun packagesChanged(sdk: Sdk) {
thisLogger().debug("Packages changed for SDK '${sdk.name}', triggering LSP tool check")
// Defer until no modal dialog is open. If the user installs a tool from within
// the settings dialog (modal), the settings haven't been applied yet at this point,
// so isEnabled() would return false and trigger a spurious notification. Scheduling
// with ModalityState.nonModal() means the check runs only after the dialog is
// closed and settings are applied, so isEnabled() reflects the user's choice.
ApplicationManager.getApplication().invokeLater({
cs.launch { checkAndAdvertise() }
}, ModalityState.nonModal())
}
}
)
}
/**
* Checks for installed LSP tools and shows notifications for any that are found
* but not yet enabled.
*/
suspend fun checkAndAdvertise() {
val installedPackages = getInstalledPackages()
val pyToolsState = PyToolsState.getInstance(project)
val toolsToAdvertise = lspTools().mapNotNull { tool ->
val isInstalled = tool.packageName.name in installedPackages
val name = tool.presentableName
val isEnabled = pyToolsState.isEnabled(tool)
if (isInstalled && (isEnabled || isDontAskSet(tool))) {
thisLogger().debug("LSP tool '$name': installed=true, enabled=$isEnabled, dontAsk=${isDontAskSet(tool)}")
}
// shownForTools (keyed on the stable presentable name) suppresses repeat notifications when
// checkAndAdvertise re-runs on package-change events.
if (!isInstalled || isEnabled || isDontAskSet(tool) || name in shownForTools) {
return@mapNotNull null
}
tool
}
if (toolsToAdvertise.size > 1) {
for (tool in toolsToAdvertise) {
thisLogger().info("LSP tool '${tool.presentableName}' installed but not enabled")
shownForTools.add(tool.presentableName)
}
showMultipleToolsNotification(toolsToAdvertise)
}
else if (toolsToAdvertise.size == 1) {
val tool = toolsToAdvertise.single()
thisLogger().info("LSP tool '${tool.presentableName}' installed but not enabled, showing notification")
shownForTools.add(tool.presentableName)
showNotification(tool)
}
}
private suspend fun getInstalledPackages(): Set<String> = project.modules
.mapNotNull { it.findPythonSdk() }
.distinct()
.also { sdks -> thisLogger().debug("Scanning ${sdks.size} SDK(s) for installed packages") }
.flatMapTo(mutableSetOf()) { sdk ->
PythonPackageManager
.forSdk(project, sdk)
.listInstalledPackages()
.map { it.name.lowercase() }
}
private fun showMultipleToolsNotification(tools: List<PyLspTool<*>>) {
val toolNames = tools.dropLast(1).joinToString { it.presentableName } +
" and " + tools.last().presentableName
thisLogger().info("Notifying user about multiple LSP tools: $toolNames")
val notification = NotificationGroupManager.getInstance()
.getNotificationGroup(NOTIFICATION_GROUP_ID)
.createNotification(
PyBundle.message("lsp.tool.advertiser.multiple.title"),
PyBundle.message("lsp.tool.advertiser.multiple.message", toolNames),
NotificationType.INFORMATION
)
.addAction(NotificationAction.createSimpleExpiring(PyBundle.message("lsp.tool.advertiser.settings")) {
ShowSettingsUtil.getInstance().showSettingsDialog(project, PyExternalToolsConfigurable::class.java)
})
.addAction(NotificationAction.createSimpleExpiring(PyBundle.message("lsp.tool.advertiser.ignore")) {
for (tool in tools) {
setDontAsk(tool)
}
})
notification.notify(project)
}
private fun showNotification(tool: PyLspTool<*>) {
val name = tool.presentableName
thisLogger().info("Notifying user about LSP tool: $name")
val notification = NotificationGroupManager.getInstance()
.getNotificationGroup(NOTIFICATION_GROUP_ID)
.createNotification(
PyBundle.message("lsp.tool.advertiser.title", name),
PyBundle.message("lsp.tool.advertiser.message", name),
NotificationType.INFORMATION
)
.setIcon(tool.icon)
.addAction(NotificationAction.createSimpleExpiring(PyBundle.message("lsp.tool.advertiser.yes")) {
// Mirror the External Tools page's "Enable" toggle: flip the framework-level state and
// run the tool's lifecycle hook (which starts the LSP server).
PyToolsState.getInstance(project).setEnabled(tool, true)
tool.onEnabledChanged(project, true)
})
.addAction(NotificationAction.createSimpleExpiring(PyBundle.message("lsp.tool.advertiser.no")) {
setDontAsk(tool)
})
.addAction(NotificationAction.createSimpleExpiring(PyBundle.message("lsp.tool.advertiser.settings")) {
ShowSettingsUtil.getInstance().showSettingsDialog(project, PyExternalToolsConfigurable::class.java)
})
notification.notify(project)
}
private fun isDontAskSet(tool: PyTool): Boolean {
return PropertiesComponent.getInstance(project).getBoolean(DONT_ASK_PROPERTY_PREFIX + tool.presentableName, false)
}
private fun setDontAsk(tool: PyTool) {
thisLogger().debug("Setting don't-ask for LSP tool: ${tool.presentableName}")
PropertiesComponent.getInstance(project).setValue(DONT_ASK_PROPERTY_PREFIX + tool.presentableName, true)
}
companion object {
fun getInstance(project: Project): PyLspToolAdvertiserService =
project.service<PyLspToolAdvertiserService>()
}
}
class PyLspToolAdvertiserStartupActivity : ProjectActivity {
override suspend fun execute(project: Project) {
thisLogger().debug("PyLspToolAdvertiserStartupActivity: waiting 15s before first check")
delay(15.seconds)
PyLspToolAdvertiserService.getInstance(project).checkAndAdvertise()
}
}
+111
View File
@@ -0,0 +1,111 @@
load("@rules_jvm//:jvm.bzl", "jvm_library")
### auto-generated section `build intellij.python.pyrefly` start
jvm_library(
name = "pyrefly",
srcs = glob(
[
"src/**/*.kt",
"src/**/*.java",
"src/**/*.form",
"gen/**/*.kt",
"gen/**/*.java",
],
allow_empty = True,
),
module_name = "intellij.python.pyrefly",
resource_strip_prefix = "resources",
resources = glob(
["resources/**/*"],
),
visibility = ["//visibility:public"],
deps = [
# do not sort,
"//platform/editor-ui-api:editor-ui",
"//platform/lang-impl",
"//python:python-community-impl",
"//python/python-sdk:sdk",
"//python/python-core-impl:community-core-impl",
"//platform/code-style-impl:codeStyle-impl",
"//platform/editor-ui-ex:editor-ex",
"//platform/platform-impl:ide-impl",
"//platform/ide-core",
"//xml/xml-parser:parser",
"//platform/core-ui",
"//xml/impl",
"//platform/util/jdom",
"//platform/smRunner",
"//libraries/guava",
"//libraries/fastutil",
"//platform/lsp",
"//platform/core-api:core",
"@lib//:eclipse-lsp4j",
"@lib//:eclipse-lsp4j-jsonrpc",
"//platform/projectModel-api:projectModel",
"//python/python-lsp-core:lsp-core",
"//platform/core-impl",
"//platform/lsp-impl",
"//platform/statistics",
"//platform/observable",
"//platform/util:util-ui",
"//python/python-pytools:pytools",
"//python/python-pytools-ui:pytools-ui",
],
)
jvm_library(
name = "pyrefly_test_lib",
testonly = True,
srcs = glob(
[],
allow_empty = True,
),
module_name = "intellij.python.pyrefly",
visibility = ["//visibility:public"],
runtime_deps = [
# do not sort,
":pyrefly",
"//platform/editor-ui-api:editor-ui_test_lib",
"//platform/lang-impl:lang-impl_test_lib",
"//python:python-community-impl_test_lib",
"//python/python-sdk:sdk_test_lib",
"//python/python-core-impl:community-core-impl_test_lib",
"//platform/code-style-impl:codeStyle-impl_test_lib",
"//platform/editor-ui-ex:editor-ex_test_lib",
"//platform/platform-impl:ide-impl_test_lib",
"//platform/ide-core:ide-core_test_lib",
"//xml/xml-parser:parser_test_lib",
"//platform/core-ui:core-ui_test_lib",
"//xml/impl:impl_test_lib",
"//platform/util/jdom:jdom_test_lib",
"//platform/smRunner:smRunner_test_lib",
"//libraries/guava:guava_test_lib",
"//libraries/fastutil:fastutil_test_lib",
"//platform/lsp:lsp_test_lib",
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//platform/lsp/testFramework",
"//platform/lsp/testFramework:testFramework_test_lib",
"//platform/core-api:core_test_lib",
"//platform/projectModel-api:projectModel_test_lib",
"//python/python-lsp-core:lsp-core_test_lib",
"//platform/core-impl:core-impl_test_lib",
"//platform/lsp-impl:lsp-impl_test_lib",
"//platform/statistics:statistics_test_lib",
"//platform/observable:observable_test_lib",
"//platform/util:util-ui_test_lib",
"//python/python-pytools:pytools_test_lib",
"//python/python-pytools-ui:pytools-ui_test_lib",
],
)
### auto-generated section `build intellij.python.pyrefly` end
### auto-generated section `iml intellij.python.pyrefly` start
exports_files(
[
"intellij.python.pyrefly.iml",
],
visibility = ["//visibility:public"],
)
### auto-generated section `iml intellij.python.pyrefly` end
+2
View File
@@ -0,0 +1,2 @@
owner 'PyCharm Codeinsight'
@@ -0,0 +1,17 @@
package com.intellij.python.pyrefly.icons;
import com.intellij.ui.IconManager;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
/**
* NOTE THIS FILE IS AUTO-GENERATED
* DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead
*/
public final class PythonPyreflyIcons {
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, PythonPyreflyIcons.class.getClassLoader(), cacheKey, flags);
}
/** 16x16 */ public static final @NotNull Icon Pyrefly = load("icons/pyrefly.svg", -1308406599, 2);
}
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.python.community.impl" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.python.community.core.impl" />
<orderEntry type="module" module-name="intellij.platform.codeStyle.impl" />
<orderEntry type="module" module-name="intellij.platform.editor.ex" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.xml.parser" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.xml.impl" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
<orderEntry type="module" module-name="intellij.platform.smRunner" />
<orderEntry type="module" module-name="intellij.libraries.guava" />
<orderEntry type="module" module-name="intellij.libraries.fastutil" />
<orderEntry type="module" module-name="intellij.platform.lsp" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.lsp.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="library" name="eclipse.lsp4j" level="project" />
<orderEntry type="library" name="eclipse.lsp4j.jsonrpc" level="project" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.python.lsp.core" />
<orderEntry type="module" module-name="intellij.platform.core.impl" />
<orderEntry type="module" module-name="intellij.platform.lsp.impl" />
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.platform.ide.observable" />
<orderEntry type="module" module-name="intellij.platform.util.ui" />
<orderEntry type="module" module-name="intellij.python.pytools" />
<orderEntry type="module" module-name="intellij.python.pytools.ui" />
</component>
</module>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 489.36 386.49"><defs><style>.cls-1{fill:url(#linear-gradient-2);}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:#fff;}</style><linearGradient id="linear-gradient" x1="244.69" y1="178.14" x2="244.69" y2="77.23" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#bdc5cf"/><stop offset="1" stop-color="#6d737a"/></linearGradient><linearGradient id="linear-gradient-2" x1="243.99" y1="356.44" x2="243.99" y2="129.44" gradientUnits="userSpaceOnUse"><stop offset=".15" stop-color="#ff4602"/><stop offset=".85" stop-color="#ff9a00"/></linearGradient></defs><path class="cls-2" d="M244.69,77.23l-169.28,82.08c62.43,38.76,112.53,9.97,141.57-17.49.02-.04.04-.06.06-.06,17.19-16.29,27-32.09,27.61-33.08.02-.04.04-.06.04-.06,0,0,.02.02.04.06.61.99,10.42,16.79,27.61,33.08.02,0,.04.02.06.06,29.04,27.46,79.14,56.25,141.57,17.49l-169.28-82.08Z"/><path class="cls-3" d="M281.4,73.52v-5.87c0-20.77-16.83-37.6-37.6-37.6s-37.6,16.83-37.6,37.6v5.48L30.98,33.84c12.79,42.15,47.26,76.4,93.32,86.73,30.47,6.82,60.85,2.02,86.46-11.49,4.4-2.31,8.67-4.88,12.77-7.7l21.13-14.16,21.13,14.16c.4.28.82.52,1.23.79,1.34.9,2.69,1.77,4.06,2.62.6.37,1.2.73,1.8,1.09,1.7,1.01,3.43,1.98,5.18,2.92.17.09.33.19.5.28h0c25.63,13.5,56.01,18.32,86.49,11.49,46.06-10.33,80.53-44.58,93.32-86.72l-176.98,39.68Z"/><path class="cls-1" d="M268.84,311.09c-4.54,5.93-7.79,9.45-8.14,9.81.29-25.86-3.51-34.7-16.86-50.02-.04-.02-.08-.02-.11-.04-24.56,42.71-16.79,82.38-16.14,85.41,0,.04.04.08.04.1.02.06.02.1.02.1-32.32-50.92-42.72-90.71-41.77-121.35.21-7.15,1.31-13.78,2.36-20.01,2.73-16.39,10.12-29.8,18.69-40.38.46-.55.93-1.11,1.39-1.68l36.36-43.58,37.66,45.13,1.62,1.94c11,13.63,16.27,27.96,17.76,42.11,4.04,38.38-19.8,75.37-32.87,92.46Z"/><path class="cls-1" d="M268.84,311.09c-4.54,5.93-7.79,9.45-8.14,9.81.29-25.86-3.51-34.7-16.86-50.02-.04-.02-.08-.02-.11-.04-24.56,42.71-16.79,82.38-16.14,85.41,0,.04.04.08.04.1.02.06.02.1.02.1-32.32-50.92-42.72-90.71-41.77-121.35.21-7.15,1.31-13.78,2.36-20.01,2.73-16.39,10.12-29.8,18.69-40.38.46-.55.93-1.11,1.39-1.68l36.36-43.58,37.66,45.13,1.62,1.94c11,13.63,16.27,27.96,17.76,42.11,4.04,38.38-19.8,75.37-32.87,92.46Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 489.36 386.49"><defs><style>.cls-1{fill:url(#linear-gradient-2);}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:#fff;}</style><linearGradient id="linear-gradient" x1="244.69" y1="178.14" x2="244.69" y2="77.23" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#bdc5cf"/><stop offset="1" stop-color="#6d737a"/></linearGradient><linearGradient id="linear-gradient-2" x1="243.99" y1="356.44" x2="243.99" y2="129.44" gradientUnits="userSpaceOnUse"><stop offset=".15" stop-color="#ff4602"/><stop offset=".85" stop-color="#ff9a00"/></linearGradient></defs><path class="cls-2" d="M244.69,77.23l-169.28,82.08c62.43,38.76,112.53,9.97,141.57-17.49.02-.04.04-.06.06-.06,17.19-16.29,27-32.09,27.61-33.08.02-.04.04-.06.04-.06,0,0,.02.02.04.06.61.99,10.42,16.79,27.61,33.08.02,0,.04.02.06.06,29.04,27.46,79.14,56.25,141.57,17.49l-169.28-82.08Z"/><path class="cls-3" d="M281.4,73.52v-5.87c0-20.77-16.83-37.6-37.6-37.6s-37.6,16.83-37.6,37.6v5.48L30.98,33.84c12.79,42.15,47.26,76.4,93.32,86.73,30.47,6.82,60.85,2.02,86.46-11.49,4.4-2.31,8.67-4.88,12.77-7.7l21.13-14.16,21.13,14.16c.4.28.82.52,1.23.79,1.34.9,2.69,1.77,4.06,2.62.6.37,1.2.73,1.8,1.09,1.7,1.01,3.43,1.98,5.18,2.92.17.09.33.19.5.28h0c25.63,13.5,56.01,18.32,86.49,11.49,46.06-10.33,80.53-44.58,93.32-86.72l-176.98,39.68Z"/><path class="cls-1" d="M268.84,311.09c-4.54,5.93-7.79,9.45-8.14,9.81.29-25.86-3.51-34.7-16.86-50.02-.04-.02-.08-.02-.11-.04-24.56,42.71-16.79,82.38-16.14,85.41,0,.04.04.08.04.1.02.06.02.1.02.1-32.32-50.92-42.72-90.71-41.77-121.35.21-7.15,1.31-13.78,2.36-20.01,2.73-16.39,10.12-29.8,18.69-40.38.46-.55.93-1.11,1.39-1.68l36.36-43.58,37.66,45.13,1.62,1.94c11,13.63,16.27,27.96,17.76,42.11,4.04,38.38-19.8,75.37-32.87,92.46Z"/><path class="cls-1" d="M268.84,311.09c-4.54,5.93-7.79,9.45-8.14,9.81.29-25.86-3.51-34.7-16.86-50.02-.04-.02-.08-.02-.11-.04-24.56,42.71-16.79,82.38-16.14,85.41,0,.04.04.08.04.1.02.06.02.1.02.1-32.32-50.92-42.72-90.71-41.77-121.35.21-7.15,1.31-13.78,2.36-20.01,2.73-16.39,10.12-29.8,18.69-40.38.46-.55.93-1.11,1.39-1.68l36.36-43.58,37.66,45.13,1.62,1.94c11,13.63,16.27,27.96,17.76,42.11,4.04,38.38-19.8,75.37-32.87,92.46Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1,32 @@
<idea-plugin visibility="internal">
<dependencies>
<module name="intellij.platform.lsp.impl"/>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.platform.smRunner"/>
<module name="intellij.python.lsp.core"/>
<module name="intellij.python.pytools"/>
<module name="intellij.python.pytools.ui"/>
<!-- endregion -->
</dependencies>
<extensions defaultExtensionNs="com.intellij">
<platform.lsp.integrationProvider implementation="com.intellij.python.pyrefly.lsp.PyreflyLspIntegrationProvider"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.python.pyrefly.PyreflyUsageCollector"/>
<registryKey key="pyrefly.type.engine" defaultValue="false"
description="Enforce pyrefly type engine"/>
<registryKey key="pyrefly.type.engine.tsp" defaultValue="false"
description="Use the Type Server Protocol (TSP) instead of LSP for the Pyrefly type engine"
restartRequired="true"/>
<python.lsp.typeEngineConfigurable implementation="com.intellij.python.pyrefly.PyreflyPyTypeEngineProvider"/>
<projectSettings service="com.intellij.python.pyrefly.PyreflyConfiguration"/>
<python.pytools.pyTool implementation="com.intellij.python.pyrefly.PyreflyPyTool"/>
</extensions>
<extensions defaultExtensionNs="Pythonid">
<typeEvalTypeEngineProvider implementation="com.intellij.python.pyrefly.typeProvider.PyreflyExternalPyTypeResolverProvider"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,2 @@
pyrefly.additional.settings.title=Other Features:
pyrefly.tool.description=A fast static type checker and language server for Python, from Meta.
@@ -0,0 +1,18 @@
package com.intellij.python.pyrefly
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
object PyreflyBundle {
private val myDynamicBundle = DynamicBundle(this::class.java, BUNDLE)
@NonNls
const val BUNDLE: String = "messages.PyreflyBundle"
@Nls
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String {
return myDynamicBundle.getMessage(key, *params)
}
}
@@ -0,0 +1,25 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.pyrefly
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.python.pytools.lsp.LSP_TOOLS_STORAGE_FILE
import com.intellij.python.pytools.lsp.PyLspToolConfiguration
import com.intellij.util.xmlb.XmlSerializerUtil
@Service(Service.Level.PROJECT)
@State(
name = "PyreflyConfiguration2",
storages = [Storage(LSP_TOOLS_STORAGE_FILE)]
)
data class PyreflyConfiguration(
override var inlayHints: Boolean? = false,
override var completions: Boolean? = false,
override var documentation: Boolean? = false,
) : PyLspToolConfiguration<PyreflyConfiguration>() {
override fun loadState(state: PyreflyConfiguration) {
XmlSerializerUtil.copyBean(state, this)
}
}
@@ -0,0 +1,12 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.pyrefly
import com.intellij.openapi.project.Project
import com.intellij.python.pytools.ui.PyLspToolDetailConfigurable
internal class PyreflyDetailConfigurable(
project: Project
) : PyLspToolDetailConfigurable<PyreflyConfiguration>(
project = project,
tool = PyreflyPyTool.getInstance()
)
@@ -0,0 +1,47 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.pyrefly
import com.intellij.openapi.components.service
import com.intellij.openapi.options.UnnamedConfigurable
import com.intellij.openapi.project.Project
import com.intellij.platform.lsp.api.LspClientManager
import com.intellij.platform.lsp.api.stopAndRestartClientsIfNeeded
import com.intellij.platform.lsp.api.stopClients
import com.intellij.python.pyrefly.lsp.PyreflyLspIntegrationProvider
import com.intellij.python.pytools.PyTool
import com.intellij.python.pytools.lsp.PyLspTool
import com.intellij.python.pytools.configuration.ConfigurablePyTool
import com.intellij.python.pytools.ui.pyLspToolFeaturesSummary
import com.jetbrains.python.packaging.PyPackageName
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon
/**
* [Pyrefly](https://pyrefly.org/) — a fast Python type checker written in Rust by Meta, providing type
* checking and IDE features through a language server.
*/
@ApiStatus.Internal
class PyreflyPyTool : PyLspTool<PyreflyConfiguration>(), ConfigurablePyTool {
override val presentableName: String = "Pyrefly"
override val description: String get() = PyreflyBundle.message("pyrefly.tool.description")
override val packageName: PyPackageName = PyPackageName.from("pyrefly")
override fun configuration(project: Project): PyreflyConfiguration = project.service<PyreflyConfiguration>()
override val icon: Icon = PyreflyUtil.getDefaultPyreflyIcon()
override fun createConfigurable(project: Project): UnnamedConfigurable = PyreflyDetailConfigurable(project)
override fun summaryFor(project: Project): String = pyLspToolFeaturesSummary(configuration(project))
override fun onEnabledChanged(project: Project, enabled: Boolean) {
val manager = LspClientManager.getInstance(project)
if (enabled) manager.stopAndRestartClientsIfNeeded<PyreflyLspIntegrationProvider>()
else manager.stopClients<PyreflyLspIntegrationProvider>()
}
@Suppress("CompanionObjectInExtension")
companion object {
fun getInstance(): PyreflyPyTool = PyTool.EP_NAME.findExtensionOrFail(PyreflyPyTool::class.java)
}
}
@@ -0,0 +1,71 @@
package com.intellij.python.pyrefly
import com.intellij.icons.AllIcons
import com.intellij.openapi.components.service
import com.intellij.openapi.observable.properties.PropertyGraph
import com.intellij.openapi.project.Project
import com.intellij.platform.lsp.api.LspClientManager
import com.intellij.platform.lsp.api.stopAndRestartClientsIfNeeded
import com.intellij.platform.lsp.api.stopClients
import com.intellij.python.lsp.core.PyLspCoreBundle
import com.intellij.python.lsp.core.typeEngine.PyTypeEngineProvider
import com.intellij.python.lsp.core.typeEngine.PyTypeEngineType
import com.intellij.python.pyrefly.lsp.PyreflyLspIntegrationProvider
import com.intellij.python.pyrefly.typeProvider.PyreflyExternalPyTypeResolverProvider
import com.intellij.python.pytools.isEnabledOn
import com.intellij.python.pytools.ui.PyToolsUiBundle
import com.intellij.python.pytools.ui.toSafeProperty
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.RightGap
import com.intellij.ui.dsl.builder.RowLayout
import com.intellij.ui.dsl.builder.RowsRange
import com.intellij.ui.dsl.builder.bindSelected
internal class PyreflyPyTypeEngineProvider : PyTypeEngineProvider {
override val pyTypeEngineType: PyTypeEngineType
get() = PyTypeEngineType.PYREFLY
override fun isSupported(project: Project): Boolean {
return PyreflyExternalPyTypeResolverProvider.Util.isAvailable(project)
}
override fun updateLspServers(project: Project) {
val lspServerManager = LspClientManager.getInstance(project)
if (shouldBeEnabled(project)) {
lspServerManager.stopAndRestartClientsIfNeeded<PyreflyLspIntegrationProvider>()
}
else {
lspServerManager.stopClients<PyreflyLspIntegrationProvider>()
}
}
private fun shouldBeEnabled(project: Project): Boolean {
return isSupported(project) && PyreflyPyTool.getInstance().isEnabledOn(project)
}
override fun Panel.createConfigurableContent(project: Project, propertyGraph: PropertyGraph): RowsRange {
val pyreflySettings = project.service<PyreflyConfiguration>()
return rowsRange {
row {
comment(PyLspCoreBundle.message("pyrefly.description")).gap(RightGap.SMALL)
icon(AllIcons.General.Beta)
}.layout(RowLayout.INDEPENDENT)
collapsibleGroup(PyreflyBundle.message("pyrefly.additional.settings.title"), indent = true) {
if (pyreflySettings.inlayHints != null) {
row("") {
checkBox(PyToolsUiBundle.message("checkbox.inlay.hints"))
.bindSelected(pyreflySettings::inlayHints.toSafeProperty())
}
}
if (pyreflySettings.documentation != null) {
row("") {
checkBox(PyToolsUiBundle.message("checkbox.documentation"))
.bindSelected(pyreflySettings::documentation.toSafeProperty())
}
}
}.expanded = false
}
}
}
@@ -0,0 +1,50 @@
package com.intellij.python.pyrefly
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.flavors.PythonSdkFlavor
import com.jetbrains.python.sdk.legacy.PythonSdkUtil
import kotlin.time.Duration
object PyreflyUsageCollector : CounterUsagesCollector() {
private val GROUP = EventLogGroup("python.pyrefly.type.inference", 7)
private val DURATION = EventFields.Int("duration_basket", "Duration of string type resolution, grouped into 100ms time buckets")
private val SUCCESS = EventFields.Boolean("success")
private val PYTHON_VERSION = EventFields.Version
private val STRING_TYPE_RESOLUTION_TIME = GROUP.registerEvent("string.type.resolution.time", DURATION)
private val PYREFLY_AUTO_INSTALLED = GROUP.registerEvent("pyrefly.auto.installed", SUCCESS)
private val SERVER_STARTUP = GROUP.registerVarargEvent("server.startup", SUCCESS, PYTHON_VERSION)
fun logStringTypeResolutionTime(duration: Duration) {
val basket = duration.inWholeMilliseconds.toInt() / 100
STRING_TYPE_RESOLUTION_TIME.log(basket)
}
fun logPyreflyAutoInstalled(success: Boolean) {
PYREFLY_AUTO_INSTALLED.log(success)
}
fun logServerStartup(success: Boolean, sdk: Sdk?) {
val fields = buildList {
add(SUCCESS with success)
sdk.pythonVersion()?.let {
add(PYTHON_VERSION with it.toPythonVersion())
}
}
SERVER_STARTUP.log(*fields.toTypedArray())
}
private fun Sdk?.pythonVersion(): LanguageLevel? {
if (this == null || !PythonSdkUtil.isPythonSdk(this)) return null
return PythonSdkFlavor.getFlavor(this)
?.getLanguageLevel(this)
?: versionString?.let(LanguageLevel::fromPythonVersion)
}
override fun getGroup(): EventLogGroup = GROUP
}
@@ -0,0 +1,11 @@
package com.intellij.python.pyrefly
import com.intellij.python.pyrefly.icons.PythonPyreflyIcons
import com.intellij.util.IconUtil
import javax.swing.Icon
object PyreflyUtil {
fun getDefaultPyreflyIcon(): Icon {
return IconUtil.resizeSquared(PythonPyreflyIcons.Pyrefly, 16)
}
}
@@ -0,0 +1,79 @@
package com.intellij.python.pyrefly.lsp
import com.intellij.platform.lsp.api.Lsp4jServer
import org.eclipse.lsp4j.MarkupContent
import org.eclipse.lsp4j.Position
import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.TextDocumentIdentifier
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
import java.util.concurrent.CompletableFuture
/**
* Pyrefly Type Server Protocol (TSP) endpoints.
*
* @see <a href="https://github.com/microsoft/pylance-release/blob/main/docs/tsp/typeServerProtocol.ts">Type Server Protocol</a>
*/
interface PyreflyLsp4jServer : Lsp4jServer {
/**
* Requests a type for a given element range.
*/
@JsonRequest("types/provide-type")
fun provideType(text_document: TextDocumentIdentifier, positions: List<Position>): CompletableFuture<ProvideTypeResponse>
@JsonRequest("typeServer/getSnapshot")
fun getSnapshot(): CompletableFuture<Int>
@JsonRequest("typeServer/getComputedType")
fun getComputedType(params: GetComputedTypeParams): CompletableFuture<TspType?>
data class ProvideTypeResponse(val contents: List<MarkupContent>)
data class TspNode(val uri: String, val range: Range)
data class GetComputedTypeParams(val arg: TspNode, val snapshot: Int)
data class TspDeclaration(
val kind: String? = null,
val name: String? = null,
val node: TspNode? = null,
val uri: String? = null,
)
data class TspSpecializedFunctionTypes(
val parameterTypes: List<TspType>? = null,
val returnType: TspType? = null,
)
data class TspType(
val kind: String? = null,
val name: String? = null,
val moduleName: String? = null,
val uri: String? = null,
val stubContent: String? = null,
val declaration: TspDeclaration? = null,
val typeArgs: List<TspType>? = null,
val subTypes: List<TspType>? = null,
val overloads: List<TspType>? = null,
val implementation: TspType? = null,
val returnType: TspType? = null,
val specializedTypes: TspSpecializedFunctionTypes? = null,
val boundToType: TspType? = null,
/**
* `TypeFlags` bitfield from the TSP protocol. Bit 3 (`0x8`) is `LITERAL` and indicates
* the type carries a literal value in [literalValue]. See `tsp_types::TypeFlags` upstream
* for the full set of bits.
*/
val flags: Int? = null,
/**
* For literal types (flag `LITERAL` set), the literal payload — an `Int`, `Boolean`, or
* `String` after Gson-deserialization of pyrefly's untagged `LiteralValue` enum. Enum
* / sentinel literals come through as JSON objects and we treat them as `null` for now.
*/
val literalValue: Any? = null,
)
companion object {
/** Bit 3 of `TypeFlags` — set when the type wraps a literal value. */
const val LITERAL_FLAG: Int = 0x8
}
}
@@ -0,0 +1,115 @@
package com.intellij.python.pyrefly.lsp
import com.intellij.execution.process.BaseProcessHandler
import com.intellij.openapi.components.service
import com.intellij.openapi.module.Module
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.lsp.api.Lsp4jServer
import com.intellij.platform.lsp.api.customization.LspFoldingRangeCustomizer
import com.intellij.platform.lsp.api.customization.LspFoldingRangeDisabled
import com.intellij.python.lsp.core.PyLspToolCustomization
import com.intellij.python.lsp.core.PyLspToolDescriptor
import com.intellij.python.lsp.core.typeEngine.PyTypeEngineUtils
import com.intellij.python.lsp.core.utils.PyLspServerModificationTracker
import com.intellij.python.pyrefly.PyreflyConfiguration
import com.intellij.python.pyrefly.PyreflyPyTool
import com.intellij.python.pyrefly.PyreflyUsageCollector
import com.intellij.python.pytools.configuration.ExecutableDiscoveryMode
import com.intellij.python.pytools.getState
import com.intellij.python.pytools.lsp.PyLspToolSettings
import com.jetbrains.python.codeInsight.typing.PyTypeShed
import com.jetbrains.python.sdk.pythonSdk
import org.eclipse.lsp4j.ConfigurationItem
import org.eclipse.lsp4j.InitializeResult
@Suppress("UsagesOfObsoleteApi")
class PyreflyLspClientDescriptor(module: Module) : PyLspToolDescriptor(module, PyreflyPyTool.getInstance()) {
override val toolConfig: PyLspToolSettings
get() = project.service<PyreflyConfiguration>()
override val lsp4jServerClass: Class<out Lsp4jServer>
get() = PyreflyLsp4jServer::class.java
override val lspCustomization: PyLspToolCustomization = object : PyLspToolCustomization(toolConfig, pyTool, project) {
override val foldingRangeCustomizer: LspFoldingRangeCustomizer = LspFoldingRangeDisabled
}
override val lspServerListener: PyLspToolDescriptorLspServerListener = object : PyLspToolDescriptorLspServerListener() {
private var initialized = false
override fun serverInitialized(params: InitializeResult) {
super.serverInitialized(params)
initialized = true
PyreflyUsageCollector.logServerStartup(success = true, sdk = module.pythonSdk)
}
override fun serverStopped(shutdownNormally: Boolean) {
super.serverStopped(shutdownNormally)
if (!shutdownNormally && !initialized) {
PyreflyUsageCollector.logServerStartup(success = false, sdk = module.pythonSdk)
}
initialized = false
PyLspServerModificationTracker.getInstance(project).incModificationCount()
}
}
override fun lspArguments(): List<String> =
listOf(if (Registry.`is`("pyrefly.type.engine.tsp")) "tsp" else "lsp")
override fun createInitializationOptions(): Map<String, Any>? {
val homePath = module.pythonSdk?.homePath ?: return null
return buildMap {
put("pythonPath", homePath)
put("pyrefly", buildPyreflyClientSettings())
}
}
/**
* Pyrefly's `apply_client_configuration` overwrites every known field on every call,
* resetting any value missing from the payload to `None`. The IDE replies to both the
* `initialize.initializationOptions` channel and the `workspace/configuration` request
* for `"python"`, so any setting we want Pyrefly to keep has to appear in **both**
* responses — otherwise the second response silently undoes the first.
*
* See [`apply_client_configuration`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/lsp/non_wasm/workspace.rs)
* and the upstream `remove_*_clears_and_flags_modified` tests for the exact contract.
*/
private fun buildPyreflyClientSettings(): Map<String, Any> = buildMap {
put("displayTypeErrors", "force-on")
// Point Pyrefly at PyCharm's bundled typeshed so stdlib (and any third-party
// packages typeshed knows about) is resolved from a directory PyCharm already
// indexes. Without this, Pyrefly responds with URIs inside its own
// materialized `<tmp>/pyrefly_bundled_typeshed_*` dir, and `PyClass.qualifiedName`
// for those symbols ends up empty because PyCharm has no source/library root
// for that path.
PyTypeShed.directory
?.let { vf -> vf.fileSystem.getNioPath(vf)?.toString() }
?.let { put("typeshedPath", it) }
// Stop Pyrefly from falling back to its bundled third-party stubs (pandas-stubs,
// boto3-stubs, ...) when a package isn't available in the user's site-packages
// and isn't in PyCharm's typeshed either. Those bundled stubs land under
// `<tmp>/pyrefly_bundled_third_party_*` which the IDE doesn't index, so PSI
// can't compute a qualifiedName for symbols resolved through them. With this
// flag set, Pyrefly instead reports `MissingStubs` / `NotFound`, and the IDE
// surfaces a regular diagnostic the user can act on (install <pkg>-stubs).
put("disableBundledThirdPartyStubs", true)
}
override fun getWorkspaceConfiguration(item: ConfigurationItem): Map<String, Map<String, Any>> =
mapOf("pyrefly" to buildPyreflyClientSettings())
override fun startServerProcess(): BaseProcessHandler<*> {
when (PyreflyPyTool.getInstance().getState(project).discoveryMode) {
ExecutableDiscoveryMode.INTERPRETER -> {
val pythonSdk = module.pythonSdk ?: error("Cannot find PythonSdk for module " + module.name)
if (!PyTypeEngineUtils.isExternalTypeEngineSupported(project)) {
error("Pyrefly type engine available only for local not read only interpreters. Current:$pythonSdk")
}
}
ExecutableDiscoveryMode.PATH -> error("Path for pyrefly executable is not setup")
ExecutableDiscoveryMode.UVX -> error("UVX mode is not yet supported for Pyrefly")
}
return super.startServerProcess()
}
}
@@ -0,0 +1,9 @@
package com.intellij.python.pyrefly.lsp
import com.intellij.openapi.module.Module
import com.intellij.python.lsp.core.PyLspToolIntegrationProvider
import com.intellij.python.lsp.core.PyLspToolDescriptor
class PyreflyLspIntegrationProvider : PyLspToolIntegrationProvider() {
override fun getDescriptor(module: Module): PyLspToolDescriptor = PyreflyLspClientDescriptor(module)
}
@@ -0,0 +1,50 @@
package com.intellij.python.pyrefly.typeProvider
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.lsp.api.LspClientManager
import com.intellij.platform.lsp.api.ensureClientStarted
import com.intellij.platform.lsp.api.getClients
import com.intellij.python.lsp.core.typeEngine.PyTypeEngineUtils
import com.intellij.python.pyrefly.PyreflyPyTool
import com.intellij.python.pyrefly.lsp.PyreflyLspClientDescriptor
import com.intellij.python.pyrefly.lsp.PyreflyLspIntegrationProvider
import com.intellij.python.pytools.isEnabledOn
import com.jetbrains.python.psi.types.engine.PyTypeEngine
import com.jetbrains.python.psi.types.engine.PyTypeEngineProvider
/**
* External type provider that delegates to Pyrefly's LSP endpoint.
* This provider is enabled when the Type Engine feature is enabled via registry.
* The actual per-module check for Pyrefly configuration is done in PyreflyTypeResolver.isSupportedForResolve.
*/
class PyreflyExternalPyTypeResolverProvider : PyTypeEngineProvider {
override fun createResolver(module: Module): PyTypeEngine? {
// Check if type engine feature is enabled (via registry or unit tests)
val isFeatureEnabled = Util.isAvailable(module.project)
if (!isFeatureEnabled) {
return null
}
if (!PyreflyPyTool.getInstance().isEnabledOn(module.project)) {
return null
}
val lspServerManager = LspClientManager.getInstance(module.project)
lspServerManager.ensureClientStarted<PyreflyLspIntegrationProvider>(PyreflyLspClientDescriptor(module))
val server = lspServerManager.getClients<PyreflyLspIntegrationProvider>().firstOrNull() ?: return null
return PyreflyTypeResolver(module, server)
}
object Util {
fun isAvailable(project: Project): Boolean {
return (PyTypeEngineUtils.isExternalTypeEngineSupported(project) ||
Registry.`is`("pyrefly.type.engine") ||
ApplicationManager.getApplication().isUnitTestMode)
}
}
}
@@ -0,0 +1,66 @@
package com.intellij.python.pyrefly.typeProvider
import com.intellij.psi.util.endOffset
import com.intellij.psi.util.startOffset
import com.jetbrains.python.psi.PyBinaryExpression
import com.jetbrains.python.psi.PyCallExpression
import com.jetbrains.python.psi.PyDecorator
import com.jetbrains.python.psi.PyDictLiteralExpression
import com.jetbrains.python.psi.PyElement
import com.jetbrains.python.psi.PyElementVisitor
import com.jetbrains.python.psi.PyFunction
import com.jetbrains.python.psi.PyLambdaExpression
import com.jetbrains.python.psi.PyPrefixExpression
import com.jetbrains.python.psi.PySequenceExpression
import com.jetbrains.python.psi.PySubscriptionExpression
import com.jetbrains.python.psi.PyTupleExpression
class PyreflyOffsetDetectorVisitor : PyElementVisitor() {
var offset: Int = -1
private set
override fun visitPyElement(node: PyElement) {
offset = node.endOffset - 1
}
override fun visitPyFunction(node: PyFunction) {
offset = node.nameNode?.startOffset ?: node.startOffset
}
override fun visitPyLambdaExpression(node: PyLambdaExpression) {
offset = node.startOffset
}
override fun visitPyCallExpression(node: PyCallExpression) {
offset = node.endOffset
}
override fun visitPyDecorator(node: PyDecorator) {
offset = node.endOffset
}
override fun visitPySubscriptionExpression(node: PySubscriptionExpression) {
offset = node.endOffset
}
override fun visitPyPrefixExpression(node: PyPrefixExpression) {
offset = node.startOffset
}
override fun visitPySequenceExpression(node: PySequenceExpression) {
offset = node.endOffset
}
override fun visitPyTupleExpression(node: PyTupleExpression) {
// we need to hit a comma or space to reveal the whole tuple type
offset = node.elements.firstOrNull()?.endOffset?.plus(1) ?: node.endOffset
}
override fun visitPyDictLiteralExpression(node: PyDictLiteralExpression) {
offset = node.endOffset
}
override fun visitPyBinaryExpression(node: PyBinaryExpression) {
offset = node.psiOperator?.startOffset ?: node.startOffset
}
}
@@ -0,0 +1,430 @@
package com.intellij.python.pyrefly.typeProvider
import com.intellij.openapi.application.runReadActionBlocking
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.lsp.api.LspClient
import com.intellij.platform.lsp.util.getOffsetInDocument
import com.intellij.platform.lsp.impl.LspClientImpl
import com.intellij.platform.lsp.util.getLsp4jPosition
import com.intellij.platform.lsp.util.getOffsetInDocument
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.python.lsp.core.type.LspTypeEvalContext
import com.intellij.python.lsp.core.type.PyStringTypeResolver
import com.intellij.python.pyrefly.PyreflyUsageCollector
import com.intellij.python.pyrefly.lsp.PyreflyLsp4jServer
import com.jetbrains.python.PyNames
import com.jetbrains.python.codeInsight.typing.PyTypingTypeProvider
import com.jetbrains.python.psi.PyCallable
import com.jetbrains.python.psi.PyClass
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.psi.PyTargetExpression
import com.jetbrains.python.psi.PyTypeParameter
import com.jetbrains.python.psi.PyTypedElement
import com.jetbrains.python.psi.impl.ParamHelper
import com.jetbrains.python.psi.impl.PyBuiltinCache
import com.jetbrains.python.psi.types.PyAnyType
import com.jetbrains.python.psi.types.PyCallableParameter
import com.jetbrains.python.psi.types.PyCallableParameterImpl
import com.jetbrains.python.psi.types.PyCallableType
import com.jetbrains.python.psi.types.PyClassTypeImpl
import com.jetbrains.python.psi.types.PyCollectionTypeImpl
import com.jetbrains.python.psi.types.PyFunctionType
import com.jetbrains.python.psi.types.PyFunctionTypeImpl
import com.jetbrains.python.psi.types.PyLiteralType
import com.jetbrains.python.psi.types.PyModuleType
import com.jetbrains.python.psi.types.PyNeverType
import com.jetbrains.python.psi.types.PyOverloadType
import com.jetbrains.python.psi.types.PyTupleType
import com.jetbrains.python.psi.types.PyType
import com.jetbrains.python.psi.types.PyTypeVarType
import com.jetbrains.python.psi.types.PyTypeVarTypeImpl
import com.jetbrains.python.psi.types.PyUnionType
import com.jetbrains.python.psi.types.TypeEvalContext
import org.eclipse.lsp4j.Range
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.Unmodifiable
import org.jetbrains.annotations.VisibleForTesting
import kotlin.time.measureTimedValue
@Suppress("SSBasedInspection")
open class PyreflyTypeEvalContext internal constructor(val lspClient: LspClient, psiFile: PsiFile) : LspTypeEvalContext(psiFile) {
private val snapshot: Int? by lazy {
try {
lspClient.sendRequestSync(LspClient.DEFAULT_REQUEST_TIMEOUT_MS) {
(it as PyreflyLsp4jServer).getSnapshot()
}
}
catch (e: ResponseErrorException) {
e.responseError?.code
null
}
}
override fun provideType(element: PyTypedElement, isUserInitiated: Boolean): Ref<PyType?>? {
if (!Registry.`is`("pyrefly.type.engine.tsp")) {
// Old way: fetch types over LSP via the base-class flow (requestTypes / resolveStringType).
return super.provideType(element, isUserInitiated)
}
if (element is PsiFile) return null
val file = psiFile.virtualFile ?: return null
val document = FileDocumentManager.getInstance().getDocument(file) ?: return null
val sourceUri = lspClient.getDocumentIdentifier(file).uri
val textRange = element.textRange
if (textRange.isEmpty) return null
val offsetDetector = PyreflyOffsetDetectorVisitor()
element.accept(offsetDetector)
val position = getLsp4jPosition(document, offsetDetector.offset)
val node = PyreflyLsp4jServer.TspNode(sourceUri, Range(position, position))
val snapshot = this.snapshot ?: return null
val tspType = try {
lspClient.sendRequestSync(LspClient.DEFAULT_REQUEST_TIMEOUT_MS) {
(it as PyreflyLsp4jServer).getComputedType(
PyreflyLsp4jServer.GetComputedTypeParams(arg = node, snapshot = snapshot)
)
}
}
catch (e: ResponseErrorException) {
e.responseError?.code
null
} ?: return null
//logTypeDefinition(node, tspType)
return buildPyType(element, tspType)
}
private fun buildPyType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
// Pyrefly marks `Literal[...]` types via the LITERAL bit (0x8) of TypeFlags and stores
// the value in `literalValue`. Detect this before falling through to the generic
// class-type path so we surface `Literal[5]` / `Literal["foo"]` instead of bare `int` /
// `str` — otherwise `tuple[Literal[1], Literal[2], Literal[3]]` collapses to `tuple[int, int, int]`.
if (tspType.kind == "3" && tspType.flags?.and(PyreflyLsp4jServer.LITERAL_FLAG) != 0) {
buildPyLiteralType(pyElement, tspType)?.let { return it }
}
return when (tspType.kind) {
"0" -> buildPyBuiltInType(pyElement, tspType)
"2" -> buildPyFunctionType(pyElement, tspType)
"3" -> buildPyClassType(pyElement, tspType)
"4" -> buildPyUnionType(pyElement, tspType)
"5" -> buildPyModuleType(pyElement, tspType)
"6" -> buildPyTypeVarType(pyElement, tspType)
"7" -> buildPyOverloadedType(pyElement, tspType)
else -> null
}
}
private data class ResolvedDefTarget(val file: PyFile, val element: PsiElement, val offset: Int)
/**
* Resolve the [PyFile] and the [PsiElement] (plus its offset) that a TSP declaration node points to.
* Must be called inside a read action.
*/
private fun resolveDefTarget(pyElement: PyTypedElement, defNode: PyreflyLsp4jServer.TspNode): ResolvedDefTarget? {
val virtualFile = VirtualFileManager.getInstance().findFileByUrl(defNode.uri) ?: return null
val document = FileDocumentManager.getInstance().getDocument(virtualFile) ?: return null
val offset = getOffsetInDocument(document, defNode.range.start) ?: return null
val targetFile = pyElement.manager.findFile(virtualFile) as? PyFile ?: return null
val targetElement = targetFile.findElementAt(offset) ?: return null
return ResolvedDefTarget(targetFile, targetElement, offset)
}
/**
* Build a `PyLiteralType` for a Class-kind TSP type that carries `LITERAL` flag.
*
* Pyrefly emits the literal payload as `literalValue` — Kotlin sees it as `Int`,
* `Boolean`, or `String` after Gson-deserialization of the untagged TSP enum.
* Bytes / enum / sentinel literals come through as JSON objects or `null` from our
* `Any?` field and fall back to the regular class-type path.
*/
private fun buildPyLiteralType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
val value = tspType.literalValue ?: return null
return runReadActionBlocking {
val literal = PyLiteralType.fromLiteralValue(pyElement, value)
literal?.let {
thisLogger().info("Pyrefly TSP: built PyLiteralType ${it.name}")
Ref.create<PyType?>(it)
}
}
}
private fun buildPyBuiltInType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
val name = tspType.name ?: return null
val type: PyType? = when (name.lowercase()) {
"any" -> PyAnyType.any
"never" -> PyNeverType.NEVER
"noreturn" -> PyNeverType.NO_RETURN
"ellipsis" -> runReadActionBlocking { PyBuiltinCache.getInstance(pyElement).ellipsisType }
"unknown" -> PyAnyType.unknown
"unbound" -> null
"callable" -> runReadActionBlocking { PyTypingTypeProvider.createTypingCallableType(pyElement) }
else -> runReadActionBlocking {
PyClassTypeImpl.createTypeByQName(pyElement, "typing.$name", false)
?: PyClassTypeImpl.createTypeByQName(pyElement, PyTypingTypeProvider.SPECIAL_FORM, false)
}
}
if (type == null) return null
thisLogger().info("Pyrefly TSP: built builtin type for $name")
return Ref.create(type)
}
private fun buildPyClassType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
val declaration = tspType.declaration ?: return null
val defNode = declaration.node
if (defNode == null || defNode.uri.isEmpty()) {
// Pyrefly emits a synthesized stub class with `typeArgs` for built-in generic instances
// such as `tuple[...]`. Recover the element types from `typeArgs`.
if (!tspType.typeArgs.isNullOrEmpty()) {
buildPyTupleType(pyElement, tspType.typeArgs)?.let { return it }
}
return buildBuiltinClassType(pyElement, declaration.name)
}
val resolved = runReadActionBlocking {
val (_, targetElement, offset) = resolveDefTarget(pyElement, defNode) ?: return@runReadActionBlocking null
val pyClass = PsiTreeUtil.getParentOfType(targetElement, PyClass::class.java) ?: return@runReadActionBlocking null
val typeArgs = tspType.typeArgs
val classType: PyType = if (!typeArgs.isNullOrEmpty()) {
val elementTypes = typeArgs.map { buildPyType(pyElement, it)?.get() }
if (pyClass.qualifiedName == PyNames.FQN.TUPLE) {
PyTupleType.create(pyElement, elementTypes) ?: PyClassTypeImpl(pyClass, false)
}
else {
PyCollectionTypeImpl(pyClass, false, elementTypes)
}
}
else {
PyClassTypeImpl(pyClass, false)
}
thisLogger().info("Pyrefly TSP: built PyClassType for ${pyClass.qualifiedName ?: pyClass.name} at ${defNode.uri}:$offset (typeArgs=${typeArgs?.size ?: 0})")
Ref.create<PyType?>(classType)
}
if (resolved != null) return resolved
// Pyrefly emits a sentinel `range=(0,0)` for builtin instances (e.g. `int` for the literal
// default value `5`). The offset points to the start of `builtins.pyi`, so PSI gives us no
// class — fall back to looking up the class by `declaration.name` in the builtin cache.
return buildBuiltinClassType(pyElement, declaration.name)
}
private fun buildBuiltinClassType(pyElement: PyTypedElement, name: String?): Ref<PyType?>? {
if (name.isNullOrEmpty()) return null
return runReadActionBlocking {
val type = PyBuiltinCache.getInstance(pyElement).getObjectType(name) ?: return@runReadActionBlocking null
thisLogger().info("Pyrefly TSP: built PyClassType for builtin $name")
Ref.create<PyType?>(type)
}
}
private fun buildPyTupleType(pyElement: PyTypedElement, typeArgs: List<PyreflyLsp4jServer.TspType>): Ref<PyType?>? {
if (typeArgs.isEmpty()) return null
return runReadActionBlocking {
val elementTypes = typeArgs.map { buildPyType(pyElement, it)?.get() }
val tupleType = PyTupleType.create(pyElement, elementTypes) ?: return@runReadActionBlocking null
thisLogger().info("Pyrefly TSP: built PyTupleType with ${elementTypes.size} elements")
Ref.create<PyType?>(tupleType)
}
}
private fun buildPyFunctionType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
val declaration = tspType.declaration ?: return null
val defNode = declaration.node
if (defNode == null || defNode.uri.isEmpty()) {
// Pyrefly wraps class/type references in a Function envelope whose top-level
// declaration is a Synthesized stub. The real symbol lives on `returnType`.
val returnType = tspType.returnType
if (returnType != null && returnType.hasSourceLocation()) {
return buildPyType(pyElement, returnType)
}
return buildBuiltinFunctionType(pyElement, declaration.name, tspType)
}
val resolved = runReadActionBlocking {
val (_, targetElement, offset) = resolveDefTarget(pyElement, defNode) ?: return@runReadActionBlocking null
val callable = PsiTreeUtil.getParentOfType(targetElement, PyCallable::class.java) ?: return@runReadActionBlocking null
thisLogger().info("Pyrefly TSP: built PyFunctionType for ${callable.name} at ${defNode.uri}:$offset")
Ref.create<PyType?>(buildFunctionType(pyElement, callable, tspType))
}
if (resolved != null) return resolved
// Sentinel `range=(0,0)` for builtin callables: PSI lookup at offset 0 misses the actual
// `def`, so resolve by `declaration.name` via the builtin cache.
return buildBuiltinFunctionType(pyElement, declaration.name, tspType)
}
private fun buildBuiltinFunctionType(pyElement: PyTypedElement, name: String?, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
if (name.isNullOrEmpty()) return null
return runReadActionBlocking {
val callable = PyBuiltinCache.getInstance(pyElement).getByName(name) as? PyCallable ?: return@runReadActionBlocking null
thisLogger().info("Pyrefly TSP: built PyFunctionType for builtin $name")
Ref.create<PyType?>(buildFunctionType(pyElement, callable, tspType))
}
}
private fun buildFunctionType(
pyElement: PyTypedElement,
callable: PyCallable,
tspType: PyreflyLsp4jServer.TspType,
): PyFunctionType {
val psiParams = callable.parameterList.parameters
val substituted = tspType.specializedTypes?.parameterTypes
val parameters = psiParams.mapIndexed { i, p ->
val resolved = substituted?.getOrNull(i)?.let { buildPyType(pyElement, it)?.get() }
if (resolved != null) PyCallableParameterImpl.psi(p, resolved) else PyCallableParameterImpl.psi(p)
}
val returnTsp = tspType.specializedTypes?.returnType ?: tspType.returnType
val returnType = returnTsp?.let { buildPyType(pyElement, it)?.get() }
return PyreflyFunctionType(callable, parameters, returnType)
}
private class PyreflyFunctionType(
callable: PyCallable,
parameters: List<PyCallableParameter>,
private val tspReturnType: PyType?,
) : PyFunctionTypeImpl(callable, parameters) {
override fun getReturnType(context: TypeEvalContext): PyType? = tspReturnType
override fun dropSelf(context: TypeEvalContext): PyFunctionType {
val params = getParameters(context) ?: return this
val newParams = ParamHelper.dropSelf(params)
return if (newParams.size < params.size) PyreflyFunctionType(callable, newParams, tspReturnType) else this
}
}
private fun buildPyUnionType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
val subTypes = tspType.subTypes ?: return null
if (subTypes.isEmpty()) return null
val members = subTypes.map { buildPyType(pyElement, it)?.get() }
if (members.all { it == null }) return null
thisLogger().info("Pyrefly TSP: built PyUnionType with ${members.size} members")
return Ref.create(PyUnionType.union(members))
}
private fun buildPyModuleType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
val uri = tspType.uri
if (uri.isNullOrEmpty()) return null
val virtualFile = VirtualFileManager.getInstance().findFileByUrl(uri) ?: return null
return runReadActionBlocking {
val moduleFile = pyElement.manager.findFile(virtualFile) as? PyFile ?: return@runReadActionBlocking null
thisLogger().info("Pyrefly TSP: built PyModuleType for ${tspType.moduleName ?: moduleFile.name}")
Ref.create<PyType?>(PyModuleType(moduleFile))
}
}
private fun buildPyTypeVarType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
val declaration = tspType.declaration ?: return null
val name = declaration.name
if (name.isNullOrEmpty()) return null
val defNode = declaration.node
if (defNode == null || defNode.uri.isEmpty()) {
thisLogger().info("Pyrefly TSP: built minimal PyTypeVarType for $name (no declaration node)")
return Ref.create<PyType?>(PyTypeVarTypeImpl(name, null))
}
val resolved = runReadActionBlocking {
val (targetFile, targetElement, offset) = resolveDefTarget(pyElement, defNode) ?: return@runReadActionBlocking null
val context = TypeEvalContext.codeAnalysis(pyElement.project, targetFile)
val typeParameter = PsiTreeUtil.getParentOfType(targetElement, PyTypeParameter::class.java)
if (typeParameter != null) {
val pep695Type = PyTypingTypeProvider.getTypeParameterTypeFromTypeParameter(typeParameter, context)
if (pep695Type is PyTypeVarType) {
thisLogger().info("Pyrefly TSP: built PyTypeVarType for $name from PyTypeParameter at ${defNode.uri}:$offset")
return@runReadActionBlocking Ref.create<PyType?>(pep695Type)
}
}
val targetExpression = PsiTreeUtil.getParentOfType(targetElement, PyTargetExpression::class.java)
if (targetExpression != null) {
val assignedType = context.getType(targetExpression)
if (assignedType is PyTypeVarType) {
thisLogger().info("Pyrefly TSP: built PyTypeVarType for $name from PyTargetExpression at ${defNode.uri}:$offset")
return@runReadActionBlocking Ref.create<PyType?>(assignedType)
}
}
null
}
if (resolved != null) return resolved
thisLogger().info("Pyrefly TSP: fell back to minimal PyTypeVarType for $name")
return Ref.create<PyType?>(PyTypeVarTypeImpl(name, null))
}
private fun buildPyOverloadedType(pyElement: PyTypedElement, tspType: PyreflyLsp4jServer.TspType): Ref<PyType?>? {
val overloads = tspType.overloads
val implementationTsp = tspType.implementation
if (overloads.isNullOrEmpty() && implementationTsp == null) return null
val items = overloads?.map { buildPyType(pyElement, it)?.get() as? PyCallableType } ?: emptyList()
val impl = implementationTsp?.let { buildPyType(pyElement, it) }
if (items.isEmpty() && impl == null) return null
thisLogger().info("Pyrefly TSP: built PyOverloadType with ${items.size} overloads (impl=${impl?.get() != null})")
return Ref.create<PyType?>(PyOverloadType(items, impl))
}
override fun requestTypes(pyTypedElements: @Unmodifiable Collection<PyTypedElement>): List<String?>? {
val file = psiFile.virtualFile ?: return null
val document = FileDocumentManager.getInstance().getDocument(file) ?: return null
val textDocument = lspClient.getDocumentIdentifier(file)
val positions = pyTypedElements.mapNotNull { psiElement ->
if (psiElement is PsiFile) return@mapNotNull null
val textRange = psiElement.textRange
if (textRange.isEmpty) return@mapNotNull null
val offsetDetector = PyreflyOffsetDetectorVisitor()
psiElement.accept(offsetDetector)
val offset = offsetDetector.offset
getLsp4jPosition(document, offset)
}
val response = try {
lspClient.sendRequestSync {
(it as PyreflyLsp4jServer).provideType(textDocument, positions)
}
}
catch (e: ResponseErrorException) {
e.responseError?.code
return null
}
return response?.contents?.map { it.value }
}
override fun resolveStringType(element: PyTypedElement, stringType: String): Ref<PyType?>? {
return resolveStringTypeImpl(element, stringType)
}
private fun PyreflyLsp4jServer.TspType?.hasSourceLocation(): Boolean {
val type = this ?: return false
val decl = type.declaration
if (decl != null) {
val nodeUri = decl.node?.uri
if (!nodeUri.isNullOrEmpty() || !decl.uri.isNullOrEmpty()) return true
}
// Class/type references wrapped in a Function envelope keep their real declaration on returnType.
return type.returnType?.hasSourceLocation() == true
}
companion object {
@ApiStatus.Internal
@VisibleForTesting
fun resolveStringTypeImpl(
element: PyTypedElement,
stringType: String,
): Ref<PyType?>? {
val (result, duration) = measureTimedValue {
PyStringTypeResolver.resolvePyType(element, stringType)
}
PyreflyUsageCollector.logStringTypeResolutionTime(duration)
return result
}
}
}
@@ -0,0 +1,54 @@
package com.intellij.python.pyrefly.typeProvider
import com.intellij.openapi.module.Module
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.registry.Registry
import com.intellij.platform.lsp.api.LspClient
import com.intellij.psi.util.parents
import com.intellij.python.lsp.core.type.LspPyTypeResolver
import com.intellij.python.lsp.core.typeEngine.PyTypeEngineType
import com.intellij.python.pyrefly.PyreflyPyTool
import com.intellij.python.pytools.isEnabledOn
import com.jetbrains.python.psi.PyFromImportStatement
import com.jetbrains.python.psi.PyQualifiedExpression
import com.jetbrains.python.psi.PyTypedElement
import com.jetbrains.python.psi.types.PyType
internal class PyreflyTypeResolver(private val module: Module, val lspClient: LspClient) : LspPyTypeResolver {
override val name: String = PyTypeEngineType.PYREFLY.name
override fun isSupportedForResolve(pyTypedElement: PyTypedElement): Boolean {
if (pyTypedElement.isInNotebookFile()) {
return false
}
// First check the base class conditions
if (!super.isSupportedForResolve(pyTypedElement)) {
return false
}
// when there is `from x.y`, for `y` pyrefly will return `Module[x]`, so we do it ourselves
if (
pyTypedElement.parents(false).any { it is PyFromImportStatement }
&& pyTypedElement is PyQualifiedExpression
&& pyTypedElement.isQualified
) {
return false
}
return Registry.`is`("pyrefly.type.engine") || PyreflyPyTool.getInstance().isEnabledOn(module.project)
}
override fun resolveType(pyTypedElement: PyTypedElement, isLibrary: Boolean, isUserInitiated: Boolean): Ref<PyType?>? {
if (pyTypedElement.isInNotebookFile()) {
return null
}
val realFile = pyTypedElement.containingFile?.originalFile ?: return null
val pyreflyTypeEvalContext = PyreflyTypeResolverFileCache.getInstance(module.project).getContext(realFile, lspClient, isLibrary)
return pyreflyTypeEvalContext.provideType(pyTypedElement, isUserInitiated)
}
private fun PyTypedElement.isInNotebookFile(): Boolean {
return containingFile?.viewProvider?.virtualFile?.extension == "ipynb"
}
}
@@ -0,0 +1,69 @@
package com.intellij.python.pyrefly.typeProvider
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.LowMemoryWatcher
import com.intellij.openapi.util.LowMemoryWatcher.LowMemoryWatcherType
import com.intellij.openapi.util.SimpleModificationTracker
import com.intellij.platform.lsp.api.LspClient
import com.intellij.psi.PsiFile
import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiModificationTracker
import com.intellij.python.lsp.core.type.LspTypeEvalContext
import com.intellij.python.lsp.core.utils.PyLspServerModificationTracker
import com.intellij.util.containers.ContainerUtil
import com.jetbrains.python.psi.types.PyLibraryModificationTracker
import java.util.concurrent.ConcurrentMap
/**
* Pyrefly contains own cache for resolved types,
* and we need to rely on it not on our psi tracker
* as we do for the built-in resolver
*/
@Service(Service.Level.PROJECT)
internal class PyreflyTypeResolverFileCache(val project: Project) : Disposable.Default {
private val lowMemoryModificationTracker = SimpleModificationTracker()
private val cachedMapStorage = CachedValuesManager.getManager(project).createCachedValue {
val map = ContainerUtil.createConcurrentSoftValueMap<PsiFile, LspTypeEvalContext>()
CachedValueProvider.Result.create(map,
PsiModificationTracker.getInstance(project ),
lowMemoryModificationTracker,
PyLspServerModificationTracker.getInstance(project))
}
private val librariesCachedMapStorage: CachedValue<ConcurrentMap<PsiFile, LspTypeEvalContext>> =
CachedValuesManager.getManager(project).createCachedValue {
val map = ContainerUtil.createConcurrentSoftValueMap<PsiFile, LspTypeEvalContext>()
CachedValueProvider.Result.create(map,
PyLibraryModificationTracker.getInstance(project),
lowMemoryModificationTracker,
PyLspServerModificationTracker.getInstance(project))
}
init {
LowMemoryWatcher.register(
{
lowMemoryModificationTracker.incModificationCount()
cachedMapStorage.value
librariesCachedMapStorage.value
}, LowMemoryWatcherType.ALWAYS, this)
}
fun getContext(file: PsiFile, lspClient: LspClient, isLibrary: Boolean): LspTypeEvalContext {
val storage = if (isLibrary) librariesCachedMapStorage else cachedMapStorage
val cache = storage.value
return cache.getOrPut(file) { PyreflyTypeEvalContext(lspClient, file) }
}
companion object {
fun getInstance(project: Project): PyreflyTypeResolverFileCache = project.service()
}
}
+107
View File
@@ -0,0 +1,107 @@
load("@rules_jvm//:jvm.bzl", "jvm_library")
### auto-generated section `build intellij.python.pyright` start
jvm_library(
name = "pyright",
srcs = glob(
[
"src/**/*.kt",
"src/**/*.java",
"src/**/*.form",
"gen/**/*.kt",
"gen/**/*.java",
],
allow_empty = True,
),
module_name = "intellij.python.pyright",
resource_strip_prefix = "resources",
resources = glob(
["resources/**/*"],
),
visibility = ["//visibility:public"],
deps = [
# do not sort,
"//libraries/gson",
"//platform/editor-ui-api:editor-ui",
"//platform/lang-impl",
"//python:python-community-impl",
"//python/python-sdk:sdk",
"//python/python-core-impl:community-core-impl",
"@lib//:kotlin-stdlib",
"//platform/code-style-impl:codeStyle-impl",
"//platform/editor-ui-ex:editor-ex",
"//platform/platform-impl:ide-impl",
"//platform/ide-core",
"//xml/xml-parser:parser",
"//platform/core-ui",
"//xml/impl",
"//platform/util/jdom",
"//platform/smRunner",
"//libraries/guava",
"//libraries/fastutil",
"//platform/lsp",
"//platform/core-api:core",
"//json",
"@lib//:eclipse-lsp4j",
"//platform/projectModel-api:projectModel",
"//python/python-lsp-core:lsp-core",
"//python/python-pytools-ui:pytools-ui",
"//platform/analysis-api:analysis",
"//python/python-pytools:pytools",
],
)
jvm_library(
name = "pyright_test_lib",
testonly = True,
srcs = glob(
[],
allow_empty = True,
),
module_name = "intellij.python.pyright",
visibility = ["//visibility:public"],
runtime_deps = [
# do not sort,
":pyright",
"//libraries/gson:gson_test_lib",
"//platform/editor-ui-api:editor-ui_test_lib",
"//platform/lang-impl:lang-impl_test_lib",
"//python:python-community-impl_test_lib",
"//python/python-sdk:sdk_test_lib",
"//python/python-core-impl:community-core-impl_test_lib",
"//platform/code-style-impl:codeStyle-impl_test_lib",
"//platform/editor-ui-ex:editor-ex_test_lib",
"//platform/platform-impl:ide-impl_test_lib",
"//platform/ide-core:ide-core_test_lib",
"//xml/xml-parser:parser_test_lib",
"//platform/core-ui:core-ui_test_lib",
"//xml/impl:impl_test_lib",
"//platform/util/jdom:jdom_test_lib",
"//platform/smRunner:smRunner_test_lib",
"//libraries/guava:guava_test_lib",
"//libraries/fastutil:fastutil_test_lib",
"//platform/lsp:lsp_test_lib",
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//platform/lsp/testFramework",
"//platform/lsp/testFramework:testFramework_test_lib",
"//platform/core-api:core_test_lib",
"//json:json_test_lib",
"//platform/projectModel-api:projectModel_test_lib",
"//python/python-lsp-core:lsp-core_test_lib",
"//python/python-pytools-ui:pytools-ui_test_lib",
"//platform/analysis-api:analysis_test_lib",
"//python/python-pytools:pytools_test_lib",
],
)
### auto-generated section `build intellij.python.pyright` end
### auto-generated section `iml intellij.python.pyright` start
exports_files(
[
"intellij.python.pyright.iml",
],
visibility = ["//visibility:public"],
)
### auto-generated section `iml intellij.python.pyright` end
+2
View File
@@ -0,0 +1,2 @@
owner 'PyCharm Codeinsight'
@@ -0,0 +1,18 @@
package com.intellij.python.pyright.icons;
import com.intellij.ui.IconManager;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
/**
* NOTE THIS FILE IS AUTO-GENERATED
* DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead
*/
public final class PythonPyrightIcons {
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, PythonPyrightIcons.class.getClassLoader(), cacheKey, flags);
}
/** 85x85 */ public static final @NotNull Icon Basedpyright = load("icons/basedpyright.png", 0, 2);
/** 128x128 */ public static final @NotNull Icon Pyright = load("icons/pyright.png", 0, 2);
}
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.libraries.gson" />
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.python.community.impl" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.python.community.core.impl" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.codeStyle.impl" />
<orderEntry type="module" module-name="intellij.platform.editor.ex" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.xml.parser" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.xml.impl" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
<orderEntry type="module" module-name="intellij.platform.smRunner" />
<orderEntry type="module" module-name="intellij.libraries.guava" />
<orderEntry type="module" module-name="intellij.libraries.fastutil" />
<orderEntry type="module" module-name="intellij.platform.lsp" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.lsp.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.json" />
<orderEntry type="library" name="eclipse.lsp4j" level="project" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.python.lsp.core" />
<orderEntry type="module" module-name="intellij.python.pytools.ui" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
<orderEntry type="module" module-name="intellij.python.pytools" />
</component>
</module>
Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

@@ -0,0 +1,20 @@
<idea-plugin>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<plugin id="com.intellij.modules.json"/>
<module name="intellij.json"/>
<module name="intellij.platform.smRunner"/>
<module name="intellij.python.lsp.core"/>
<module name="intellij.python.pytools"/>
<module name="intellij.python.pytools.ui"/>
</dependencies>
<!-- endregion -->
<extensions defaultExtensionNs="com.intellij">
<platform.lsp.integrationProvider implementation="com.intellij.python.pyright.PyrightLspIntegrationProvider"/>
<projectSettings service="com.intellij.python.pyright.PyrightConfiguration"/>
<projectSettings service="com.intellij.python.pyright.BasedpyrightConfiguration"/>
<python.pytools.pyTool implementation="com.intellij.python.pyright.PyrightPyTool"/>
<python.pytools.pyTool implementation="com.intellij.python.pyright.BasedpyrightPyTool"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,2 @@
pyright.tool.description=A static type checker for Python, with type inference and language server support.
basedpyright.tool.description=A community fork of Pyright with additional features and bug fixes.
@@ -0,0 +1,32 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.pyright
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.project.Project
import com.intellij.python.pytools.lsp.LSP_TOOLS_STORAGE_FILE
import com.intellij.python.pytools.lsp.PyLspToolConfiguration
import com.intellij.python.pytools.ui.PyLspToolDetailConfigurable
import com.intellij.util.xmlb.XmlSerializerUtil
class BasedpyrightConfigurable(
project: Project,
) : PyLspToolDetailConfigurable<BasedpyrightConfiguration>(
project = project,
tool = BasedpyrightPyTool.getInstance()
)
@Service(Service.Level.PROJECT)
@State(
name = "BasedpyrightConfiguration",
storages = [Storage(LSP_TOOLS_STORAGE_FILE)]
)
data class BasedpyrightConfiguration(
override var inlayHints: Boolean? = true,
override var completions: Boolean? = true,
) : PyLspToolConfiguration<BasedpyrightConfiguration>() {
override fun loadState(state: BasedpyrightConfiguration) {
XmlSerializerUtil.copyBean(state, this)
}
}
@@ -0,0 +1,38 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.pyright
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.python.pytools.PyTool
import com.intellij.python.pytools.lsp.PyLspTool
import com.intellij.python.pytools.configuration.ConfigurablePyTool
import com.intellij.python.pytools.ui.pyLspToolFeaturesSummary
import com.jetbrains.python.packaging.PyPackageName
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon
/**
* [Basedpyright](https://docs.basedpyright.com/) — an open-source fork of Pyright that adds features
* such as stricter type inference and language-server capabilities otherwise exclusive to Pylance.
*/
@ApiStatus.Internal
class BasedpyrightPyTool : PyLspTool<BasedpyrightConfiguration>(), ConfigurablePyTool {
override val presentableName: String = "Basedpyright"
override val description: String get() = PyrightBundle.message("basedpyright.tool.description")
override val packageName: PyPackageName = PyPackageName.from("basedpyright")
override fun configuration(project: Project): BasedpyrightConfiguration = project.service<BasedpyrightConfiguration>()
override val icon: Icon = PyrightUtil.getDefaultBasedPyrightIcon()
override fun createConfigurable(project: Project): BasedpyrightConfigurable = BasedpyrightConfigurable(project)
override fun summaryFor(project: Project): String = pyLspToolFeaturesSummary(configuration(project))
override fun onEnabledChanged(project: Project, enabled: Boolean): Unit = restartOrStopPyrightProvider(project)
@Suppress("CompanionObjectInExtension")
companion object {
fun getInstance(): BasedpyrightPyTool = PyTool.EP_NAME.findExtensionOrFail(BasedpyrightPyTool::class.java)
}
}
@@ -0,0 +1,18 @@
package com.intellij.python.pyright
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
object PyrightBundle {
private val myDynamicBundle = DynamicBundle(this::class.java, BUNDLE)
@NonNls
const val BUNDLE: String = "messages.PyrightBundle"
@Nls
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String {
return myDynamicBundle.getMessage(key, *params)
}
}
@@ -0,0 +1,31 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.pyright
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.project.Project
import com.intellij.python.pytools.lsp.LSP_TOOLS_STORAGE_FILE
import com.intellij.python.pytools.lsp.PyLspToolConfiguration
import com.intellij.python.pytools.ui.PyLspToolDetailConfigurable
import com.intellij.util.xmlb.XmlSerializerUtil
class PyrightConfigurable(
project: Project
) : PyLspToolDetailConfigurable<PyrightConfiguration>(
project = project,
tool = PyrightPyTool.getInstance()
)
@Service(Service.Level.PROJECT)
@State(
name = "PyrightConfiguration",
storages = [Storage(LSP_TOOLS_STORAGE_FILE)]
)
data class PyrightConfiguration(
override var completions: Boolean? = true,
) : PyLspToolConfiguration<PyrightConfiguration>() {
override fun loadState(state: PyrightConfiguration) {
XmlSerializerUtil.copyBean(state, this)
}
}
@@ -0,0 +1,89 @@
package com.intellij.python.pyright
import com.google.gson.JsonObject
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.platform.lsp.api.LspClient
import com.intellij.platform.lsp.api.LspClientManager
import com.intellij.platform.lsp.api.LspClientManagerListener
import com.intellij.platform.lsp.api.LspServerState
import com.intellij.platform.lsp.api.stopAndRestartClientsIfNeeded
import com.intellij.platform.lsp.api.stopClients
import com.intellij.python.lsp.core.PyLspToolDescriptor
import com.intellij.python.lsp.core.PyLspToolIntegrationProvider
import com.intellij.python.pytools.isEnabledOn
import com.intellij.python.pytools.lsp.PyLspTool
import com.intellij.python.pytools.lsp.PyLspToolSettings
import com.jetbrains.python.PythonPluginDisposable
import org.eclipse.lsp4j.DidChangeConfigurationParams
/** The pyright-family tools sharing one LSP server, in priority order (basedpyright wins over pyright). */
private fun pyrightFamilyTools(): List<PyLspTool<*>> = listOf(
BasedpyrightPyTool.getInstance(),
PyrightPyTool.getInstance(),
)
/** The pyright-family tool that should serve a project: the first enabled one, or basedpyright as a (disabled) fallback. */
private fun activePyrightTool(project: Project): PyLspTool<*> {
val tools = pyrightFamilyTools()
return tools.firstOrNull { it.isEnabledOn(project) } ?: tools.first()
}
/**
* Re-evaluate which pyright-family server should serve [project]: restart the shared
* [PyrightLspIntegrationProvider] clients while either tool is enabled (so the rebuilt descriptor binds to
* the now-active one), otherwise stop them. Called from each pyright-family tool's `onEnabledChanged`.
*/
internal fun restartOrStopPyrightProvider(project: Project) {
val manager = LspClientManager.getInstance(project)
if (pyrightFamilyTools().any { it.isEnabledOn(project) }) manager.stopAndRestartClientsIfNeeded<PyrightLspIntegrationProvider>()
else manager.stopClients<PyrightLspIntegrationProvider>()
}
class PyrightLspIntegrationProvider : PyLspToolIntegrationProvider() {
override fun getDescriptor(module: Module): PyLspToolDescriptor =
PyrightLspClientDescriptor(module)
}
class PyrightLspClientDescriptor(module: Module) : PyLspToolDescriptor(
module = module,
pyTool = activePyrightTool(module.project),
) {
init {
// Send empty settings to make pyright LSP work properly
LspClientManager.getInstance(project).addListener(object : LspClientManagerListener {
override fun serverStateChanged(lspClient: LspClient) {
if (lspClient.descriptor !is PyrightLspClientDescriptor) return
if (lspClient.state == LspServerState.Running) {
lspClient.sendNotification { server ->
server.workspaceService.didChangeConfiguration(DidChangeConfigurationParams(JsonObject()))
}
}
}
}, PythonPluginDisposable.getInstance(project))
}
override fun createInitializationOptions(): Any {
return JsonObject()
}
override val executableName: String
get() = "${pyTool.packageName.name}-langserver"
override fun lspArguments(): List<String> {
return listOf("server", "--stdio")
}
override val toolConfig: PyLspToolSettings
get() = pyTool.configuration(project)
override val commandDescriptions: Map<String, String?> = mapOf(
"pyright.createtypestub" to null,
"pyright.restartserver" to "Restart server",
"basedpyright.createtypestub" to null,
"basedpyright.restartserver" to "Restart server",
"basedpyright.writeBaseline" to "Write new errors to baseline",
)
}
@@ -0,0 +1,38 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.pyright
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.python.pytools.PyTool
import com.intellij.python.pytools.lsp.PyLspTool
import com.intellij.python.pytools.configuration.ConfigurablePyTool
import com.intellij.python.pytools.ui.pyLspToolFeaturesSummary
import com.jetbrains.python.packaging.PyPackageName
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon
/**
* [Pyright](https://microsoft.github.io/pyright/) — a fast static type checker for Python from
* Microsoft, providing type checking and language-server features such as completions and hovers.
*/
@ApiStatus.Internal
class PyrightPyTool : PyLspTool<PyrightConfiguration>(), ConfigurablePyTool {
override val presentableName: String = "Pyright"
override val description: String get() = PyrightBundle.message("pyright.tool.description")
override val packageName: PyPackageName = PyPackageName.from("pyright")
override fun configuration(project: Project): PyrightConfiguration = project.service<PyrightConfiguration>()
override val icon: Icon = PyrightUtil.getDefaultPyrightIcon()
override fun createConfigurable(project: Project): PyrightConfigurable = PyrightConfigurable(project)
override fun summaryFor(project: Project): String = pyLspToolFeaturesSummary(configuration(project))
override fun onEnabledChanged(project: Project, enabled: Boolean): Unit = restartOrStopPyrightProvider(project)
@Suppress("CompanionObjectInExtension")
companion object {
fun getInstance(): PyrightPyTool = PyTool.EP_NAME.findExtensionOrFail(PyrightPyTool::class.java)
}
}
@@ -0,0 +1,14 @@
package com.intellij.python.pyright
import com.intellij.python.pyright.icons.PythonPyrightIcons
import com.intellij.util.IconUtil
import javax.swing.Icon
object PyrightUtil {
fun getDefaultPyrightIcon(): Icon {
return IconUtil.resizeSquared(PythonPyrightIcons.Pyright, 16)
}
fun getDefaultBasedPyrightIcon(): Icon {
return IconUtil.resizeSquared(PythonPyrightIcons.Basedpyright, 16)
}
}
+125
View File
@@ -0,0 +1,125 @@
load("@rules_jvm//:jvm.bzl", "jvm_library")
### auto-generated section `build intellij.python.ruff` start
jvm_library(
name = "ruff",
srcs = glob(
[
"src/**/*.kt",
"src/**/*.java",
"src/**/*.form",
"gen/**/*.kt",
"gen/**/*.java",
],
allow_empty = True,
),
module_name = "intellij.python.ruff",
resource_strip_prefix = "resources",
resources = glob(
["resources/**/*"],
),
visibility = ["//visibility:public"],
deps = [
# do not sort,
"//platform/editor-ui-api:editor-ui",
"//platform/lang-impl",
"//python:python-community-impl",
"//python/python-sdk:sdk",
"//python/python-core-impl:community-core-impl",
"//platform/core-impl",
"//platform/code-style-impl:codeStyle-impl",
"//platform/editor-ui-ex:editor-ex",
"//platform/platform-impl:ide-impl",
"//platform/ide-core",
"//xml/xml-parser:parser",
"//platform/core-ui",
"//xml/impl",
"//platform/util/jdom",
"//platform/util:util-ui",
"//platform/smRunner",
"//libraries/guava",
"//libraries/fastutil",
"//platform/projectModel-api:projectModel",
"//platform/projectModel-impl",
"//platform/lsp",
"//platform/core-api:core",
"@lib//:eclipse-lsp4j",
"@lib//:eclipse-lsp4j-jsonrpc",
"//platform/analysis-api:analysis",
"//platform/code-style-api:codeStyle",
"//python/python-lsp-core:lsp-core",
"//python/python-pytools-ui:pytools-ui",
"//plugins/toml/core",
"//libraries/gson",
"//platform/indexing-api:indexing",
"//platform/markdown-utils",
"//python/python-pyproject:pyproject",
"//platform/lang-api:lang",
"//python/python-pytools:pytools",
"//python/python-exec-service:community-execService",
],
)
jvm_library(
name = "ruff_test_lib",
testonly = True,
srcs = glob(
[],
allow_empty = True,
),
module_name = "intellij.python.ruff",
visibility = ["//visibility:public"],
runtime_deps = [
# do not sort,
":ruff",
"//platform/editor-ui-api:editor-ui_test_lib",
"//platform/lang-impl:lang-impl_test_lib",
"//python:python-community-impl_test_lib",
"//python/python-sdk:sdk_test_lib",
"//python/python-core-impl:community-core-impl_test_lib",
"//platform/core-impl:core-impl_test_lib",
"//platform/code-style-impl:codeStyle-impl_test_lib",
"//platform/editor-ui-ex:editor-ex_test_lib",
"//platform/platform-impl:ide-impl_test_lib",
"//platform/ide-core:ide-core_test_lib",
"//xml/xml-parser:parser_test_lib",
"//platform/core-ui:core-ui_test_lib",
"//xml/impl:impl_test_lib",
"//platform/util/jdom:jdom_test_lib",
"//platform/util:util-ui_test_lib",
"//platform/smRunner:smRunner_test_lib",
"//libraries/guava:guava_test_lib",
"//libraries/fastutil:fastutil_test_lib",
"//platform/projectModel-api:projectModel_test_lib",
"//platform/projectModel-impl:projectModel-impl_test_lib",
"//platform/lsp:lsp_test_lib",
"//platform/testFramework",
"//platform/testFramework:testFramework_test_lib",
"//platform/lsp/testFramework",
"//platform/lsp/testFramework:testFramework_test_lib",
"//platform/core-api:core_test_lib",
"//platform/analysis-api:analysis_test_lib",
"//platform/code-style-api:codeStyle_test_lib",
"//python/python-lsp-core:lsp-core_test_lib",
"//python/python-pytools-ui:pytools-ui_test_lib",
"//plugins/toml/core:core_test_lib",
"//libraries/gson:gson_test_lib",
"//platform/indexing-api:indexing_test_lib",
"//platform/markdown-utils:markdown-utils_test_lib",
"//python/python-pyproject:pyproject_test_lib",
"//platform/lang-api:lang_test_lib",
"//python/python-pytools:pytools_test_lib",
"//python/python-exec-service:community-execService_test_lib",
],
)
### auto-generated section `build intellij.python.ruff` end
### auto-generated section `iml intellij.python.ruff` start
exports_files(
[
"intellij.python.ruff.iml",
],
visibility = ["//visibility:public"],
)
### auto-generated section `iml intellij.python.ruff` end
+2
View File
@@ -0,0 +1,2 @@
owner 'PyCharm Codeinsight'
@@ -0,0 +1,17 @@
package com.intellij.python.ruff.icons;
import com.intellij.ui.IconManager;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
/**
* NOTE THIS FILE IS AUTO-GENERATED
* DO NOT EDIT IT BY HAND, run "Generate icon classes" configuration instead
*/
public final class PythonRuffIcons {
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, PythonRuffIcons.class.getClassLoader(), cacheKey, flags);
}
/** 100x100 */ public static final @NotNull Icon Ruff = load("icons/ruff.svg", 19919750, 2);
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.platform.editor.ui" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.python.community.impl" />
<orderEntry type="module" module-name="intellij.python.sdk" />
<orderEntry type="module" module-name="intellij.python.community.core.impl" />
<orderEntry type="module" module-name="intellij.platform.core.impl" />
<orderEntry type="module" module-name="intellij.platform.codeStyle.impl" />
<orderEntry type="module" module-name="intellij.platform.editor.ex" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.ide.core" />
<orderEntry type="module" module-name="intellij.xml.parser" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.xml.impl" />
<orderEntry type="module" module-name="intellij.platform.util.jdom" />
<orderEntry type="module" module-name="intellij.platform.util.ui" />
<orderEntry type="module" module-name="intellij.platform.smRunner" />
<orderEntry type="module" module-name="intellij.libraries.guava" />
<orderEntry type="module" module-name="intellij.libraries.fastutil" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
<orderEntry type="module" module-name="intellij.platform.projectModel.impl" />
<orderEntry type="module" module-name="intellij.platform.lsp" />
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.lsp.testFramework" scope="TEST" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="library" name="eclipse.lsp4j" level="project" />
<orderEntry type="library" name="eclipse.lsp4j.jsonrpc" level="project" />
<orderEntry type="module" module-name="intellij.platform.analysis" />
<orderEntry type="module" module-name="intellij.platform.codeStyle" />
<orderEntry type="module" module-name="intellij.python.lsp.core" />
<orderEntry type="module" module-name="intellij.python.pytools.ui" />
<orderEntry type="module" module-name="intellij.toml.core" />
<orderEntry type="module" module-name="intellij.libraries.gson" />
<orderEntry type="module" module-name="intellij.platform.indexing" />
<orderEntry type="module" module-name="intellij.platform.markdown.utils" />
<orderEntry type="module" module-name="intellij.python.pyproject" />
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="intellij.python.pytools" />
<orderEntry type="module" module-name="intellij.python.community.execService" />
</component>
</module>
@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="6" fill="#30173D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M80 23.9941C80 21.7882 78.2118 20 76.0059 20H20V80H47.6V62H52.4V80H80V57.2H68V52.4H76.0059C78.2118 52.4 80 50.6118 80 48.4059V23.9941ZM57.2 42.8V47.6H42.8V42.8H57.2Z" fill="#D7FF64"/>
</svg>

After

Width:  |  Height:  |  Size: 395 B

@@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="100" height="100" rx="6" fill="#30173D"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M80 23.9941C80 21.7882 78.2118 20 76.0059 20H20V80H47.6V62H52.4V80H80V57.2H68V52.4H76.0059C78.2118 52.4 80 50.6118 80 48.4059V23.9941ZM57.2 42.8V47.6H42.8V42.8H57.2Z" fill="#D7FF64"/>
</svg>

After

Width:  |  Height:  |  Size: 395 B

@@ -0,0 +1,3 @@
# ruff: ignore: E101/*<# mixed-spaces-and-tabs #>*/, E111/*<# indentation-with-invalid-multiple #>*/
print() # noqa: T201/*<# print #>*/
@@ -0,0 +1,5 @@
[lint]
select = [
"F"/*<# Pyflakes #>*/,
"E101"/*<# mixed-spaces-and-tabs #>*/,
]
@@ -0,0 +1,46 @@
<idea-plugin>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<dependencies>
<plugin id="org.toml.lang"/>
<module name="intellij.platform.smRunner"/>
<module name="intellij.python.community.execService"/>
<module name="intellij.python.lsp.core"/>
<module name="intellij.python.pytools"/>
<module name="intellij.python.pytools.ui"/>
<module name="intellij.toml.core"/>
</dependencies>
<!-- endregion -->
<extensions defaultExtensionNs="com.intellij">
<platform.lsp.integrationProvider implementation="com.intellij.python.ruff.server.RuffLspIntegrationProvider"/>
<lang.importOptimizer language="Python" implementationClass="com.intellij.python.ruff.RuffImportOptimizer" order="first"/>
<codeInsight.declarativeInlayProvider group="OTHER_GROUP"
implementationClass="com.intellij.python.ruff.codeinsight.RuffSuppressionInlayHintsProvider"
isEnabledByDefault="true"
language="Python"
bundle="messages.RuffBundle"
nameKey="ruff.suppression.inlay.hints.name"
providerId="RuffSuppressionCodes"/>
<codeInsight.declarativeInlayProvider group="OTHER_GROUP"
implementationClass="com.intellij.python.ruff.codeinsight.RuffTomlInlayHintsProvider"
isEnabledByDefault="true"
language="TOML"
bundle="messages.RuffBundle"
nameKey="ruff.toml.inlay.hints.name"
providerId="RuffTomlCodes"/>
<completion.contributor language="TOML" implementationClass="com.intellij.python.ruff.codeinsight.RuffConfigOptionCompletionContributor"/>
<gotoDeclarationHandler implementation="com.intellij.python.ruff.codeinsight.RuffConfigOptionGotoDeclarationHandler"/>
<codeInsight.linkHandler prefix="https://docs.astral.sh/ruff/" handlerClass="com.intellij.python.ruff.codeinsight.RuffTooltipLinkHandler"/>
<platform.backend.documentation.lookupElementTargetProvider implementation="com.intellij.python.ruff.codeinsight.RuffErrorCodeCompletionDocumentationProvider"/>
<platform.backend.documentation.lookupElementTargetProvider implementation="com.intellij.python.ruff.codeinsight.RuffConfigOptionCompletionDocumentationProvider"/>
<platform.backend.documentation.targetProvider implementation="com.intellij.python.ruff.codeinsight.RuffErrorCodeDocumentationProvider"/>
<platform.backend.documentation.targetProvider implementation="com.intellij.python.ruff.codeinsight.RuffConfigOptionDocumentationProvider"/>
<completion.contributor language="Python" implementationClass="com.intellij.python.ruff.codeinsight.RuffErrorCodeCompletionContributor" order="before PyDocstringCompletionContributor"/>
<completion.contributor language="TOML" implementationClass="com.intellij.python.ruff.codeinsight.RuffErrorCodeCompletionContributor"/>
<psi.referenceContributor language="Python" implementation="com.intellij.python.ruff.codeinsight.RuffErrorCodeReferenceContributor"/>
<psi.referenceContributor language="TOML" implementation="com.intellij.python.ruff.codeinsight.RuffErrorCodeReferenceContributor"/>
<externalAnnotator language="TOML" implementationClass="com.intellij.python.ruff.codeinsight.RuffConfigExternalAnnotator"/>
<projectSettings service="com.intellij.python.ruff.RuffConfiguration"/>
<python.pytools.pyTool implementation="com.intellij.python.ruff.RuffPyTool"/>
</extensions>
</idea-plugin>
@@ -0,0 +1,15 @@
ruff.suppression.inlay.hints.name=Ruff error code names
ruff.tool.description=An extremely fast Python linter and code formatter, written in Rust.
ruff.toml.inlay.hints.name=Ruff error code names
intention.name.disable.for.this.project=Disable for this project
intention.family.name.disable.ruff.rule=Disable Ruff rule
command.name.ruff.disable.rule=Ruff Disable Rule
intention.family.name.disable.ruff.rule.for.entire.file=Disable Ruff rule for the entire file
intention.name.disable.for.this.file=Disable for this file
command.name.append.ruff.rule.to.noqa.comment=Append Ruff Rule to Noqa Comment
command.name.add.ruff.noqa.comment=Add Ruff Noqa Comment
inspection.message.ruff.config.error=Ruff config error: {0}
checkbox.formatting=Formatting
checkbox.import.optimizer=Import optimizer
inspection.message.ruff.rule.documentation=Ruff rule documentation
intention.name.disable.for.this.line=Disable for this line
@@ -0,0 +1,18 @@
package com.intellij.python.ruff
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
object RuffBundle {
private val myDynamicBundle = DynamicBundle(this::class.java, BUNDLE)
@NonNls
const val BUNDLE: String = "messages.RuffBundle"
@Nls
fun message(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any): String {
return myDynamicBundle.getMessage(key, *params)
}
}
@@ -0,0 +1,52 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.project.Project
import com.intellij.python.pytools.lsp.LSP_TOOLS_STORAGE_FILE
import com.intellij.python.pytools.lsp.PyLspToolConfiguration
import com.intellij.python.pytools.lsp.PyLspToolSettings
import com.intellij.python.pytools.ui.PyLspToolDetailConfigurable
import com.intellij.ui.dsl.builder.Panel
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.util.xmlb.XmlSerializerUtil
/**
* Project-wide Ruff detail configurable.
*
* Path / discovery mode / enabled state live in [com.intellij.python.pytools.PyToolsState] and are edited from the
* `External Tools` table; this dialog only exposes the tool-specific feature toggles.
*/
class RuffConfigurable(project: Project) :
PyLspToolDetailConfigurable<RuffConfiguration>(project, RuffPyTool.getInstance()) {
override fun Panel.extraRows() {
row("") {
checkBox(RuffBundle.message("checkbox.formatting")).bindSelected(settings::formatting)
}
row("") {
checkBox(RuffBundle.message("checkbox.import.optimizer")).bindSelected(settings::sortImports)
}
}
}
interface RuffSettings : PyLspToolSettings {
var sortImports: Boolean
var formatting: Boolean
}
@Service(Service.Level.PROJECT)
@State(
name = "RuffConfiguration",
storages = [Storage(LSP_TOOLS_STORAGE_FILE)]
)
data class RuffConfiguration(
override var sortImports: Boolean = true,
override var formatting: Boolean = true,
) : PyLspToolConfiguration<RuffConfiguration>(), RuffSettings {
override fun loadState(state: RuffConfiguration) {
XmlSerializerUtil.copyBean(state, this)
}
}
@@ -0,0 +1,96 @@
package com.intellij.python.ruff
import com.intellij.lang.ImportOptimizer
import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.impl.DocumentImpl
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.platform.lsp.api.LspClientManager
import com.intellij.platform.lsp.api.getClients
import com.intellij.platform.lsp.util.applyTextEdits
import com.intellij.platform.lsp.util.getLsp4jRange
import com.intellij.psi.PsiFile
import com.intellij.python.ruff.server.RuffLspIntegrationProvider
import org.eclipse.lsp4j.CodeActionContext
import org.eclipse.lsp4j.CodeActionKind.SourceOrganizeImports
import org.eclipse.lsp4j.CodeActionParams
import org.eclipse.lsp4j.CodeActionTriggerKind
private val LOG = logger<RuffImportOptimizer>()
/**
* Import optimizer that uses the Ruff LSP server to optimize imports in Python files.
*
*/
class RuffImportOptimizer : ImportOptimizer {
override fun supports(psiFile: PsiFile): Boolean {
val virtualFile = psiFile.virtualFile ?: return false
val project = psiFile.project
val toolConfig = project.service<RuffConfiguration>()
if (!toolConfig.sortImports) return false
val lspServerManager = LspClientManager.getInstance(project)
val servers = lspServerManager.getClients<RuffLspIntegrationProvider>()
return servers.any { it.descriptor.isSupportedFile(virtualFile) }
}
override fun processFile(psiFile: PsiFile): Runnable {
val noResult = Runnable { }
val virtualFile = psiFile.virtualFile ?: return noResult
val project = psiFile.project
val lspServerManager = LspClientManager.getInstance(project)
val servers = lspServerManager.getClients<RuffLspIntegrationProvider>()
if (servers.isEmpty()) {
LOG.warn("No Ruff LSP server found for file: ${virtualFile.path}")
return noResult
}
val document = FileDocumentManager.getInstance().getDocument(virtualFile) ?: return noResult
val server = servers.first { it.descriptor.isSupportedFile(virtualFile) }
val range = getLsp4jRange(document, 0, document.textLength)
val params = CodeActionParams(
server.getDocumentIdentifier(virtualFile),
range,
CodeActionContext().apply {
diagnostics = emptyList()
triggerKind = CodeActionTriggerKind.Invoked
only = listOf(SourceOrganizeImports)
}
)
val tempDocument = DocumentImpl(document.text, false, true)
val codeActions = server.sendRequestSync { it.textDocumentService.codeAction(params) } ?: return noResult
var updated = false
for (either in codeActions) {
val codeAction = either.right ?: continue
if (codeAction.data == null) continue
val resolvedAction = server.sendRequestSync { it.textDocumentService.resolveCodeAction(codeAction) } ?: continue
if (resolvedAction.edit == null) continue
val changes = resolvedAction.edit?.documentChanges ?: continue
for (change in changes) {
if (!change.isLeft) continue
val textDocumentEdit = change.left ?: continue
val edits = textDocumentEdit.edits
updated = true
applyTextEdits(tempDocument, edits)
}
}
return Runnable {
if (updated) {
document.setText(tempDocument.text)
}
}
}
}
@@ -0,0 +1,64 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.platform.lsp.api.LspClientManager
import com.intellij.python.pytools.PyTool
import com.intellij.python.pytools.statistics.PyToolFusSnapshot
import com.intellij.python.pytools.lsp.PyLspTool
import com.intellij.python.pytools.configuration.ConfigurablePyTool
import com.intellij.python.pytools.ui.PyToolsUiBundle
import com.intellij.python.ruff.server.RuffLspIntegrationProvider
import com.jetbrains.python.packaging.PyPackageName
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon
/**
* [Ruff](https://docs.astral.sh/ruff/) — an extremely fast Python linter and code formatter written in
* Rust by Astral. It combines linting, import sorting, and formatting in a single tool, aiming to
* replace Flake8, isort, pyupgrade, and Black.
*/
@ApiStatus.Internal
class RuffPyTool : PyLspTool<RuffConfiguration>(), ConfigurablePyTool {
override val presentableName: String = "Ruff"
override val description: String get() = RuffBundle.message("ruff.tool.description")
override val packageName: PyPackageName = PyPackageName.from("ruff")
override fun configuration(project: Project): RuffConfiguration = project.service<RuffConfiguration>()
override val icon: Icon = RuffUtil.getDefaultRuffIcon()
override fun createConfigurable(project: Project): RuffConfigurable = RuffConfigurable(project)
override fun summaryFor(project: Project): String {
val cfg = configuration(project)
return buildList {
if (cfg.inspections) add(PyToolsUiBundle.message("checkbox.inspections"))
if (cfg.formatting) add(RuffBundle.message("checkbox.formatting"))
if (cfg.sortImports) add(RuffBundle.message("checkbox.import.optimizer"))
if (cfg.completions == true) add(PyToolsUiBundle.message("checkbox.completions"))
if (cfg.inlayHints == true) add(PyToolsUiBundle.message("checkbox.inlay.hints"))
if (cfg.documentation == true) add(PyToolsUiBundle.message("checkbox.documentation"))
}.joinToString(", ")
}
override fun onEnabledChanged(project: Project, enabled: Boolean) {
val manager = LspClientManager.getInstance(project)
if (enabled) manager.startClientsIfNeeded(RuffLspIntegrationProvider::class.java)
else manager.stopClients(RuffLspIntegrationProvider::class.java)
}
override fun configurationFusSnapshot(project: Project): PyToolFusSnapshot {
val cfg = configuration(project)
return super.configurationFusSnapshot(project).copy(
formatting = cfg.formatting,
sortImports = cfg.sortImports,
)
}
@Suppress("CompanionObjectInExtension")
companion object {
fun getInstance(): RuffPyTool = PyTool.EP_NAME.findExtensionOrFail(RuffPyTool::class.java)
}
}
@@ -0,0 +1,242 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.python.community.execService.Args
import com.intellij.python.pytools.executeOn
import com.jetbrains.python.NON_INTERACTIVE_ROOT_TRACE_CONTEXT
import com.jetbrains.python.orLogException
import com.jetbrains.python.sdk.ModuleOrProject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.intellij.lang.annotations.Language
import org.jetbrains.annotations.Nls
/**
* Information about a Ruff configuration option.
*/
data class RuffConfigOptionInfo(
/**
* The documentation for the config option.
*/
@param:Nls val doc: String,
/**
* The default value for the config option.
*/
@param:Nls val default: String,
/**
* The type of value expected for the config option.
*/
@param:Nls val valueType: String,
/**
* The scope of the config option.
*/
@param:Nls val scope: String?,
/**
* An example of how to use the config option.
*/
@param:Nls val example: String,
/**
* Whether the config option is deprecated.
*/
@param:Nls val deprecated: String?,
)
/**
* Information about a Ruff rule.
*/
data class RuffRuleInfo(
@param:Nls val name: String,
@param:Nls val code: String,
@param:Nls val linter: String,
val summary: String,
val fix: String,
/**
* formatted in Markdown
*/
val explanation: String,
val preview: Boolean,
)
/**
* Service for fetching and storing Ruff configuration options and rule information.
*/
@Service(Service.Level.PROJECT)
class RuffService(val project: Project, val cs: CoroutineScope) {
private var _configOptions: Map<String, RuffConfigOptionInfo>? = null
var configOptions: Map<String, RuffConfigOptionInfo>
get() {
_configOptions?.let { return it }
cs.launch(NON_INTERACTIVE_ROOT_TRACE_CONTEXT) { gatherConfigOptionInformation() }
return _configOptions ?: emptyMap()
}
private set(value) {
_configOptions = value
}
private var _configOptionGroups: Set<String>? = null
var configOptionGroups: Set<String>
get() {
_configOptionGroups?.let { return it }
cs.launch(NON_INTERACTIVE_ROOT_TRACE_CONTEXT) { gatherConfigOptionInformation() }
return _configOptionGroups ?: emptySet()
}
private set(value) {
_configOptionGroups = value
}
private var _ruleInformation: Map<String, RuffRuleInfo>? = null
var ruleInformation: Map<String, RuffRuleInfo>
get() {
_ruleInformation?.let { return it }
cs.launch(NON_INTERACTIVE_ROOT_TRACE_CONTEXT) { gatherRuleInformation() }
return _ruleInformation ?: emptyMap()
}
private set(value) {
_ruleInformation = value
}
private var _linterInformation: Map<String, String>? = null
var linterInformation: Map<String, String>
get() {
_linterInformation?.let { return it }
cs.launch(NON_INTERACTIVE_ROOT_TRACE_CONTEXT) { gatherRuleInformation() }
return _linterInformation ?: emptyMap()
}
private set(value) {
_linterInformation = value
}
companion object {
private val LOG = logger<RuffService>()
}
/**
* Fetches all Ruff configuration options from the Ruff executable.
*
* @return A map of option paths to their information, or an empty map if fetching fails.
*/
suspend fun gatherConfigOptionInformation() {
val output = RuffPyTool.getInstance().executeOn(
ModuleOrProject.ProjectOnly(project),
Args("config", "--output-format=json")
).orLogException(LOG)
output?.let { loadConfigOptionInformation(output) }
}
/**
* Parses the JSON output from the Ruff config command containing all configuration options.
*
* @param jsonString The JSON output from the Ruff config command.
* @return A map of option paths to their information, or an empty map if parsing fails.
*/
fun loadConfigOptionInformation(@Language("JSON") jsonString: String) {
try {
val jsonElement = JsonParser.parseString(jsonString)
val jsonObject = jsonElement as? JsonObject ?: return
configOptions = buildMap {
jsonObject.entrySet().forEach { (key, value) ->
processConfigOption(key, value, this)
}
}
configOptionGroups = configOptions.keys.asSequence()
.filter { "." in it }
.map { option -> option.dropLastWhile { it != '.' }.dropLast(1) }
.toSet()
}
catch (e: Exception) {
LOG.warn("Error parsing Ruff config options JSON", e)
}
}
/**
* Processes a configuration option from the JSON output.
*
* @param path The path to the configuration option.
* @param value The JSON value for the configuration option.
* @param result The map to add the processed option to.
*/
private fun processConfigOption(path: String, value: JsonElement, result: MutableMap<String, RuffConfigOptionInfo>) {
if (value !is JsonObject) {
return
}
// Check if this is a config option, or a nested object
if (!value.has("doc")) return
try {
result[path] = RuffConfigOptionInfo(
doc = value.get("doc").asString,
default = value.get("default")?.let { if (it.isJsonNull) "" else it.asString } ?: "",
valueType = value.get("value_type")?.let { if (it.isJsonNull) "" else it.asString } ?: "",
scope = value.get("scope")?.let { if (it.isJsonNull) null else it.asString },
example = value.get("example")?.let { if (it.isJsonNull) "" else it.asString } ?: "",
deprecated = value.get("deprecated")?.let {
if (it.isJsonNull) null
else
it.asJsonObject.get("message").asString
}
)
}
catch (e: Exception) {
LOG.warn("Error parsing config option at path $path", e)
}
value.entrySet().forEach { (nestedKey, nestedValue) ->
if (nestedValue is JsonObject) {
processConfigOption("$path.$nestedKey", nestedValue, result)
}
}
}
/**
* Gathers rule information from the Ruff executable.
*/
suspend fun gatherRuleInformation() {
val output = RuffPyTool.getInstance().executeOn(
ModuleOrProject.ProjectOnly(project),
Args("rule", "--output-format", "json", "--all")
).orLogException(LOG)
output?.let { loadRuleInformation(it) }
}
/**
* Loads rule information from a JSON string.
* This method is primarily used for testing.
*
* @param jsonString The JSON string containing rule information.
*/
fun loadRuleInformation(@Language("JSON") jsonString: String) {
val jsonArray = JsonParser.parseString(jsonString) as? JsonArray ?: return
ruleInformation = jsonArray.associate { item ->
item as JsonObject
val code = item.get("code").asString
code to RuffRuleInfo(
name = item.get("name").asString,
code = code,
linter = item.get("linter").asString,
summary = item.get("summary").asString,
fix = item.get("fix").asString,
explanation = item.get("explanation").asString,
preview = item.get("preview").asBoolean,
)
}
linterInformation = ruleInformation.entries.associate { (key, value) ->
key.takeWhile { it.isLetter() } to value.linter
}
}
}
@@ -0,0 +1,13 @@
package com.intellij.python.ruff
import com.intellij.python.ruff.icons.PythonRuffIcons
import com.intellij.util.IconUtil
import javax.swing.Icon
object RuffUtil {
const val RUFF_WEBSITE: String = "https://docs.astral.sh/ruff"
fun getDefaultRuffIcon(): Icon {
return IconUtil.resizeSquared(PythonRuffIcons.Ruff, 16)
}
}
@@ -0,0 +1,22 @@
package com.intellij.python.ruff.codeinsight
import com.intellij.patterns.PatternCondition
import com.intellij.patterns.PlatformPatterns
import com.intellij.patterns.PsiElementPattern
import com.intellij.psi.PsiElement
import com.intellij.util.ProcessingContext
inline fun <reified T : PsiElement> PsiElementPattern.Capture<out PsiElement>.withParent(): PsiElementPattern.Capture<PsiElement> =
withParent(T::class.java) as PsiElementPattern.Capture<PsiElement>
inline fun <reified T : PsiElement> psiElement(): PsiElementPattern.Capture<T> =
PlatformPatterns.psiElement(T::class.java)
inline fun <reified T : PsiElement> PsiElementPattern.Capture<T>.with(
debugMethodName: String? = null,
crossinline accept: PatternCondition<T>.(T, ProcessingContext?) -> Boolean
): PsiElementPattern.Capture<T> =
with(object : PatternCondition<T>(debugMethodName) {
override fun accepts(t: T, context: ProcessingContext?) = accept(t, context)
})
@@ -0,0 +1,158 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff.codeinsight
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.ExternalAnnotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.module.ModuleUtilCore
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.openapi.util.TextRange
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.python.ruff.RuffBundle
import com.jetbrains.python.sdk.getExecutablePath
import com.jetbrains.python.sdk.pythonSdk
import org.toml.lang.psi.TomlFile
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.absolutePathString
import kotlin.io.path.div
import kotlin.io.path.writeText
/**
* External annotator for Ruff config files (.ruff.toml, ruff.toml, and pyproject.toml).
*/
class RuffConfigExternalAnnotator : ExternalAnnotator<RuffConfigExternalAnnotator.State, RuffConfigExternalAnnotator.Result>() {
companion object {
private val LOG = logger<RuffConfigExternalAnnotator>()
}
data class State(
val project: Project,
val file: PsiFile,
val virtualFile: VirtualFile,
val tempFile: Path,
)
class Result(
val errorLine: Int = -1,
val errorColumn: Int = -1,
val errorWidth: Int = -1,
val errorMessage: String = "",
)
/**
* Collects information from the file to be used for annotation.
*/
override fun collectInformation(file: PsiFile): State? {
// Only process TOML files
if (file !is TomlFile) return null
if (!file.isRuffConfigFile) return null
val virtualFile = file.virtualFile ?: return null
val tempDir = Files.createTempDirectory(null)
val tempFile = tempDir / file.name
tempFile.writeText(file.text)
return State(file.project, file, virtualFile, tempFile)
}
/**
* Executes the external tool and processes its output.
*/
override fun doAnnotate(state: State): Result? {
val module = ModuleUtilCore.findModuleForFile(state.virtualFile, state.project)
val sdk = module?.pythonSdk ?: state.project.pythonSdk ?: return null
val ruffExecutable = sdk.getExecutablePath("ruff") ?: return null.also {
LOG.info("Could not find ruff executable in SDK: ${sdk.name}")
}
val workingDir = state.virtualFile.parent.path
val processBuilder = ProcessBuilder(ruffExecutable.absolutePathString(), "check", "?", "--config", state.tempFile.absolutePathString())
.directory(File(workingDir))
.redirectErrorStream(true)
try {
val process = processBuilder.start()
val output = BufferedReader(InputStreamReader(process.inputStream)).use { it.readText() }
return parseOutput(output)
}
catch (e: Exception) {
LOG.warn("Error executing ruff check ? command", e)
return null
}
}
private fun parseOutput(output: String): Result? {
if (SUCCESS_PATTERN matches output) {
return null
}
val match = ERROR_PATTERN.find(output) ?: return null.also {
LOG.info("Could not parse `ruff check ?` output: $output")
}
return Result(
errorLine = match.groups[1]!!.value.toInt(),
errorColumn = match.groups[2]!!.value.toInt(),
errorWidth = match.groups[3]!!.value.length,
errorMessage = match.groups[4]!!.value
)
}
override fun apply(file: PsiFile, result: Result?, holder: AnnotationHolder) {
if (result == null) return
val document = file.viewProvider.document ?: return
val lineStartOffset = document.getLineStartOffset(result.errorLine - 1)
val startOffset = lineStartOffset + result.errorColumn - 1
val endOffset = startOffset + result.errorWidth
val message = RuffBundle.message("inspection.message.ruff.config.error", result.errorMessage)
holder.newAnnotation(HighlightSeverity.ERROR, message)
.range(TextRange(startOffset, endOffset))
.tooltip(buildTooltip(result))
.create()
}
@NlsSafe
private fun buildTooltip(result: Result): String {
return """
<html>
<body>
<p><b>Ruff config error:</b> ${result.errorMessage}</p>
</body>
</html>
""".trimIndent()
}
}
private val ERROR_PATTERN = Regex(
"""
ruff failed
(?:Cause: Failed to load configuration `.+`
)?Cause: Failed to parse .+
Cause: TOML parse error at line (\d+), column (\d+)
\s*\|
\d+ \| .*
\s*\|\s+(\^+)
(.+)
""".trimIndent()
)
// Pattern to match the success message from ruff check ? output
private val SUCCESS_PATTERN = Regex("\\?:1:1: E902 No such file or directory \\(os error 2\\)\\s+Found 1 error\\.")
@@ -0,0 +1,133 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff.codeinsight
import com.intellij.codeInsight.completion.BaseCompletionService
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProcessEx
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.components.service
import com.intellij.patterns.PlatformPatterns.psiElement
import com.intellij.psi.PsiElement
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.python.ruff.RuffService
import com.intellij.util.ProcessingContext
import org.toml.lang.psi.TomlFile
import org.toml.lang.psi.TomlKeySegment
import org.toml.lang.psi.TomlTable
import org.toml.lang.psi.TomlTableHeader
import kotlin.math.max
/**
* Provides code completion for Ruff configuration options in TOML files.
*/
class RuffConfigOptionCompletionContributor : CompletionContributor() {
init {
extend(
CompletionType.BASIC,
psiElement().inFile(
psiElement<TomlFile>().withName(".ruff.toml", "ruff.toml", PY_PROJECT_TOML)
),
RuffConfigOptionCompletionProvider()
)
}
}
/**
* Completion provider for Ruff configuration options.
* Provides completion items with both the option name and its documentation.
*/
private class RuffConfigOptionCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
val project = parameters.position.project
val ruffService = project.service<RuffService>()
val configOptions = ruffService.configOptions
val configGroups = ruffService.configOptionGroups
val position = parameters.position
if (configGroups.isEmpty()) {
return
}
/**
* Case 1: Completing inside a table header, e.g., [tool.ruff.lint.ru<caret>]
*/
fun completeInsideTableHeader(): Boolean {
var e: PsiElement? = position
while (e != null && e !is TomlTableHeader) e = e.parent
val header = e ?: return false
val key = header.key ?: return true
val full = key.text
if (header.containingFile.name == PY_PROJECT_TOML && !full.startsWith("tool.ruff")) return true
val ruffPathRaw = full.removePrefix("tool.ruff").trimStart('.')
val parts = if (ruffPathRaw.isEmpty()) emptyList() else ruffPathRaw.split('.')
val prefixParts = if (parts.isEmpty()) emptyList() else parts.dropLast(1)
addGroupSegments(prefixParts, parameters, result, configGroups)
return true
}
/**
* Case 2: Completing in the body of a \[tool.ruff...] table (new key at blank line, etc.)
*/
fun completeInTableBody(): Boolean {
var e: PsiElement? = position
if (position.parent !is TomlKeySegment) return false
while (e != null && e !is TomlTable) e = e.parent
val full = (position.parent as TomlKeySegment).fullName ?: return false
if (position.containingFile.name == PY_PROJECT_TOML && !full.startsWith("tool.ruff")) return false
val ruffPathRaw = full.removePrefix("tool.ruff").trimStart('.')
val prefixParts = if (ruffPathRaw.isEmpty()) emptyList() else ruffPathRaw.split('.').dropLast(1)
// First, groups (nested tables)
addGroupSegments(prefixParts, parameters, result, configGroups)
// then keys (leaf options)
for ((path, configInfo) in configOptions) {
val parts = path.split('.')
if (parts.dropLast(1) == prefixParts) {
val segment = parts.last()
result.addElement(
LookupElementBuilder.create(segment)
.withCaseSensitivity(false)
.withTypeText(configInfo.valueType, true)
.withTailText(" default: ${configInfo.default}", true)
)
}
}
return true
}
if (completeInsideTableHeader()) return
if (completeInTableBody()) return
}
/**
* Helper to add next-level group segments under a given prefix
*/
private fun addGroupSegments(prefixParts: List<String>, parameters: CompletionParameters, result: CompletionResultSet, configGroups: Set<String> = emptySet()) {
// remove junk words
(parameters.process as CompletionProcessEx).putUserData(BaseCompletionService.FORBID_WORD_COMPLETION, true)
// prevent the default stuff from appearing
result.stopHere()
for (option in configGroups) {
val optionParts = option.split('.')
if (optionParts.size == prefixParts.size + 1 && optionParts.take(max(0, prefixParts.size - 1)) == prefixParts.dropLast(1)) {
val segment = optionParts.last()
result.addElement(
LookupElementBuilder.create(segment)
.withCaseSensitivity(false)
)
}
}
}
}
@@ -0,0 +1,38 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff.codeinsight
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.components.service
import com.intellij.platform.backend.documentation.DocumentationTarget
import com.intellij.platform.backend.documentation.LookupElementDocumentationTargetProvider
import com.intellij.psi.PsiFile
import com.intellij.python.ruff.RuffService
import org.toml.lang.psi.TomlKeySegment
import org.toml.lang.psi.TomlKeyValue
import org.toml.lang.psi.TomlTable
/**
* Provides documentation for Ruff configuration option completion items.
* This provider serves the same documentation as [RuffConfigOptionDocumentationProvider]
* but for lookup elements in code completion.
*/
private class RuffConfigOptionCompletionDocumentationProvider : LookupElementDocumentationTargetProvider {
override fun documentationTarget(psiFile: PsiFile, element: LookupElement, offset: Int): DocumentationTarget? {
val anchorElement = (if (offset - 1 >= 0) psiFile.findElementAt(offset - 1) else psiFile.findElementAt(offset))
val anchorParent = anchorElement?.parent
val configPath = when (anchorParent) {
is TomlKeySegment ->
anchorParent.fullName?.dropLastWhile { it != '.' } ?: return null
is TomlKeyValue ->
anchorParent.fullName + "."
is PsiFile if anchorElement.prevSibling is TomlTable ->
(anchorElement.prevSibling as TomlTable).header.key?.fullName?.plus(".") ?: return null
else ->
return null
} + element.lookupString
val configInfo = psiFile.project.service<RuffService>().configOptions[configPath.removePrefix("tool.ruff.")] ?: return null
return RuffConfigOptionDocumentationTarget(configPath, configInfo, psiFile.project)
}
}
@@ -0,0 +1,173 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff.codeinsight
import com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
import com.intellij.model.Pointer
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.platform.backend.documentation.DocumentationResult
import com.intellij.platform.backend.documentation.DocumentationTarget
import com.intellij.platform.backend.documentation.DocumentationTargetProvider
import com.intellij.platform.backend.presentation.TargetPresentation
import com.intellij.psi.PsiFile
import com.intellij.python.ruff.RuffConfigOptionInfo
import com.intellij.python.ruff.RuffService
import com.intellij.python.ruff.RuffUtil
import org.jetbrains.annotations.Nls
import org.toml.lang.psi.TomlFile
import org.toml.lang.psi.TomlHeaderOwner
import org.toml.lang.psi.TomlKey
import org.toml.lang.psi.TomlKeySegment
import org.toml.lang.psi.TomlKeyValue
import org.toml.lang.psi.TomlTable
import org.toml.lang.psi.TomlTableHeader
/**
* Provides documentation tooltips for Ruff configuration options in ruff config files.
* When hovering over a configuration option key in a ruff config file, this provider
* shows the option documentation fetched from the Ruff executable.
*/
class RuffConfigOptionDocumentationProvider : DocumentationTargetProvider {
override fun documentationTargets(file: PsiFile, offset: Int): List<DocumentationTarget> {
if (file !is TomlFile) {
return emptyList()
}
if (!file.isRuffConfigFile) return emptyList()
val element = file.findElementAt(offset) ?: return emptyList()
val keySegment = element.parent as? TomlKeySegment ?: return emptyList()
val configPath = keySegment.ruffConfigPath() ?: return emptyList()
val configInfo = file.project.service<RuffService>().configOptions[configPath] ?: return emptyList()
return listOf(RuffConfigOptionDocumentationTarget(configPath, configInfo, file.project))
}
/**
* Gets the full path to a key in a TOML file.
*
* @param keyValue The key-value pair.
* @return The full path to the key (e.g., "tool.ruff.lint.select").
*/
private fun getFullPath(keyValue: TomlKeyValue): String {
val parent = keyValue.parent
if (parent is TomlTable) {
val tableHeader = parent.header
val tableKey = tableHeader.key
if (tableKey != null) {
return tableKey.text + "." + keyValue.key.text
}
}
return keyValue.key.text
}
}
/**
* Documentation target for a Ruff configuration option.
*/
class RuffConfigOptionDocumentationTarget(
@param:Nls private val configPath: String,
private val configInfo: RuffConfigOptionInfo,
private val project: Project,
) : DocumentationTarget {
override fun createPointer(): Pointer<out DocumentationTarget> {
return Pointer.hardPointer(this)
}
override fun computePresentation(): TargetPresentation {
return TargetPresentation.builder(configPath)
.icon(RuffUtil.getDefaultRuffIcon())
.presentableText(configPath)
.presentation()
}
override fun computeDocumentationHint(): @Nls String {
return formatConfigOptionDocumentationHint(configInfo)
}
override fun computeDocumentation(): DocumentationResult {
val html = formatConfigOptionDocumentation(configInfo, project)
return DocumentationResult.documentation(html)
}
/**
* Formats a hint for configuration option documentation.
*
* @param configInfo The configuration option information.
* @return A formatted hint for the documentation.
*/
@Nls
private fun formatConfigOptionDocumentationHint(configInfo: RuffConfigOptionInfo): String {
val firstLine = configInfo.doc.split("\n").firstOrNull() ?: ""
return "<b>$configPath</b><br>$firstLine"
}
/**
* Formats full documentation for a configuration option.
*
* @param configInfo The configuration option information.
* @param project The project.
* @return Formatted HTML documentation.
*/
@Nls
private fun formatConfigOptionDocumentation(configInfo: RuffConfigOptionInfo, project: Project): String {
@Suppress("HardCodedStringLiteral")
@Nls val markdown = buildString {
append("# ${configPath.split(".").last()}\n\n")
if (configInfo.deprecated != null) {
append("**This option is deprecated**: ${configInfo.deprecated}\n\n")
}
append(configInfo.doc)
append("\n\n")
append("**Type:** `${configInfo.valueType}`\n\n")
if (configInfo.default.isNotEmpty()) {
append("**Default:** `${configInfo.default}`\n\n")
}
if (configInfo.scope != null) {
append("**Scope:** ${configInfo.scope}\n\n")
}
if (configInfo.example.isNotEmpty()) {
append("**Example:**\n\n```toml\n${configInfo.example}\n```\n\n")
}
}
return DocMarkdownToHtmlConverter.convert(project, markdown)
}
}
val TomlKey.fullName: String
get() = parent.let {
when (it) {
is TomlKeyValue -> (it.parent as TomlTable).header.key!!.text + "." + text
is TomlTableHeader -> text
else -> error("invalid toml structure")
}
}
/**
* gets the full name up until this KeySegment
*/
val TomlKeySegment.fullName: String?
get() = parent.parent.let {
when (it) {
is TomlKeyValue -> when (val ancestor = it.parent) {
is TomlHeaderOwner -> ancestor.header.key!!.text + "." + parent.text.take(textRangeInParent.endOffset)
is TomlFile -> parent.text.take(textRangeInParent.endOffset)
else -> null
}
is TomlTableHeader -> parent.text.take(textRangeInParent.endOffset)
else -> null
}
}
@@ -0,0 +1,42 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff.codeinsight
import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandlerBase
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.paths.WebReference
import com.intellij.psi.PsiElement
import com.intellij.python.ruff.RuffUtil.RUFF_WEBSITE
import org.toml.lang.psi.TomlFile
import org.toml.lang.psi.TomlKeySegment
/**
* Handles documentation links for Ruff configuration options in TOML files.
* For example:
* ```toml
* [tool.ruff.lint]
* allowed-confusables = ["a"]
* ```
* Then it will link to `https://docs.astral.sh/ruff/settings/#lint_allowed-confusables`
*/
class RuffConfigOptionGotoDeclarationHandler : GotoDeclarationHandlerBase() {
override fun getGotoDeclarationTarget(sourceElement: PsiElement?, editor: Editor): PsiElement? {
sourceElement ?: return null
if (sourceElement.containingFile !is TomlFile) return null
return handleTomlFile(sourceElement)
}
private fun handleTomlFile(sourceElement: PsiElement): PsiElement? {
val file = sourceElement.containingFile as? TomlFile ?: return null
if (!file.isRuffConfigFile) return null
val keySegment = sourceElement.parent as? TomlKeySegment ?: return null
val configPath = keySegment.ruffConfigPath() ?: return null
val formattedPath = if (configPath.isEmpty()) "top-level" else configPath.replace(".", "_")
val url = "${RUFF_WEBSITE}/settings/#$formattedPath"
return WebReference(keySegment, url).resolve()
}
}
@@ -0,0 +1,107 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff.codeinsight
import com.intellij.markdown.utils.doc.DocMarkdownToHtmlConverter
import com.intellij.model.Pointer
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsSafe
import com.intellij.platform.backend.documentation.DocumentationResult
import com.intellij.platform.backend.documentation.DocumentationTarget
import com.intellij.platform.backend.presentation.TargetPresentation
import com.intellij.python.pyproject.PY_PROJECT_TOML
import com.intellij.python.ruff.RuffRuleInfo
import com.intellij.python.ruff.RuffUtil
import org.jetbrains.annotations.Nls
import org.toml.lang.psi.TomlArray
import org.toml.lang.psi.TomlFile
import org.toml.lang.psi.TomlKeySegment
import org.toml.lang.psi.TomlKeyValue
import org.toml.lang.psi.TomlLiteral
import org.toml.lang.psi.TomlTable
/**
* Utility class for Ruff documentation-related functionality.
* Contains common code shared between Ruff-related classes for handling TOML configuration and documentation.
*/
object RuffDocumentationUtil {
val CODE_PATTERN = Regex("[A-Z]+[0-9]{3}")
val RUFF_LINTER_PATTERN = Regex("[A-Z]+")
val SUPPRESSION_PATTERN = Regex("# (?:ruff: )?noqa: ?([A-Z]+[0-9]{3}(?:, ?([A-Z]+[0-9]{3}))*)?")
val ERROR_CODE_ARRAY_KEYS = setOf(
"select", "ignore", "fixable", "unfixable",
"extend-fixable", "extend-safe-fixes", "extend-select",
"extend-unsafe-fixes", "extend-ignore"
).map { "lint.$it" }
fun isRuffErrorCode(code: String): Boolean {
return CODE_PATTERN.matches(code)
}
fun TomlLiteral.isRuffCodeElement(): Boolean {
val array = this.parent as? TomlArray ?: return false
val keyValue = array.parent as? TomlKeyValue ?: return false
val configPath = (keyValue.key.children.last() as TomlKeySegment).ruffConfigPath().orEmpty()
return configPath in ERROR_CODE_ARRAY_KEYS || Regex("""lint\.(extend-)?per-file-ignores\..*""") matches configPath
}
@Nls
fun formatRuleDocumentationHint(ruleInfo: RuffRuleInfo): @NlsSafe String {
return "<b>${ruleInfo.name}</b>: ${ruleInfo.code}<br>${ruleInfo.summary}"
}
@Nls
fun formatRuleDocumentation(ruleInfo: RuffRuleInfo, project: Project): String {
@Suppress("HardCodedStringLiteral")
return DocMarkdownToHtmlConverter.convert(project, """
# ${ruleInfo.name} (${ruleInfo.code})
Derived from the **${ruleInfo.linter}** linter.
""".trimIndent() + ruleInfo.explanation)
}
}
class RuffRuleDocumentationTarget(private val ruleInfo: RuffRuleInfo, private val project: Project) : DocumentationTarget {
override fun createPointer(): Pointer<out DocumentationTarget> {
return Pointer.hardPointer(this)
}
override fun computePresentation(): TargetPresentation {
return TargetPresentation.builder(ruleInfo.code)
.icon(RuffUtil.getDefaultRuffIcon())
.presentableText("${ruleInfo.code}: ${ruleInfo.name}")
.presentation()
}
override fun computeDocumentationHint(): @Nls String {
return RuffDocumentationUtil.formatRuleDocumentationHint(ruleInfo)
}
override fun computeDocumentation(): DocumentationResult {
val html = RuffDocumentationUtil.formatRuleDocumentation(ruleInfo, project)
return DocumentationResult.documentation(html)
}
}
/**
* warning: is not robust against keys that contain dots (but no ruff keys do 🙂)
*/
val TomlKeyValue.fullName: String
get() = (parent as TomlTable).header.key?.text + "." + key.text
val TomlFile.isRuffConfigFile: Boolean
get() = name == PY_PROJECT_TOML || name == "ruff.toml" || name == ".ruff.toml"
fun TomlKeySegment.ruffConfigPath(): @NlsSafe String? {
val fullName = fullName ?: return null
return when {
this.containingFile.name != PY_PROJECT_TOML -> fullName
fullName.startsWith("tool.ruff.") -> fullName.removePrefix("tool.ruff.")
else -> null
}
}
@@ -0,0 +1,126 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff.codeinsight
import com.intellij.codeInsight.completion.CompletionContributor
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.CompletionProvider
import com.intellij.codeInsight.completion.CompletionResultSet
import com.intellij.codeInsight.completion.CompletionType
import com.intellij.codeInsight.completion.PrioritizedLookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.components.service
import com.intellij.patterns.PlatformPatterns.psiComment
import com.intellij.patterns.PlatformPatterns.psiElement
import com.intellij.patterns.PsiElementPattern
import com.intellij.patterns.StandardPatterns
import com.intellij.psi.PsiElement
import com.intellij.python.ruff.RuffService
import com.intellij.python.ruff.codeinsight.RuffDocumentationUtil.isRuffCodeElement
import com.intellij.util.ProcessingContext
import com.jetbrains.python.psi.PyFile
import org.toml.lang.TomlLanguage
import org.toml.lang.psi.TomlArray
import org.toml.lang.psi.TomlKeyValue
import org.toml.lang.psi.TomlLiteral
/**
* Provides code completion for Ruff error codes in Python comments and TOML files.
* In the completion item, both the error code and its name are shown.
*/
class RuffErrorCodeCompletionContributor : CompletionContributor() {
init {
val provider = RuffErrorCodeCompletionProvider()
// Python `ruff: noqa:` completion
extend(
CompletionType.BASIC,
psiComment().withParent<PyFile>().withText(StandardPatterns.string().startsWith("# ruff: noqa:")),
provider
)
// Python `noqa: ` completion
extend(
CompletionType.BASIC,
psiComment().withText(StandardPatterns.string().startsWith("# noqa:")),
provider
)
// TOML file completion
extend(
CompletionType.BASIC,
tomlRuffLintPattern(),
provider
)
}
/**
* Pattern to match TOML elements where Ruff error codes can be completed.
*/
private fun tomlRuffLintPattern(): PsiElementPattern.Capture<PsiElement> {
return psiElement()
.withLanguage(TomlLanguage)
.withParent(
psiElement<TomlLiteral>()
.with("ruffLintKey") { literal, _ ->
literal.isRuffCodeElement()
}
.withParent(
psiElement<TomlArray>()
.withParent<TomlKeyValue>()
)
)
}
/**
* Completion provider for Ruff error codes.
* Provides completion items with both the error code and its name.
*/
private class RuffErrorCodeCompletionProvider : CompletionProvider<CompletionParameters>() {
override fun addCompletions(parameters: CompletionParameters, context: ProcessingContext, result: CompletionResultSet) {
// prevent the default stuff from appearing
result.stopHere()
val project = parameters.position.project
val ruffService = project.service<RuffService>()
for ((code, ruleInfo) in ruffService.ruleInformation) {
result.addElement(
LookupElementBuilder.create(code)
.withPresentableText(code)
.withTypeText(ruleInfo.name, true)
.withTailText(" (${ruleInfo.linter})", true)
.withLookupString(ruleInfo.name)
.withCaseSensitivity(true),
)
}
// Add TOML config exclusive options
if (parameters.position.containingFile.language == TomlLanguage) {
// Add special "ALL" code for selecting all rules
result.addElement(
PrioritizedLookupElement.withPriority(
LookupElementBuilder.create("ALL")
.withPresentableText("ALL")
.withTypeText("All Ruff rules", true)
.withLookupString("everything")
.withLookupString("all rules")
.withLookupString("all linters")
.withTailText(" (all rules)", true)
.withCaseSensitivity(true),
1.0, // want it to appear at the top of the results
)
)
// Add linter prefixes
for ((prefix, linterName) in ruffService.linterInformation) {
result.addElement(
LookupElementBuilder.create(prefix)
.withPresentableText(prefix)
.withTypeText("All $linterName rules", true)
.withTailText(" ($linterName)", true)
.withLookupString(linterName)
.withCaseSensitivity(true)
)
}
}
}
}
}
@@ -0,0 +1,26 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.python.ruff.codeinsight
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.components.service
import com.intellij.platform.backend.documentation.DocumentationTarget
import com.intellij.platform.backend.documentation.LookupElementDocumentationTargetProvider
import com.intellij.psi.PsiFile
import com.intellij.python.ruff.RuffService
/**
* Provides documentation for Ruff error code completion items.
*/
class RuffErrorCodeCompletionDocumentationProvider : LookupElementDocumentationTargetProvider {
override fun documentationTarget(psiFile: PsiFile, element: LookupElement, offset: Int): DocumentationTarget? {
val code = element.lookupString
if (!RuffDocumentationUtil.isRuffErrorCode(code)) {
return null
}
val ruleInfo = psiFile.project.service<RuffService>().ruleInformation[code] ?: return null
return RuffRuleDocumentationTarget(ruleInfo, psiFile.project)
}
}

Some files were not shown because too many files have changed in this diff Show More