PY-90092 lsp-tools: inline lsp plugin into community
IJ-MR-207996 GitOrigin-RevId: 8a8619c679d0991f0a3c8a1cf3c425e3d1eaa4c4
@@ -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" />
|
||||
|
||||
@@ -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)."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
owner 'PyCharm Codeinsight'
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 151 B |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 8.3 KiB |
|
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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||