├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── ci-gradle.properties ├── ci-reporter.yml ├── labels.json ├── no-response.yml ├── stale.yml └── workflows │ └── ci.yml ├── .gitignore ├── .idea ├── checkstyle-idea.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml └── copyright │ ├── Apache_2_0.xml │ └── profiles_settings.xml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASING.md ├── art ├── actions1.png ├── actions2.png ├── pylint-inspection-severity.png ├── pylint-pycharm.png └── pylint-settings.png ├── build.gradle ├── config └── checkstyle │ ├── checkstyle-suppressions.xml │ └── checkstyle.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── src └── main │ ├── java │ └── com │ │ └── leinardi │ │ └── pycharm │ │ └── pylint │ │ ├── PylintAnnotator.java │ │ ├── PylintBatchInspection.java │ │ ├── PylintBundle.java │ │ ├── PylintConfigService.java │ │ ├── PylintConfigurable.java │ │ ├── PylintPlugin.java │ │ ├── actions │ │ ├── BaseAction.java │ │ ├── ClearAll.java │ │ ├── Close.java │ │ ├── CollapseAll.java │ │ ├── DisplayConvention.java │ │ ├── DisplayErrors.java │ │ ├── DisplayInfo.java │ │ ├── DisplayRefactor.java │ │ ├── DisplayWarnings.java │ │ ├── ExpandAll.java │ │ ├── ScanCurrentChangeList.java │ │ ├── ScanCurrentFile.java │ │ ├── ScanEverythingAction.java │ │ ├── ScanModifiedFiles.java │ │ ├── ScanModule.java │ │ ├── ScanProject.java │ │ ├── ScanSourceRootsAction.java │ │ ├── ScrollToSource.java │ │ ├── Settings.java │ │ ├── StopCheck.java │ │ └── ToolWindowAccess.java │ │ ├── checker │ │ ├── CreateScannableFileAction.java │ │ ├── Problem.java │ │ ├── PsiFileValidator.java │ │ ├── ScanFiles.java │ │ ├── ScannableFile.java │ │ ├── ScannerListener.java │ │ └── UiFeedbackScannerListener.java │ │ ├── exception │ │ ├── PylintPluginException.java │ │ ├── PylintPluginParseException.java │ │ ├── PylintServiceException.java │ │ └── PylintToolException.java │ │ ├── handlers │ │ ├── ScanFilesBeforeCheckinHandler.java │ │ └── ScanFilesBeforeCheckinHandlerFactory.java │ │ ├── plapi │ │ ├── Issue.java │ │ ├── ProcessResultsThread.java │ │ ├── PylintRunner.java │ │ └── SeverityLevel.java │ │ ├── toolwindow │ │ ├── PylintToolWindowFactory.java │ │ ├── PylintToolWindowPanel.java │ │ ├── ResultTreeModel.java │ │ ├── ResultTreeNode.java │ │ ├── ResultTreeRenderer.java │ │ └── TogglableTreeNode.java │ │ ├── ui │ │ ├── PylintConfigPanel.form │ │ └── PylintConfigPanel.java │ │ └── util │ │ ├── Async.java │ │ ├── Exceptions.java │ │ ├── FileTypes.java │ │ ├── Icons.java │ │ ├── Notifications.java │ │ ├── OS.java │ │ ├── PyPackageManagerUtil.java │ │ ├── Strings.java │ │ ├── TempDirProvider.java │ │ └── VfUtil.java │ └── resources │ ├── META-INF │ └── plugin.xml │ ├── com │ └── leinardi │ │ └── pycharm │ │ └── pylint │ │ ├── PylintBundle.properties │ │ └── images │ │ ├── pylint.png │ │ ├── pylint@2x.png │ │ ├── pylint@2x_dark.png │ │ ├── pylint_dark.png │ │ ├── pythonFile.png │ │ └── pythonFile@2x.png │ └── inspectionDescriptions │ └── Pylint.html └── versions-plugin.gradle /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at roberto@leinardi.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Considering that this project is actively maintained, contributions of all types are welcome. 3 | 4 | 5 | ## Opening issues 6 | Open a new issue when: 7 | - you notice an unwanted behavior 8 | - you want a new feature implemented 9 | - you have just some doubts 10 | 11 | To open a new issue, please use the provided issue template and fill it out as much as possible. 12 | If you are interested to an existing issue, feel free to comment the issue or subscribe to it. 13 | 14 | 15 | ## Submitting pull requests 16 | If you want to fix a bug or implement a new feature, feel free to submit a new pull request. 17 | To submit a pull request, you have to fork this repository and fill the PR template. 18 | When you want to submit a pull request, remember to: 19 | - follow this project's code style 20 | - run `./gradlew clean check` 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | 12 | ### Step 1: Are you in the right place? 13 | - [ ] I have verified there are no duplicate active or recent bugs, questions, or requests 14 | - [ ] I have verified that I am using the latest version of the plugin. 15 | 16 | ### Step 2: Describe your environment 17 | - Plugin version: `?` 18 | - PyCharm/IDEA version: `?` 19 | - Pylint version: `?` 20 | 21 | ### Step 3: Describe the problem: 22 | #### Steps to reproduce: 23 | 24 | 1. _____ 25 | 2. _____ 26 | 3. _____ 27 | 28 | 31 | #### Observed Results: 32 | 33 | * 34 | 35 | 38 | #### Expected Results: 39 | 40 | * 41 | 42 | #### Relevant Code: 43 | 46 | ```java 47 | System.out.println("Hello, world!"); 48 | ``` 49 | 50 | 54 | ```Gradle 55 | java.lang.RuntimeException: This is an example Exception log 56 | at com.leinardi.HelloWorld 57 | at com.leinardi.HelloWorld$ThisIsNotARealLog 58 | at bigcorporate.app.Instrumentation.callActivityOnResume(Instrumentation.kt) 59 | ``` 60 | 61 | 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | ### First time contributor checklist 7 | 8 | - [ ] I have read [how to contribute](/.github/CONTRIBUTING.md) to this project 9 | - [ ] I have read [the code of conduct](/.github/CODE_OF_CONDUCT.md) to this project 10 | 11 | ### Contributor checklist 12 | 13 | - [ ] I am targeting the `master` branch (and **not** the `release` branch) 14 | - [ ] I am using the provided [codeStyleConfig.xml](/.idea/codeStyles) 15 | - [ ] I have tested my contribution on these versions of PyCharm/IDEA: 16 | * PyCharm Community XXXX.Y.Z 17 | * IntelliJ IDEA XXXX.Y.Z 18 | - [ ] My contribution is fully baked and ready to be merged as is 19 | - [ ] I ensure that all the open issues my contribution fixes are mentioned in the commit message of my first commit 20 | using the `Fixes #1234` [syntax](https://help.github.com/articles/closing-issues-using-keywords/) 21 | 22 | ---------- 23 | 24 | ### Description 25 | 30 | 31 | ### Type of Changes 32 | 33 | | | Type | 34 | | ------------- | ------------- | 35 | | ✓ | :bug: Bug fix | 36 | | ✓ | :sparkles: New feature | 37 | | ✓ | :hammer: Refactoring | 38 | | ✓ | :scroll: Docs | 39 | 40 | ### Related Issue 41 | 42 | 48 | -------------------------------------------------------------------------------- /.github/ci-gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023 Roberto Leinardi. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | org.gradle.daemon=false 18 | org.gradle.workers.max=2 19 | -------------------------------------------------------------------------------- /.github/ci-reporter.yml: -------------------------------------------------------------------------------- 1 | # Set to false to create a new comment instead of updating the app's first one 2 | updateComment: true 3 | 4 | # Use a custom string, or set to false to disable 5 | before: "Unfortunately, the [{{ provider }} build]({{ targetUrl }}) is failing as of {{ commit }}. Here's the output:" 6 | 7 | # Use a custom string, or set to false to disable 8 | after: "If you need help with this issue, don't hesitate to ask a maintainer of the project!" 9 | -------------------------------------------------------------------------------- /.github/labels.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Priority: Critical", 4 | "color": "#D32F2F" 5 | }, 6 | { 7 | "name": "Priority: High", 8 | "color": "#FF9800" 9 | }, 10 | { 11 | "name": "Priority: Low", 12 | "color": "#4CAF50" 13 | }, 14 | { 15 | "name": "Priority: Medium", 16 | "color": "#CDDC39" 17 | }, 18 | { 19 | "name": "Status: Abandoned", 20 | "color": "#000000" 21 | }, 22 | { 23 | "name": "Status: Accepted", 24 | "color": "#4CAF50" 25 | }, 26 | { 27 | "name": "Status: Available", 28 | "color": "#C8E6C9" 29 | }, 30 | { 31 | "name": "Status: Blocked", 32 | "color": "#D32F2F" 33 | }, 34 | { 35 | "name": "Status: Completed", 36 | "color": "#009688" 37 | }, 38 | { 39 | "name": "Status: In Progress", 40 | "color": "#E0E0E0" 41 | }, 42 | { 43 | "name": "Status: Info needed", 44 | "color": "#5C6BC0" 45 | }, 46 | { 47 | "name": "Status: On Hold", 48 | "color": "#D32F2F" 49 | }, 50 | { 51 | "name": "Status: Pending", 52 | "color": "#FFF176" 53 | }, 54 | { 55 | "name": "Status: Review Needed", 56 | "color": "#CDDC39" 57 | }, 58 | { 59 | "name": "Status: Revision Needed", 60 | "color": "#D32F2F" 61 | }, 62 | { 63 | "name": "Status: Stale", 64 | "color": "#9E9E9E" 65 | }, 66 | { 67 | "name": "triage", 68 | "color": "#81D4FA" 69 | }, 70 | { 71 | "name": "Type: Bug", 72 | "color": "#D32F2F" 73 | }, 74 | { 75 | "name": "Type: Enhancement", 76 | "color": "#546E7A" 77 | }, 78 | { 79 | "name": "Type: Maintenance", 80 | "color": "#CDDC39" 81 | }, 82 | { 83 | "name": "Type: Question", 84 | "color": "#673AB7" 85 | } 86 | ] 87 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 21 5 | # Label requiring a response 6 | responseRequiredLabel: "Status: Info needed" 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response 10 | to our request for more information from the original author. With only the 11 | information that is currently in the issue, we don't have enough information 12 | to take action. Please reach out if you have or find the answers we need so 13 | that we can investigate further. 14 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 60 5 | 6 | # Number of days of inactivity before a stale Issue or Pull Request is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: false 9 | 10 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 11 | exemptLabels: 12 | - pinned 13 | - security 14 | 15 | # Set to true to ignore issues in a project (defaults to false) 16 | exemptProjects: false 17 | 18 | # Set to true to ignore issues in a milestone (defaults to false) 19 | exemptMilestones: false 20 | 21 | # Label to use when marking as stale 22 | staleLabel: "Status: Stale" 23 | 24 | # Comment to post when marking as stale. Set to `false` to disable 25 | markComment: > 26 | This issue has been automatically marked as stale because it has not had activity in the last 60 days. 27 | 28 | # Comment to post when removing the stale label. 29 | # unmarkComment: > 30 | # Your comment here. 31 | 32 | # Comment to post when closing a stale Issue or Pull Request. 33 | # closeComment: > 34 | # Your comment here. 35 | 36 | # Limit the number of actions per hour, from 1-30. Default is 30 37 | limitPerRun: 30 38 | 39 | # Limit to only `issues` or `pulls` 40 | # only: issues 41 | 42 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 43 | # pulls: 44 | # daysUntilStale: 30 45 | # markComment: > 46 | # This pull request has been automatically marked as stale because it has not had 47 | # recent activity. It will be closed if no further activity occurs. Thank you 48 | # for your contributions. 49 | 50 | # issues: 51 | # exemptLabels: 52 | # - confirmed 53 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - '**.md' 7 | push: 8 | branches: 9 | - master 10 | - release 11 | paths-ignore: 12 | - '**.md' 13 | workflow_dispatch: 14 | 15 | # This allows a subsequently queued workflow run to interrupt previous runs 16 | concurrency: 17 | group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | build: 22 | name: Build 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 10 25 | env: 26 | TERM: dumb 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup JDK 35 | uses: actions/setup-java@v4 36 | with: 37 | distribution: 'temurin' 38 | java-version: 17.0.10 39 | 40 | - name: Setup Gradle 41 | uses: gradle/actions/setup-gradle@v4 42 | 43 | - name: Copy CI gradle.properties 44 | run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties 45 | 46 | - name: Validate gradle wrapper 47 | uses: gradle/wrapper-validation-action@v1 48 | 49 | ## Actual task 50 | - name: Check and build 51 | run: ./gradlew check buildPlugin 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | .idea/* 3 | !.idea/codeStyles/ 4 | !.idea/copyright/ 5 | !.idea/checkstyle-idea.xml 6 | **/*.iml 7 | .gradle/ 8 | **/build/ 9 | **/out/ 10 | -------------------------------------------------------------------------------- /.idea/checkstyle-idea.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8.29 5 | JavaOnly 6 | 10 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 23 | 24 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/copyright/Apache_2_0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: openjdk8 3 | sudo: true 4 | 5 | os: 6 | - linux 7 | addons: 8 | apt_packages: 9 | - pandoc 10 | 11 | before_cache: 12 | # Do not cache a few Gradle files/directories (see https://docs.travis-ci.com/user/languages/java/#Caching) 13 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 14 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 15 | 16 | cache: 17 | directories: 18 | # Gradle dependencies 19 | - $HOME/.gradle/caches/ 20 | - $HOME/.gradle/wrapper/ 21 | 22 | install: 23 | - true 24 | 25 | before_script: 26 | - sed -i -e 's/pycharmPath=/#pycharmPath=/g' gradle.properties 27 | - sed -i -e 's/#pythonPlugin=/pythonPlugin=/g' gradle.properties 28 | 29 | script: 30 | - set -o pipefail 31 | - ./gradlew clean check --profile --continue 2>&1 | tee build.log 32 | - set +o pipefail 33 | - | 34 | if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 35 | ./gradlew violationCommentsToGitHub -DGITHUB_PULLREQUESTID=$TRAVIS_PULL_REQUEST -DGITHUB_OAUTH2TOKEN=$GITHUB_OAUTH2TOKEN --info 36 | fi 37 | - pandoc `ls -1rt build/reports/profile/profile-*.html | tail -n1` -t plain 38 | - ./gradlew dependencyUpdate 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | **[0.16.4] 2024-10-05** 2 | 3 | - Defined action update thread for actions for ScrollToSource 4 | 5 | **[0.16.3] 2024-10-05** 6 | 7 | - Fix #103 #105: Defined action update thread for actions 8 | - New: Min IDEA version raised from PC-2022.1.4 to PC-2023.3.6 9 | - Several dependency updates 10 | 11 | **[0.16.2] 2023-10-16** 12 | 13 | - Fix compatibility issues 14 | 15 | **[0.16.1] 2023-10-09** 16 | 17 | - Fix several compatibility issues 18 | 19 | **[0.16.0] 2023-09-02** 20 | 21 | - Possible fixe for #82: Pylint doesn't detect remote interpreter 22 | - Possible fixe for #58: On Windows allow pylint to use pylint installed in WSL 23 | - Respect Pylint path field and don't force usage of project interpreter 24 | 25 | **[0.15.0] 2023-04-24** 26 | 27 | - Fixed Icons not visible in new Jetbrains UI 28 | - New: Min IDEA version raised from PC-2021.2 to PC-2022.1.4 29 | - Several dependency updates 30 | 31 | **[0.14.0] 2022-02-26** 32 | 33 | - New: Improved executable auto-detection on Windows 34 | - New: Make plugin hot-reloadable 35 | - New: Show notification when PyLint exits abnormally 36 | - Several dependency updates 37 | 38 | **[0.13.1] 2021-12-06** 39 | 40 | - New: Minimum compatibility version raised to 201.8743 41 | 42 | **[0.13.0] 2021-12-05** 43 | 44 | - Fixed #11: Stopping old instances of PyLint when requesting new ones (a huge thanks to @intgr for fixing this issue 45 | for [mypy-pycharm](https://github.com/leinardi/mypy-pycharm), making the port for this plugin trivial!) 46 | - New: Min IDEA version raised from 2018 to PC-2021.2.3 47 | - Several dependency updates 48 | 49 | **[0.12.2] 2020-04-25** 50 | 51 | - Fixed #61: Changed module/project icons to be compatible with EAPs of IDEA 2020.1 52 | 53 | **[0.12.1] 2020-02-04** 54 | 55 | - Fixed regression generating several errors in Event Log during inspection 56 | 57 | **[0.12.0] 2020-02-01** 58 | 59 | - New: Min IDEA version raised from 2016 to 2018 60 | - New: Tidied up deprecations in the 2018 SDK 61 | - New: Fixed possible deadlock during inspection 62 | 63 | **[0.11.2] 2020-02-01** 64 | 65 | - Fix #42: no linting when using `--init-hook` in the parameter field 66 | 67 | **[0.11.1] 2019-09-15** 68 | 69 | - New: Improved error handling 70 | 71 | **[0.11.0] 2019-01-02** 72 | 73 | - PyLint real-time inspection disabled by default as numerous users find running it in the background has a negative 74 | impact on their system performance 75 | - Fix #29: Implementing a better virtualenv detection 76 | 77 | **[0.10.2] 2018-09-25** 78 | 79 | - Fix #26: SyntaxError: Non-UTF-8 code starting with '\x90' when interpreter is set on Windows 80 | 81 | **[0.10.1] 2018-09-21** 82 | 83 | - Fix #22: PyLint absolute path not working on Windows 84 | - Fix #24: PyLint auto-detection not working on Windows 85 | 86 | **[0.10.0] 2018-09-12** 87 | 88 | - Fix #7: Support linting inside current Virtualenv 89 | - Fix #19: Don't show the 'syntax-error' message for real-time scan 90 | - New: Improved Pylint auto-detection 91 | - New: Option to install Pylint if missing 92 | - New: Settings button now opens File | Settings | Pylint 93 | - New: Minimum compatibility version raised to 163.15529 94 | 95 | **[0.9.0] 2018-09-04** 96 | 97 | - Info: I am aware of the venv import error but for now I only have a partial solution. If you want to help or just get 98 | updates on the issue, click [here](https://github.com/leinardi/pylint-pycharm/issues/7). 99 | - New: Showing better info to the user if Pylint is missing 100 | - New: Added ability to optionally specify a pylintrc 101 | - New: Added ability to optionally specify Pylint arguments 102 | 103 | **[0.8.0] 2018-09-01** 104 | 105 | - New: Added missing type `info` 106 | - New: Autoscroll to Source is disabled by default 107 | 108 | **[0.7.1] 2018-09-01** 109 | 110 | - New: Project has moved to [https://github.com/leinardi/pylint-pycharm](https://github.com/leinardi/pylint-pycharm) 111 | 112 | **[0.7.0] 2018-08-31** 113 | 114 | - New: Added Scan Files Before Checkin 115 | - New: Added real-time scanning! 116 | - New: UX based on Checkstyle-IDEA plugin. 117 | - New: Plugin id and name changed (please remove manually the old plugin) 118 | 119 | **[0.1.0]** 120 | 121 | - Initial release. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pylint-pycharm 2 | [![GitHub (pre-)release](https://img.shields.io/github/release/leinardi/pylint-pycharm/all.svg?style=plastic)](https://github.com/leinardi/pylint-pycharm/releases) 3 | [![Travis](https://img.shields.io/travis/leinardi/pylint-pycharm/master.svg?style=plastic)](https://travis-ci.org/leinardi/pylint-pycharm) 4 | [![GitHub license](https://img.shields.io/github/license/leinardi/pylint-pycharm.svg?style=plastic)](https://github.com/leinardi/pylint-pycharm/blob/master/LICENSE) 5 | [![Stars](https://img.shields.io/github/stars/leinardi/pylint-pycharm.svg?style=social&label=Stars)](https://github.com/leinardi/pylint-pycharm/stargazers) 6 | 7 | This plugin provides both real-time and on-demand scanning of Python files with Pylint from within PyCharm/IDEA. 8 | 9 | Pylint is a Python source code analyzer which looks for programming errors, 10 | helps enforcing a coding standard and sniffs for some code smells 11 | (as defined in Martin Fowler's Refactoring book). 12 | 13 | ![pylint plugin screenshot](https://github.com/leinardi/pylint-pycharm/blob/master/art/pylint-pycharm.png) 14 | 15 | ## Installation steps 16 | 1. In the **Settings/Preferences** dialog (CTRL+Alt+S), click **Plugins**. The [Plugins page](https://www.jetbrains.com/help/pycharm/plugins-settings.html) opens. 17 | 2. Click **Browse repositories**. 18 | 3. In the [Browse Repositories dialog](https://www.jetbrains.com/help/pycharm/browse-repositories-dialog.html) that opens, right-click on the plugin named **Pylint** and select **Download and Install**. 19 | 4. Confirm your intention to download and install the selected plugin. 20 | 5. Click **Close**. 21 | 6. Click **OK** in the **Settings** dialog and restart PyCharm for the changes to take effect. 22 | 23 | ## Configuration 24 | 25 | The only configuration needed is to set the path to Pylint executable, and only if is not already 26 | inside the PATH environment variable. 27 | 28 | To reach the Plugin configuration screen you can open **Settings/Preferences** dialog (CTRL+Alt+S), click **Other Settings** and then **Pylint** or simply click the gear icon from the side bar of the Pylint tool window. 29 | 30 | To change the path to your Pylint executable you can either type the path directly or use 31 | the Browse button to open a file selection dialog. 32 | 33 | Once you changed the path you should press the Test button to check if the plugin is able to run 34 | the executable. 35 | 36 | ![plugin settings screenshot](https://github.com/leinardi/pylint-pycharm/blob/master/art/pylint-settings.png) 37 | 38 | ### Real-time inspection disabled by default 39 | The plugin real-time inspection is disabled by default as numerous users find running `mix credo` in the background has 40 | a negative impact on their system performance. If you like to try enabling the annotation, you can turn it on: 41 | 42 | 1. Preferences > Editor > Inspections > Pylint 43 | 2. Check "Pylint real-time scan" 44 | 45 | ### Inspection severity 46 | 47 | By default, Pylint message severity is set to Warning. It is possible to change the severity level 48 | by going to **Settings/Preferences** dialog (CTRL+Alt+S) -> **Editor** -> **Inspections** -> **Pylint** -> **Severity**: 49 | 50 | ![plugin inspection severity screenshot](https://github.com/leinardi/pylint-pycharm/blob/master/art/pylint-inspection-severity.png) 51 | 52 | ## Usage 53 | 54 | ![plugin actions screenshot](https://github.com/leinardi/pylint-pycharm/blob/master/art/actions1.png) 55 | ![plugin actions screenshot](https://github.com/leinardi/pylint-pycharm/blob/master/art/actions2.png) 56 | 57 | ## FAQ 58 | ### How can I prevent the code inspection to run on a specific folder? 59 | 60 | The easiest way to ignore a specific folder is to mark it as Excluded from PyCharm/IDEA: 61 | 62 | 1. Open PyCharm/IDEA Settings -> *your project* -> Project structure 63 | 2. Select the directory you want to exclude 64 | 3. Click the Excluded button (red folder icon) 65 | 66 | More info [here](https://www.jetbrains.com/help/pycharm/configuring-folders-within-a-content-root.html#mark). 67 | 68 | ### The name of the plugin is `pylint-pycharm`, can I also use it with IntelliJ IDEA? 69 | 70 | This plugin officially supports only PyCharm, but it should work also on IntelliJ IDEA 71 | if you have the [Python Community Edition](https://plugins.jetbrains.com/plugin/7322-python-community-edition) 72 | plugin installed. If it does not work, feel free to open a bug on the [issue tracker](https://github.com/leinardi/pylint-pycharm/issues). 73 | 74 | ### I like this plugin, how can I support it? 75 | 76 | The best way to support this plugin is to rate it on the [JetBrains Plugin Repository page](https://plugins.jetbrains.com/plugin/11084-pylint) and to star this project on GitHub. 77 | Feedback is always welcome: if you found a bug or would like to suggest a feature, 78 | feel free to open an issue on the [issue tracker](https://github.com/leinardi/pylint-pycharm/issues). If your feedback doesn't fall in the previous categories, 79 | you can always leave a comment on the [Plugin Repository page](https://plugins.jetbrains.com/plugin/11084-pylint). 80 | 81 | ## Acknowledgements 82 | _If I have seen further it is by standing on the shoulders of Giants - Isaac Newton_ 83 | 84 | A huge thanks: 85 | - to the project [CheckStyle-IDEA](https://github.com/jshiell/checkstyle-idea), 86 | which code and architecture I have heavily used when developing this plugin. 87 | - to @intgr, for [the contribution to this project](https://github.com/leinardi/pylint-pycharm/pulls?q=is%3Apr+author%3Aintgr) and to [mypy-pycharm](https://github.com/leinardi/mypy-pycharm/pulls?q=is%3Apr+author%3Aintgr) 88 | 89 | ## License 90 | 91 | ``` 92 | Copyright 2021 Roberto Leinardi. 93 | 94 | Licensed to the Apache Software Foundation (ASF) under one or more contributor 95 | license agreements. See the NOTICE file distributed with this work for 96 | additional information regarding copyright ownership. The ASF licenses this 97 | file to you under the Apache License, Version 2.0 (the "License"); you may not 98 | use this file except in compliance with the License. You may obtain a copy of 99 | the License at 100 | 101 | http://www.apache.org/licenses/LICENSE-2.0 102 | 103 | Unless required by applicable law or agreed to in writing, software 104 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 105 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 106 | License for the specific language governing permissions and limitations under 107 | the License. 108 | ``` 109 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Bump the `version` property in `gradle.properties` based on Major.Minor.Patch naming scheme 4 | 2. Update `CHANGELOG.md` for the impending release. 5 | 3. Update the `README.md` with the new changes (if necessary). 6 | 4. `./gradlew clean buildPlugin` 7 | 5. `git commit -am "Prepare for release x.y.z"` (where x.y.z is the version you set in step 1) 8 | 6. `git push` 9 | 7. Create a new release on Github 10 | 1. Tag version `x.y.z` (`git tag -s x.y.z && git push --tags`) 11 | 2. Release title `x.y.z` 12 | 3. Paste the content from `CHANGELOG.md` as the description 13 | 4. Upload the `build/distributions/pylint-plugin-x.y.z.zip` 14 | 8. Create a PR from [master](../../tree/master) to [release](../../tree/release) 15 | 9. `./gradlew publishPlugin` 16 | -------------------------------------------------------------------------------- /art/actions1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/art/actions1.png -------------------------------------------------------------------------------- /art/actions2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/art/actions2.png -------------------------------------------------------------------------------- /art/pylint-inspection-severity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/art/pylint-inspection-severity.png -------------------------------------------------------------------------------- /art/pylint-pycharm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/art/pylint-pycharm.png -------------------------------------------------------------------------------- /art/pylint-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/art/pylint-settings.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import org.commonmark.node.Node 18 | import org.commonmark.parser.Parser 19 | import org.commonmark.renderer.html.HtmlRenderer 20 | import se.bjurr.violations.comments.github.plugin.gradle.ViolationCommentsToGitHubTask 21 | 22 | buildscript { 23 | repositories { 24 | mavenCentral() 25 | gradlePluginPortal() 26 | } 27 | dependencies { 28 | classpath 'com.atlassian.commonmark:commonmark:0.17.0' 29 | // NOTE: Do not place your application dependencies here; they belong 30 | // in the individual module build.gradle files 31 | } 32 | } 33 | 34 | plugins { 35 | id 'org.jetbrains.intellij' version '1.17.4' 36 | id 'net.ltgt.errorprone' version '4.0.1' 37 | id 'idea' 38 | id 'java' 39 | id 'checkstyle' 40 | id 'com.github.ben-manes.versions' version '0.51.0' 41 | id 'se.bjurr.violations.violation-comments-to-github-gradle-plugin' version '1.70.0' 42 | } 43 | 44 | checkstyle { 45 | ignoreFailures = false // Whether this task will ignore failures and continue running the build. 46 | configFile rootProject.file('config/checkstyle/checkstyle.xml') 47 | // The Checkstyle configuration file to use. 48 | toolVersion = '9.1' // The version of Checkstyle you want to be used 49 | } 50 | 51 | def hasPycharmPath = project.hasProperty('pycharmPath') 52 | def props = new Properties() 53 | rootProject.file('src/main/resources/com/leinardi/pycharm/pylint/PylintBundle.properties') 54 | .withInputStream { 55 | props.load(it) 56 | } 57 | 58 | gradle.projectsEvaluated { 59 | tasks.withType(JavaCompile).configureEach { 60 | options.compilerArgs << '-Xlint:unchecked' << '-Xlint:deprecation' 61 | } 62 | } 63 | 64 | group 'com.leinardi.pycharm' 65 | version version 66 | 67 | sourceCompatibility = JavaVersion.VERSION_17 68 | targetCompatibility = JavaVersion.VERSION_17 69 | 70 | intellij { 71 | version = ideVersion 72 | pluginName = props.getProperty('plugin.name').toLowerCase().replace(' ', '-') 73 | downloadSources = Boolean.valueOf(downloadIdeaSources) 74 | updateSinceUntilBuild = true 75 | if (hasPycharmPath) { 76 | alternativeIdePath = pycharmPath 77 | } 78 | plugins = [pythonPlugin] 79 | } 80 | 81 | runIde { 82 | systemProperties.put("idea.log.debug.categories", "#com.leinardi.pycharm.mypy") 83 | // Log verbose information when dynamic plugin unloading fails 84 | systemProperties.put("ide.plugins.snapshot.on.unload.fail", "true") 85 | } 86 | 87 | // Causes error popup when building while sandbox IDE is open. Disable in development 88 | if (System.getenv('DEVELOP')) { 89 | buildSearchableOptions.enabled = false 90 | } 91 | 92 | patchPluginXml { 93 | version = project.property('version') 94 | sinceBuild = project.property('sinceBuild') 95 | untilBuild = project.property('untilBuild') 96 | pluginDescription = props.getProperty('plugin.Pylint-PyCharm.description') 97 | changeNotes = getChangelogHtml() 98 | } 99 | 100 | publishPlugin { 101 | def publishToken = project.hasProperty('jetbrainsPublishToken') ? jetbrainsPublishToken : "" 102 | token.set(publishToken) 103 | channels = [publishChannels] 104 | } 105 | 106 | repositories { 107 | mavenCentral() 108 | gradlePluginPortal() 109 | if (hasPycharmPath) { 110 | flatDir { 111 | dirs "$pycharmPath/lib" 112 | } 113 | } 114 | } 115 | 116 | dependencies { 117 | if (hasPycharmPath) { 118 | compileOnly name: 'pycharm' 119 | } 120 | implementation 'com.squareup.moshi:moshi:1.14.0' 121 | errorprone 'com.google.errorprone:error_prone_core:2.21.1' 122 | } 123 | 124 | def getChangelogHtml() { 125 | Parser parser = Parser.builder().build() 126 | Node document = parser.parseReader(rootProject.file('CHANGELOG.md').newReader()) 127 | HtmlRenderer renderer = HtmlRenderer.builder().build() 128 | renderer.render(document.firstChild.next) 129 | } 130 | 131 | check.dependsOn(verifyPlugin) 132 | 133 | task violationCommentsToGitHub(type: ViolationCommentsToGitHubTask) { 134 | repositoryOwner = "leinardi" 135 | repositoryName = "pylint-pycharm" 136 | pullRequestId = System.properties['GITHUB_PULLREQUESTID'] 137 | oAuth2Token = System.properties['GITHUB_OAUTH2TOKEN'] 138 | gitHubUrl = "https://api.github.com/" 139 | createCommentWithAllSingleFileComments = false 140 | createSingleFileComments = true 141 | commentOnlyChangedContent = true 142 | violations = [ 143 | ["FINDBUGS", ".", ".*/reports/findbugs/.*\\.xml\$", "Findbugs"], 144 | ["CHECKSTYLE", ".", ".*/reports/checkstyle/.*debug\\.xml\$", "Checkstyle"], 145 | ["ANDROIDLINT", ".", ".*/reports/lint-results.*\\.xml\$", "Android Lint"], 146 | ["GOOGLEERRORPRONE", ".", ".*/build.log\$", "Error Prone"] 147 | ] 148 | } 149 | 150 | wrapper { 151 | gradleVersion = "8.10.2" 152 | distributionType = Wrapper.DistributionType.ALL 153 | } 154 | -------------------------------------------------------------------------------- /config/checkstyle/checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023 Roberto Leinardi. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | version=0.16.4 17 | ideVersion=PC-2023.3.6 18 | pythonPlugin=python-ce 19 | sinceBuild=233.15325 20 | untilBuild= 21 | downloadIdeaSources=true 22 | publishUsername=leinardi 23 | publishChannels=Stable 24 | ####################################################################################################################### 25 | # To run PyCharm from a custom path, uncomment and change the following line 26 | # pycharmPath=/home/leinardi/pycharm-community 27 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | rootProject.name = 'pylint-pycharm' 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/PylintBatchInspection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint; 18 | 19 | import com.intellij.codeInspection.LocalInspectionTool; 20 | import com.intellij.codeInspection.ex.ExternalAnnotatorBatchInspection; 21 | import org.jetbrains.annotations.NotNull; 22 | 23 | /** 24 | * By itself, the `PylintAnnotator` class does not provide support for the explicit "Inspect code" feature. 25 | *

26 | * This class uses `ExternalAnnotatorBatchInspection` middleware to provides that functionality. 27 | *

28 | * Modeled after `com.jetbrains.python.inspections.PyPep8Inspection` 29 | */ 30 | public class PylintBatchInspection extends LocalInspectionTool implements ExternalAnnotatorBatchInspection { 31 | public static final String INSPECTION_SHORT_NAME = "Pylint"; 32 | 33 | @Override 34 | public @NotNull String getShortName() { 35 | return INSPECTION_SHORT_NAME; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/PylintBundle.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint; 18 | 19 | import com.intellij.AbstractBundle; 20 | import org.jetbrains.annotations.NonNls; 21 | import org.jetbrains.annotations.PropertyKey; 22 | 23 | public final class PylintBundle extends AbstractBundle { 24 | @NonNls 25 | private static final String BUNDLE = "com.leinardi.pycharm.pylint.PylintBundle"; 26 | 27 | private static final PylintBundle INSTANCE = new PylintBundle(); 28 | 29 | private PylintBundle() { 30 | super(BUNDLE); 31 | } 32 | 33 | public static String message(@PropertyKey(resourceBundle = BUNDLE) final String key, final Object... params) { 34 | return INSTANCE.getMessage(key, params); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/PylintConfigService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint; 18 | 19 | import com.intellij.openapi.components.PersistentStateComponent; 20 | import com.intellij.openapi.components.State; 21 | import com.intellij.openapi.components.Storage; 22 | import com.intellij.openapi.project.Project; 23 | import com.intellij.util.xmlb.XmlSerializerUtil; 24 | import org.jetbrains.annotations.NotNull; 25 | import org.jetbrains.annotations.Nullable; 26 | 27 | @State(name = "PylintConfigService", storages = {@Storage("pylint.xml")}) 28 | public class PylintConfigService implements PersistentStateComponent { 29 | public PylintConfigService() { 30 | customPylintPath = ""; 31 | pylintArguments = ""; 32 | pylintrcPath = ""; 33 | scanBeforeCheckin = true; 34 | } 35 | 36 | private String customPylintPath; 37 | private String pylintrcPath; 38 | private String pylintArguments; 39 | private boolean scanBeforeCheckin; 40 | 41 | public String getCustomPylintPath() { 42 | return customPylintPath; 43 | } 44 | 45 | public void setCustomPylintPath(String pathToPylint) { 46 | this.customPylintPath = pathToPylint; 47 | } 48 | 49 | public String getPylintrcPath() { 50 | return pylintrcPath; 51 | } 52 | 53 | public void setPylintrcPath(String pathToPylintrcFile) { 54 | this.pylintrcPath = pathToPylintrcFile; 55 | } 56 | 57 | public String getPylintArguments() { 58 | return pylintArguments; 59 | } 60 | 61 | public void setPylintArguments(String pylintArguments) { 62 | this.pylintArguments = pylintArguments; 63 | } 64 | 65 | public boolean isScanBeforeCheckin() { 66 | return scanBeforeCheckin; 67 | } 68 | 69 | public void setScanBeforeCheckin(boolean scanBeforeCheckin) { 70 | this.scanBeforeCheckin = scanBeforeCheckin; 71 | } 72 | 73 | @Nullable 74 | @Override 75 | public PylintConfigService getState() { 76 | return this; 77 | } 78 | 79 | @Override 80 | public void loadState(@NotNull PylintConfigService config) { 81 | XmlSerializerUtil.copyBean(config, this); 82 | } 83 | 84 | @Nullable 85 | public static PylintConfigService getInstance(Project project) { 86 | return project.getService(PylintConfigService.class); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/PylintConfigurable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint; 18 | 19 | import com.intellij.openapi.diagnostic.Logger; 20 | import com.intellij.openapi.options.Configurable; 21 | import com.intellij.openapi.project.Project; 22 | import com.leinardi.pycharm.pylint.ui.PylintConfigPanel; 23 | import org.jetbrains.annotations.NotNull; 24 | 25 | import javax.swing.JComponent; 26 | 27 | /** 28 | * The "configurable component" required by PyCharm to provide a Swing form for inclusion into the 'Settings' 29 | * dialog. Registered in {@code plugin.xml} as a {@code projectConfigurable} extension. 30 | */ 31 | public class PylintConfigurable implements Configurable { 32 | private static final Logger LOG = Logger.getInstance(PylintConfigurable.class); 33 | 34 | private final PylintConfigPanel configPanel; 35 | private final PylintConfigService pylintConfigService; 36 | 37 | public PylintConfigurable(@NotNull final Project project) { 38 | this(project, new PylintConfigPanel(project)); 39 | } 40 | 41 | PylintConfigurable(@NotNull final Project project, 42 | @NotNull final PylintConfigPanel configPanel) { 43 | this.configPanel = configPanel; 44 | pylintConfigService = PylintConfigService.getInstance(project); 45 | } 46 | 47 | @Override 48 | public String getDisplayName() { 49 | return PylintBundle.message("plugin.configuration-name"); 50 | } 51 | 52 | @Override 53 | public String getHelpTopic() { 54 | return null; 55 | } 56 | 57 | @Override 58 | public JComponent createComponent() { 59 | reset(); 60 | return configPanel.getPanel(); 61 | } 62 | 63 | @Override 64 | public void reset() { 65 | } 66 | 67 | @Override 68 | public boolean isModified() { 69 | boolean result = !configPanel.getPylintPath().equals(pylintConfigService.getCustomPylintPath()) 70 | || !configPanel.getPylintrcPath().equals(pylintConfigService.getPylintrcPath()) 71 | || !configPanel.getPylintArguments().equals(pylintConfigService.getPylintArguments()); 72 | if (LOG.isDebugEnabled()) { 73 | LOG.debug("Has config changed? " + result); 74 | } 75 | return result; 76 | } 77 | 78 | @Override 79 | public void apply() { 80 | pylintConfigService.setCustomPylintPath(configPanel.getPylintPath()); 81 | pylintConfigService.setPylintrcPath(configPanel.getPylintrcPath()); 82 | pylintConfigService.setPylintArguments(configPanel.getPylintArguments()); 83 | } 84 | 85 | @Override 86 | public void disposeUIResources() { 87 | // do nothing 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/PylintPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint; 18 | 19 | import com.intellij.openapi.components.Service; 20 | import com.intellij.openapi.diagnostic.Logger; 21 | import com.intellij.openapi.project.Project; 22 | import com.intellij.openapi.project.ProjectUtil; 23 | import com.intellij.openapi.vfs.VirtualFile; 24 | import com.intellij.psi.PsiFile; 25 | import com.leinardi.pycharm.pylint.checker.Problem; 26 | import com.leinardi.pycharm.pylint.checker.ScanFiles; 27 | import com.leinardi.pycharm.pylint.checker.ScannerListener; 28 | import com.leinardi.pycharm.pylint.checker.UiFeedbackScannerListener; 29 | import com.leinardi.pycharm.pylint.exception.PylintPluginException; 30 | import com.leinardi.pycharm.pylint.util.Async; 31 | import org.jetbrains.annotations.NotNull; 32 | import org.jetbrains.annotations.Nullable; 33 | 34 | import java.io.File; 35 | import java.util.Collections; 36 | import java.util.HashSet; 37 | import java.util.List; 38 | import java.util.Map; 39 | import java.util.Set; 40 | import java.util.concurrent.Future; 41 | 42 | import static com.leinardi.pycharm.pylint.util.Async.whenFinished; 43 | 44 | /** 45 | * Main class for the Pylint scanning plug-in. 46 | */ 47 | @Service 48 | public final class PylintPlugin { 49 | 50 | private static final Logger LOG = com.intellij.openapi.diagnostic.Logger.getInstance(PylintPlugin.class); 51 | 52 | private static final long NO_TIMEOUT = 0L; 53 | 54 | private final Set> checksInProgress = new HashSet<>(); 55 | private final Project project; 56 | 57 | /** 58 | * Construct a plug-in instance for the given project. 59 | * 60 | * @param project the current project. 61 | */ 62 | public PylintPlugin(@NotNull final Project project) { 63 | this.project = project; 64 | 65 | LOG.info("Pylint Plugin loaded with project base dir: \"" + getProjectPath() + "\""); 66 | 67 | } 68 | 69 | public Project getProject() { 70 | return project; 71 | } 72 | 73 | @Nullable 74 | private File getProjectPath() { 75 | final VirtualFile baseDir = ProjectUtil.guessProjectDir(project); 76 | if (baseDir == null) { 77 | return null; 78 | } 79 | 80 | return new File(baseDir.getPath()); 81 | } 82 | 83 | /** 84 | * Is a scan in progress? 85 | *

86 | * This is only expected to be called from the event thread. 87 | * 88 | * @return true if a scan is in progress. 89 | */ 90 | public boolean isScanInProgress() { 91 | synchronized (checksInProgress) { 92 | return !checksInProgress.isEmpty(); 93 | } 94 | } 95 | 96 | public static void processErrorAndLog(@NotNull final String action, @NotNull final Throwable e) { 97 | LOG.warn(action + " failed", e); 98 | } 99 | 100 | private Future checkInProgress(final Future checkFuture) { 101 | synchronized (checksInProgress) { 102 | if (!checkFuture.isDone()) { 103 | checksInProgress.add(checkFuture); 104 | } 105 | } 106 | return checkFuture; 107 | } 108 | 109 | public void stopChecks() { 110 | synchronized (checksInProgress) { 111 | checksInProgress.forEach(task -> task.cancel(true)); 112 | checksInProgress.clear(); 113 | } 114 | } 115 | 116 | public void checkComplete(final Future task) { 117 | if (task == null) { 118 | return; 119 | } 120 | 121 | synchronized (checksInProgress) { 122 | checksInProgress.remove(task); 123 | } 124 | } 125 | 126 | @SuppressWarnings("FutureReturnValueIgnored") 127 | public void asyncScanFiles(final List files) { 128 | LOG.info("Scanning current file(s)."); 129 | 130 | if (files == null || files.isEmpty()) { 131 | LOG.debug("No files provided."); 132 | return; 133 | } 134 | 135 | final ScanFiles checkFiles = new ScanFiles(this, files); 136 | checkFiles.addListener(new UiFeedbackScannerListener(this)); 137 | runAsyncCheck(checkFiles); 138 | } 139 | 140 | public Map> scanFiles(@NotNull final List files) { 141 | if (files.isEmpty()) { 142 | return Collections.emptyMap(); 143 | } 144 | 145 | try { 146 | return whenFinished(runAsyncCheck(new ScanFiles(this, files)), NO_TIMEOUT).get(); 147 | } catch (final Throwable e) { 148 | LOG.warn("ERROR scanning files", e); 149 | return Collections.emptyMap(); 150 | } 151 | } 152 | 153 | private Future>> runAsyncCheck(final ScanFiles checker) { 154 | final Future>> checkFilesFuture = 155 | checkInProgress(Async.executeOnPooledThread(checker)); 156 | checker.addListener(new ScanCompletionTracker(checkFilesFuture)); 157 | return checkFilesFuture; 158 | } 159 | 160 | private class ScanCompletionTracker implements ScannerListener { 161 | 162 | private final Future>> future; 163 | 164 | ScanCompletionTracker(final Future>> future) { 165 | this.future = future; 166 | } 167 | 168 | @Override 169 | public void scanStarting(final List filesToScan) { 170 | } 171 | 172 | @Override 173 | public void filesScanned(final int count) { 174 | } 175 | 176 | @Override 177 | public void scanCompletedSuccessfully( 178 | final Map> scanResults) { 179 | checkComplete(future); 180 | } 181 | 182 | @Override 183 | public void scanFailedWithError(final PylintPluginException error) { 184 | checkComplete(future); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/BaseAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.actionSystem.Presentation; 22 | import com.intellij.openapi.diagnostic.Logger; 23 | import com.intellij.openapi.project.DumbAwareAction; 24 | import com.intellij.openapi.project.Project; 25 | import com.intellij.openapi.vfs.VfsUtilCore; 26 | import com.intellij.openapi.vfs.VirtualFile; 27 | import com.intellij.openapi.vfs.VirtualFileVisitor; 28 | import com.intellij.openapi.wm.ToolWindow; 29 | import com.leinardi.pycharm.pylint.PylintBundle; 30 | import com.leinardi.pycharm.pylint.PylintPlugin; 31 | import org.jetbrains.annotations.NotNull; 32 | 33 | import java.util.Optional; 34 | import java.util.concurrent.atomic.AtomicBoolean; 35 | 36 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 37 | import static java.util.Optional.ofNullable; 38 | 39 | /** 40 | * Base class for plug-in actions. 41 | */ 42 | public abstract class BaseAction extends DumbAwareAction { 43 | 44 | private static final Logger LOG = Logger.getInstance(BaseAction.class); 45 | 46 | @Override 47 | public @NotNull ActionUpdateThread getActionUpdateThread() { 48 | return ActionUpdateThread.EDT; 49 | } 50 | 51 | @Override 52 | public void update(final @NotNull AnActionEvent event) { 53 | try { 54 | final Project project = getEventProject(event); 55 | final Presentation presentation = event.getPresentation(); 56 | 57 | // check a project is loaded 58 | if (project == null) { 59 | presentation.setEnabled(false); 60 | presentation.setVisible(false); 61 | 62 | return; 63 | } 64 | 65 | final PylintPlugin pylintPlugin = project.getService(PylintPlugin.class); 66 | if (pylintPlugin == null) { 67 | throw new IllegalStateException("Couldn't get pylint plugin"); 68 | } 69 | 70 | // check if tool window is registered 71 | final ToolWindow toolWindow = ToolWindowAccess.toolWindow(project); 72 | if (toolWindow == null) { 73 | presentation.setEnabled(false); 74 | presentation.setVisible(false); 75 | 76 | return; 77 | } 78 | 79 | // enable 80 | presentation.setEnabled(toolWindow.isAvailable()); 81 | presentation.setVisible(true); 82 | } catch (Throwable e) { 83 | LOG.warn("Action update failed", e); 84 | } 85 | } 86 | 87 | protected void setProgressText(final ToolWindow toolWindow, final String progressTextKey) { 88 | actOnToolWindowPanel(toolWindow, panel -> panel.setProgressText(PylintBundle.message(progressTextKey))); 89 | } 90 | 91 | protected Optional project(@NotNull final AnActionEvent event) { 92 | return ofNullable(getEventProject(event)); 93 | } 94 | 95 | protected boolean containsAtLeastOneFile(@NotNull final VirtualFile... files) { 96 | final var result = new AtomicBoolean(false); 97 | for (VirtualFile file : files) { 98 | VfsUtilCore.visitChildrenRecursively(file, new VirtualFileVisitor<>() { 99 | @Override 100 | public @NotNull Result visitFileEx(@NotNull final VirtualFile file) { 101 | if (!file.isDirectory() && file.isValid()) { 102 | result.set(true); 103 | return SKIP_CHILDREN; 104 | } 105 | return CONTINUE; 106 | } 107 | }); 108 | 109 | if (result.get()) { 110 | break; 111 | } 112 | } 113 | return result.get(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ClearAll.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.AnActionEvent; 20 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 21 | import org.jetbrains.annotations.NotNull; 22 | 23 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 24 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 25 | 26 | /** 27 | * Action to toggle error display in tool window. 28 | */ 29 | public class ClearAll extends BaseAction { 30 | 31 | @Override 32 | public void actionPerformed(final @NotNull AnActionEvent event) { 33 | project(event).ifPresent(project -> actOnToolWindowPanel(toolWindow(project), 34 | PylintToolWindowPanel::clearResult)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/Close.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.AnActionEvent; 20 | import org.jetbrains.annotations.NotNull; 21 | 22 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 23 | 24 | /** 25 | * Action to close the tool window. 26 | */ 27 | public class Close extends BaseAction { 28 | 29 | @Override 30 | public void actionPerformed(final @NotNull AnActionEvent event) { 31 | project(event).ifPresent(project -> toolWindow(project).hide(null)); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/CollapseAll.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.AnActionEvent; 20 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 21 | import org.jetbrains.annotations.NotNull; 22 | 23 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 24 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 25 | 26 | /** 27 | * Action to collapse all nodes in the results window. 28 | */ 29 | public class CollapseAll extends BaseAction { 30 | 31 | @Override 32 | public void actionPerformed(final @NotNull AnActionEvent event) { 33 | project(event).ifPresent(project -> actOnToolWindowPanel(toolWindow(project), 34 | PylintToolWindowPanel::collapseTree)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/DisplayConvention.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.project.DumbAwareToggleAction; 22 | import com.intellij.openapi.project.Project; 23 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | import java.util.Objects; 27 | 28 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 29 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.getFromToolWindowPanel; 30 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 31 | 32 | /** 33 | * Action to toggle error display in tool window. 34 | */ 35 | public class DisplayConvention extends DumbAwareToggleAction { 36 | 37 | @Override 38 | public @NotNull ActionUpdateThread getActionUpdateThread() { 39 | return ActionUpdateThread.EDT; 40 | } 41 | 42 | @Override 43 | public boolean isSelected(final @NotNull AnActionEvent event) { 44 | final Project project = getEventProject(event); 45 | if (project == null) { 46 | return false; 47 | } 48 | 49 | Boolean displayingConvention = getFromToolWindowPanel(toolWindow(project), 50 | PylintToolWindowPanel::isDisplayingConvention); 51 | return Objects.requireNonNullElse(displayingConvention, false); 52 | } 53 | 54 | @Override 55 | public void setSelected(final @NotNull AnActionEvent event, final boolean selected) { 56 | final Project project = getEventProject(event); 57 | if (project == null) { 58 | return; 59 | } 60 | 61 | actOnToolWindowPanel(toolWindow(project), panel -> { 62 | panel.setDisplayingConvention(selected); 63 | panel.filterDisplayedResults(); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/DisplayErrors.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.project.DumbAwareToggleAction; 22 | import com.intellij.openapi.project.Project; 23 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | import java.util.Objects; 27 | 28 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 29 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.getFromToolWindowPanel; 30 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 31 | 32 | /** 33 | * Action to toggle error display in tool window. 34 | */ 35 | public class DisplayErrors extends DumbAwareToggleAction { 36 | 37 | @Override 38 | public @NotNull ActionUpdateThread getActionUpdateThread() { 39 | return ActionUpdateThread.EDT; 40 | } 41 | 42 | @Override 43 | public boolean isSelected(final @NotNull AnActionEvent event) { 44 | final Project project = getEventProject(event); 45 | if (project == null) { 46 | return false; 47 | } 48 | 49 | Boolean displayingErrors = getFromToolWindowPanel(toolWindow(project), 50 | PylintToolWindowPanel::isDisplayingErrors); 51 | return Objects.requireNonNullElse(displayingErrors, false); 52 | } 53 | 54 | @Override 55 | public void setSelected(final @NotNull AnActionEvent event, final boolean selected) { 56 | final Project project = getEventProject(event); 57 | if (project == null) { 58 | return; 59 | } 60 | 61 | actOnToolWindowPanel(toolWindow(project), panel -> { 62 | panel.setDisplayingErrors(selected); 63 | panel.filterDisplayedResults(); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/DisplayInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.project.DumbAwareToggleAction; 22 | import com.intellij.openapi.project.Project; 23 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | import java.util.Objects; 27 | 28 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 29 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.getFromToolWindowPanel; 30 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 31 | 32 | /** 33 | * Action to toggle error display in tool window. 34 | */ 35 | public class DisplayInfo extends DumbAwareToggleAction { 36 | 37 | @Override 38 | public @NotNull ActionUpdateThread getActionUpdateThread() { 39 | return ActionUpdateThread.EDT; 40 | } 41 | 42 | @Override 43 | public boolean isSelected(final @NotNull AnActionEvent event) { 44 | final Project project = getEventProject(event); 45 | if (project == null) { 46 | return false; 47 | } 48 | 49 | Boolean displayingInfo = getFromToolWindowPanel(toolWindow(project), 50 | PylintToolWindowPanel::isDisplayingInfo); 51 | return Objects.requireNonNullElse(displayingInfo, false); 52 | } 53 | 54 | @Override 55 | public void setSelected(final @NotNull AnActionEvent event, final boolean selected) { 56 | final Project project = getEventProject(event); 57 | if (project == null) { 58 | return; 59 | } 60 | 61 | actOnToolWindowPanel(toolWindow(project), panel -> { 62 | panel.setDisplayingInfo(selected); 63 | panel.filterDisplayedResults(); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/DisplayRefactor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.project.DumbAwareToggleAction; 22 | import com.intellij.openapi.project.Project; 23 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | import java.util.Objects; 27 | 28 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 29 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.getFromToolWindowPanel; 30 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 31 | 32 | /** 33 | * Action to toggle error display in tool window. 34 | */ 35 | public class DisplayRefactor extends DumbAwareToggleAction { 36 | 37 | @Override 38 | public @NotNull ActionUpdateThread getActionUpdateThread() { 39 | return ActionUpdateThread.EDT; 40 | } 41 | 42 | @Override 43 | public boolean isSelected(final @NotNull AnActionEvent event) { 44 | final Project project = getEventProject(event); 45 | if (project == null) { 46 | return false; 47 | } 48 | 49 | Boolean displayingRefactors = getFromToolWindowPanel(toolWindow(project), 50 | PylintToolWindowPanel::isDisplayingRefactors); 51 | return Objects.requireNonNullElse(displayingRefactors, false); 52 | } 53 | 54 | @Override 55 | public void setSelected(final @NotNull AnActionEvent event, final boolean selected) { 56 | final Project project = getEventProject(event); 57 | if (project == null) { 58 | return; 59 | } 60 | 61 | actOnToolWindowPanel(toolWindow(project), panel -> { 62 | panel.setDisplayingRefactors(selected); 63 | panel.filterDisplayedResults(); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/DisplayWarnings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.project.DumbAwareToggleAction; 22 | import com.intellij.openapi.project.Project; 23 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | import java.util.Objects; 27 | 28 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 29 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.getFromToolWindowPanel; 30 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 31 | 32 | /** 33 | * Action to toggle error display in tool window. 34 | */ 35 | public class DisplayWarnings extends DumbAwareToggleAction { 36 | 37 | @Override 38 | public @NotNull ActionUpdateThread getActionUpdateThread() { 39 | return ActionUpdateThread.EDT; 40 | } 41 | 42 | @Override 43 | public boolean isSelected(final @NotNull AnActionEvent event) { 44 | final Project project = getEventProject(event); 45 | if (project == null) { 46 | return false; 47 | } 48 | 49 | Boolean displayingWarnings = getFromToolWindowPanel(toolWindow(project), 50 | PylintToolWindowPanel::isDisplayingWarnings); 51 | return Objects.requireNonNullElse(displayingWarnings, false); 52 | } 53 | 54 | @Override 55 | public void setSelected(final @NotNull AnActionEvent event, final boolean selected) { 56 | final Project project = getEventProject(event); 57 | if (project == null) { 58 | return; 59 | } 60 | 61 | actOnToolWindowPanel(toolWindow(project), panel -> { 62 | panel.setDisplayingWarnings(selected); 63 | panel.filterDisplayedResults(); 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ExpandAll.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.AnActionEvent; 20 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 21 | import org.jetbrains.annotations.NotNull; 22 | 23 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.actOnToolWindowPanel; 24 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 25 | 26 | /** 27 | * Action to expand all nodes in the results window. 28 | */ 29 | public class ExpandAll extends BaseAction { 30 | 31 | @Override 32 | public void actionPerformed(final @NotNull AnActionEvent event) { 33 | project(event).ifPresent(project -> actOnToolWindowPanel(toolWindow(project), 34 | PylintToolWindowPanel::expandTree)); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ScanCurrentChangeList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.actionSystem.Presentation; 22 | import com.intellij.openapi.diagnostic.Logger; 23 | import com.intellij.openapi.vcs.changes.Change; 24 | import com.intellij.openapi.vcs.changes.ChangeListManager; 25 | import com.intellij.openapi.vcs.changes.LocalChangeList; 26 | import com.intellij.openapi.vfs.VirtualFile; 27 | import com.leinardi.pycharm.pylint.PylintPlugin; 28 | import com.leinardi.pycharm.pylint.util.VfUtil; 29 | import org.jetbrains.annotations.NotNull; 30 | 31 | import java.util.ArrayList; 32 | import java.util.Collection; 33 | import java.util.HashSet; 34 | import java.util.List; 35 | 36 | /** 37 | * Scan files in the current change-list. 38 | */ 39 | public class ScanCurrentChangeList extends BaseAction { 40 | 41 | private static final Logger LOG = Logger.getInstance(ScanCurrentChangeList.class); 42 | 43 | @Override 44 | public @NotNull ActionUpdateThread getActionUpdateThread() { 45 | return ActionUpdateThread.BGT; 46 | } 47 | 48 | @Override 49 | public final void actionPerformed(final @NotNull AnActionEvent event) { 50 | project(event).ifPresent(project -> { 51 | try { 52 | final ChangeListManager changeListManager = ChangeListManager.getInstance(project); 53 | project.getService(PylintPlugin.class) 54 | .asyncScanFiles(VfUtil.filterOnlyPythonProjectFiles(project, 55 | filesFor(changeListManager.getDefaultChangeList()))); 56 | } catch (Throwable e) { 57 | LOG.warn("Modified files scan failed", e); 58 | } 59 | }); 60 | } 61 | 62 | private List filesFor(final LocalChangeList changeList) { 63 | if (changeList == null || changeList.getChanges() == null) { 64 | return new ArrayList<>(); 65 | } 66 | 67 | final Collection filesInChanges = new HashSet<>(); 68 | for (Change change : changeList.getChanges()) { 69 | if (change.getVirtualFile() != null) { 70 | filesInChanges.add(change.getVirtualFile()); 71 | } 72 | } 73 | 74 | return new ArrayList<>(filesInChanges); 75 | } 76 | 77 | @Override 78 | public void update(final AnActionEvent event) { 79 | final Presentation presentation = event.getPresentation(); 80 | 81 | project(event).ifPresentOrElse(project -> { 82 | try { 83 | final PylintPlugin mypyPlugin = project.getService(PylintPlugin.class); 84 | if (mypyPlugin == null) { 85 | throw new IllegalStateException("Couldn't get mypy plugin"); 86 | } 87 | 88 | final LocalChangeList changeList = ChangeListManager.getInstance(project).getDefaultChangeList(); 89 | if (changeList.getChanges() == null || changeList.getChanges().isEmpty()) { 90 | presentation.setEnabled(false); 91 | } else { 92 | presentation.setEnabled(!mypyPlugin.isScanInProgress()); 93 | } 94 | } catch (Throwable e) { 95 | LOG.warn("Button update failed.", e); 96 | } 97 | }, () -> presentation.setEnabled(false)); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ScanCurrentFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.actionSystem.Presentation; 22 | import com.intellij.openapi.editor.Editor; 23 | import com.intellij.openapi.fileEditor.FileDocumentManager; 24 | import com.intellij.openapi.fileEditor.FileEditorManager; 25 | import com.intellij.openapi.project.Project; 26 | import com.intellij.openapi.vfs.VirtualFile; 27 | import com.intellij.openapi.wm.ToolWindow; 28 | import com.leinardi.pycharm.pylint.PylintPlugin; 29 | import com.leinardi.pycharm.pylint.util.FileTypes; 30 | import org.jetbrains.annotations.NotNull; 31 | 32 | import java.util.Collections; 33 | 34 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 35 | 36 | /** 37 | * Action to execute a Pylint scan on the current editor file. 38 | */ 39 | public class ScanCurrentFile extends BaseAction { 40 | 41 | @Override 42 | public @NotNull ActionUpdateThread getActionUpdateThread() { 43 | return ActionUpdateThread.BGT; 44 | } 45 | 46 | @Override 47 | public void actionPerformed(final @NotNull AnActionEvent event) { 48 | project(event).ifPresent(project -> { 49 | try { 50 | final ToolWindow toolWindow = toolWindow(project); 51 | toolWindow.activate(() -> { 52 | try { 53 | setProgressText(toolWindow, "plugin.status.in-progress.current"); 54 | 55 | final VirtualFile selectedFile = getSelectedFile(project); 56 | if (selectedFile != null) { 57 | project.getService(PylintPlugin.class).asyncScanFiles( 58 | Collections.singletonList(selectedFile)); 59 | } 60 | 61 | } catch (Throwable e) { 62 | PylintPlugin.processErrorAndLog("Current File scan", e); 63 | } 64 | }); 65 | 66 | } catch (Throwable e) { 67 | PylintPlugin.processErrorAndLog("Current File scan", e); 68 | } 69 | }); 70 | } 71 | 72 | private VirtualFile getSelectedFile(final @NotNull Project project) { 73 | VirtualFile selectedFile = null; 74 | 75 | final Editor selectedTextEditor = FileEditorManager.getInstance(project).getSelectedTextEditor(); 76 | if (selectedTextEditor != null) { 77 | selectedFile = FileDocumentManager.getInstance().getFile(selectedTextEditor.getDocument()); 78 | } 79 | 80 | if (selectedFile == null) { 81 | // this is the preferred solution, but it doesn't respect the focus of split editors at present 82 | final VirtualFile[] selectedFiles = FileEditorManager.getInstance(project).getSelectedFiles(); 83 | if (selectedFiles.length > 0) { 84 | selectedFile = selectedFiles[0]; 85 | } 86 | } 87 | 88 | // validate selected file against scan scope 89 | if (selectedFile != null) { 90 | if (!FileTypes.isPython(selectedFile.getFileType())) { 91 | selectedFile = null; 92 | } 93 | } 94 | 95 | return selectedFile; 96 | } 97 | 98 | @Override 99 | public void update(final @NotNull AnActionEvent event) { 100 | final Presentation presentation = event.getPresentation(); 101 | 102 | project(event).ifPresentOrElse(project -> { 103 | try { 104 | final PylintPlugin mypyPlugin 105 | = project.getService(PylintPlugin.class); 106 | if (mypyPlugin == null) { 107 | throw new IllegalStateException("Couldn't get mypy plugin"); 108 | } 109 | final VirtualFile selectedFile = getSelectedFile(project); 110 | 111 | // disable if no file is selected or scan in progress 112 | if (selectedFile != null) { 113 | presentation.setEnabled(!mypyPlugin.isScanInProgress()); 114 | } else { 115 | presentation.setEnabled(false); 116 | } 117 | } catch (Throwable e) { 118 | PylintPlugin.processErrorAndLog("Current File button update", e); 119 | } 120 | }, () -> presentation.setEnabled(false)); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ScanEverythingAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.project.Project; 20 | import com.intellij.openapi.project.ProjectUtil; 21 | import com.intellij.openapi.vfs.VirtualFile; 22 | import com.intellij.util.ThrowableRunnable; 23 | import com.leinardi.pycharm.pylint.PylintPlugin; 24 | import com.leinardi.pycharm.pylint.util.VfUtil; 25 | import org.jetbrains.annotations.NotNull; 26 | 27 | import java.util.List; 28 | 29 | class ScanEverythingAction implements ThrowableRunnable { 30 | 31 | private final Project project; 32 | 33 | ScanEverythingAction(@NotNull final Project project) { 34 | this.project = project; 35 | } 36 | 37 | @Override 38 | public void run() { 39 | List filesToScan; 40 | // all non-excluded files of the project 41 | filesToScan = VfUtil.flattenFiles(new VirtualFile[]{ProjectUtil.guessProjectDir(project)}); 42 | filesToScan = VfUtil.filterOnlyPythonProjectFiles(project, filesToScan); 43 | 44 | project.getService(PylintPlugin.class).asyncScanFiles(filesToScan); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ScanModifiedFiles.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.actionSystem.Presentation; 22 | import com.intellij.openapi.diagnostic.Logger; 23 | import com.intellij.openapi.vcs.changes.ChangeListManager; 24 | import com.intellij.openapi.vfs.VirtualFile; 25 | import com.leinardi.pycharm.pylint.PylintPlugin; 26 | import com.leinardi.pycharm.pylint.util.VfUtil; 27 | import org.jetbrains.annotations.NotNull; 28 | 29 | import java.util.List; 30 | 31 | /** 32 | * Scan modified files. 33 | *

34 | * If the project is not setup to use VCS then no files will be scanned. 35 | */ 36 | public class ScanModifiedFiles extends BaseAction { 37 | 38 | private static final Logger LOG = Logger.getInstance(ScanModifiedFiles.class); 39 | 40 | @Override 41 | public @NotNull ActionUpdateThread getActionUpdateThread() { 42 | return ActionUpdateThread.BGT; 43 | } 44 | 45 | @Override 46 | public final void actionPerformed(final @NotNull AnActionEvent event) { 47 | project(event).ifPresent(project -> { 48 | try { 49 | final ChangeListManager changeListManager = ChangeListManager.getInstance(project); 50 | project.getService(PylintPlugin.class).asyncScanFiles( 51 | VfUtil.filterOnlyPythonProjectFiles(project, changeListManager.getAffectedFiles()) 52 | ); 53 | } catch (Throwable e) { 54 | LOG.warn("Modified files scan failed", e); 55 | } 56 | }); 57 | } 58 | 59 | @Override 60 | public void update(final @NotNull AnActionEvent event) { 61 | final Presentation presentation = event.getPresentation(); 62 | 63 | project(event).ifPresentOrElse(project -> { 64 | try { 65 | final PylintPlugin mypyPlugin = project.getService(PylintPlugin.class); 66 | if (mypyPlugin == null) { 67 | throw new IllegalStateException("Couldn't get mypy plugin"); 68 | } 69 | // disable if no files are modified 70 | final List modifiedFiles = ChangeListManager.getInstance(project).getAffectedFiles(); 71 | if (modifiedFiles.isEmpty()) { 72 | presentation.setEnabled(false); 73 | } else { 74 | presentation.setEnabled(!mypyPlugin.isScanInProgress()); 75 | } 76 | } catch (Throwable e) { 77 | LOG.warn("Button update failed.", e); 78 | } 79 | }, () -> presentation.setEnabled(false)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ScanModule.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.actionSystem.Presentation; 22 | import com.intellij.openapi.application.ReadAction; 23 | import com.intellij.openapi.fileEditor.FileEditorManager; 24 | import com.intellij.openapi.module.Module; 25 | import com.intellij.openapi.module.ModuleUtil; 26 | import com.intellij.openapi.roots.ModuleRootManager; 27 | import com.intellij.openapi.vfs.VirtualFile; 28 | import com.intellij.openapi.wm.ToolWindow; 29 | import com.intellij.util.ThrowableRunnable; 30 | import com.leinardi.pycharm.pylint.PylintPlugin; 31 | import com.leinardi.pycharm.pylint.util.VfUtil; 32 | import org.jetbrains.annotations.NotNull; 33 | 34 | import java.util.List; 35 | 36 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 37 | 38 | /** 39 | * Action to execute a Pylint scan on the current module. 40 | */ 41 | public class ScanModule extends BaseAction { 42 | 43 | @Override 44 | public @NotNull ActionUpdateThread getActionUpdateThread() { 45 | return ActionUpdateThread.BGT; 46 | } 47 | 48 | @Override 49 | public final void actionPerformed(final @NotNull AnActionEvent event) { 50 | project(event).ifPresent(project -> { 51 | try { 52 | final ToolWindow toolWindow = toolWindow(project); 53 | 54 | final VirtualFile[] selectedFiles 55 | = FileEditorManager.getInstance(project).getSelectedFiles(); 56 | if (selectedFiles.length == 0) { 57 | setProgressText(toolWindow, "plugin.status.in-progress.no-file"); 58 | return; 59 | } 60 | 61 | toolWindow.activate(() -> { 62 | try { 63 | setProgressText(toolWindow, "plugin.status.in-progress.module"); 64 | 65 | List moduleFiles = VfUtil.filterOnlyPythonProjectFiles( 66 | project, VfUtil.flattenFiles(new VirtualFile[]{selectedFiles[0].getParent()})); 67 | ThrowableRunnable scanAction = new ScanSourceRootsAction(project, 68 | moduleFiles.toArray(new VirtualFile[0])); 69 | ReadAction.run(scanAction); 70 | } catch (Throwable e) { 71 | PylintPlugin.processErrorAndLog("Current Module scan", e); 72 | } 73 | }); 74 | 75 | } catch (Throwable e) { 76 | PylintPlugin.processErrorAndLog("Current Module scan", e); 77 | } 78 | }); 79 | } 80 | 81 | @Override 82 | public final void update(final @NotNull AnActionEvent event) { 83 | final Presentation presentation = event.getPresentation(); 84 | 85 | project(event).ifPresentOrElse(project -> { 86 | try { 87 | final VirtualFile[] selectedFiles 88 | = FileEditorManager.getInstance(project).getSelectedFiles(); 89 | if (selectedFiles.length == 0) { 90 | return; 91 | } 92 | 93 | final Module module = ModuleUtil.findModuleForFile( 94 | selectedFiles[0], project); 95 | if (module == null) { 96 | return; 97 | } 98 | 99 | final PylintPlugin mypyPlugin 100 | = project.getService(PylintPlugin.class); 101 | if (mypyPlugin == null) { 102 | throw new IllegalStateException("Couldn't get mypy plugin"); 103 | } 104 | 105 | VirtualFile[] moduleFiles; 106 | final ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module); 107 | moduleFiles = moduleRootManager.getContentRoots(); 108 | 109 | // disable if no files are selected or scan in progress 110 | if (containsAtLeastOneFile(moduleFiles)) { 111 | presentation.setEnabled(!mypyPlugin.isScanInProgress()); 112 | } else { 113 | presentation.setEnabled(false); 114 | } 115 | } catch (Throwable e) { 116 | PylintPlugin.processErrorAndLog("Current Module button update", e); 117 | } 118 | }, () -> presentation.setEnabled(false)); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ScanProject.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.actionSystem.Presentation; 22 | import com.intellij.openapi.application.ReadAction; 23 | import com.intellij.openapi.project.ProjectUtil; 24 | import com.intellij.openapi.vfs.VirtualFile; 25 | import com.intellij.openapi.wm.ToolWindow; 26 | import com.intellij.openapi.wm.ToolWindowManager; 27 | import com.intellij.util.ThrowableRunnable; 28 | import com.leinardi.pycharm.pylint.PylintPlugin; 29 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 30 | import org.jetbrains.annotations.NotNull; 31 | 32 | /** 33 | * Action to execute a Pylint scan on the current project. 34 | */ 35 | public class ScanProject extends BaseAction { 36 | 37 | @Override 38 | public @NotNull ActionUpdateThread getActionUpdateThread() { 39 | return ActionUpdateThread.BGT; 40 | } 41 | 42 | @Override 43 | public void actionPerformed(final @NotNull AnActionEvent event) { 44 | project(event).ifPresent(project -> { 45 | try { 46 | final PylintPlugin mypyPlugin = project.getService(PylintPlugin.class); 47 | if (mypyPlugin == null) { 48 | throw new IllegalStateException("Couldn't get mypy plugin"); 49 | } 50 | 51 | final ToolWindow toolWindow = ToolWindowManager.getInstance( 52 | project).getToolWindow(PylintToolWindowPanel.ID_TOOLWINDOW); 53 | toolWindow.activate(() -> { 54 | try { 55 | setProgressText(toolWindow, "plugin.status.in-progress.project"); 56 | // if (scope == ScanScope.Everything) { 57 | ThrowableRunnable scanAction = new ScanEverythingAction(project); 58 | // } else { 59 | // final ProjectRootManager projectRootManager = ProjectRootManager 60 | // .getInstance(project); 61 | // final VirtualFile[] sourceRoots = 62 | // projectRootManager.getContentSourceRoots(); 63 | // if (sourceRoots.length > 0) { 64 | // scanAction = new ScanSourceRootsAction(project, sourceRoots/*, 65 | // getSelectedOverride(toolWindow)*/); 66 | // } 67 | // } 68 | // if (scanAction != null) { 69 | ReadAction.run(scanAction); 70 | // } 71 | } catch (Throwable e) { 72 | PylintPlugin.processErrorAndLog("Project scan", e); 73 | } 74 | }); 75 | 76 | } catch (Throwable e) { 77 | PylintPlugin.processErrorAndLog("Project scan", e); 78 | } 79 | }); 80 | } 81 | 82 | @Override 83 | public final void update(final @NotNull AnActionEvent event) { 84 | final Presentation presentation = event.getPresentation(); 85 | 86 | project(event).ifPresentOrElse(project -> { 87 | try { 88 | 89 | final PylintPlugin mypyPlugin = project.getService(PylintPlugin.class); 90 | if (mypyPlugin == null) { 91 | throw new IllegalStateException("Couldn't get mypy plugin"); 92 | } 93 | // final ScanScope scope = mypyPlugin.configurationManager().getCurrent().getScanScope(); 94 | 95 | VirtualFile[] sourceRoots; 96 | // if (scope == ScanScope.Everything) { 97 | sourceRoots = new VirtualFile[]{ProjectUtil.guessProjectDir(project)}; 98 | // } else { 99 | // final ProjectRootManager projectRootManager = ProjectRootManager.getInstance(project); 100 | // sourceRoots = projectRootManager.getContentSourceRoots(); 101 | // } 102 | 103 | // disable if no files are selected or scan in progress 104 | if (containsAtLeastOneFile(sourceRoots)) { 105 | presentation.setEnabled(!mypyPlugin.isScanInProgress()); 106 | } else { 107 | presentation.setEnabled(false); 108 | } 109 | } catch (Throwable e) { 110 | PylintPlugin.processErrorAndLog("Project button update", e); 111 | } 112 | }, () -> presentation.setEnabled(false)); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ScanSourceRootsAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.project.Project; 20 | import com.intellij.openapi.vfs.VirtualFile; 21 | import com.intellij.util.ThrowableRunnable; 22 | import com.leinardi.pycharm.pylint.PylintPlugin; 23 | import com.leinardi.pycharm.pylint.util.VfUtil; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | class ScanSourceRootsAction implements ThrowableRunnable { 27 | private final Project project; 28 | private final VirtualFile[] sourceRoots; 29 | 30 | ScanSourceRootsAction(@NotNull final Project project, 31 | @NotNull final VirtualFile[] sourceRoots) { 32 | this.project = project; 33 | this.sourceRoots = sourceRoots; 34 | } 35 | 36 | @Override 37 | public void run() { 38 | project.getService(PylintPlugin.class) 39 | .asyncScanFiles(VfUtil.filterOnlyPythonProjectFiles(project, 40 | VfUtil.flattenFiles(sourceRoots))); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ScrollToSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 22 | import com.intellij.openapi.project.DumbAwareToggleAction; 23 | import com.intellij.openapi.project.Project; 24 | import com.intellij.openapi.wm.ToolWindow; 25 | import com.intellij.openapi.wm.ToolWindowManager; 26 | import com.intellij.ui.content.Content; 27 | import com.leinardi.pycharm.pylint.PylintPlugin; 28 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 29 | import org.jetbrains.annotations.NotNull; 30 | 31 | /** 32 | * Toggle the scroll to source setting. 33 | */ 34 | public final class ScrollToSource extends DumbAwareToggleAction { 35 | 36 | @Override 37 | public @NotNull ActionUpdateThread getActionUpdateThread() { 38 | return ActionUpdateThread.EDT; 39 | } 40 | 41 | @Override 42 | public boolean isSelected(final AnActionEvent event) { 43 | final Project project = PlatformDataKeys.PROJECT.getData(event.getDataContext()); 44 | if (project == null) { 45 | return false; 46 | } 47 | 48 | final PylintPlugin pylintPlugin 49 | = project.getService(PylintPlugin.class); 50 | if (pylintPlugin == null) { 51 | throw new IllegalStateException("Couldn't get pylint plugin"); 52 | } 53 | 54 | final ToolWindow toolWindow = ToolWindowManager.getInstance( 55 | project).getToolWindow(PylintToolWindowPanel.ID_TOOLWINDOW); 56 | 57 | final Content content = toolWindow.getContentManager().getContent(0); 58 | if (content != null && content.getComponent() instanceof PylintToolWindowPanel) { 59 | return ((PylintToolWindowPanel) content.getComponent()).isScrollToSource(); 60 | } 61 | 62 | return true; 63 | } 64 | 65 | @Override 66 | public void setSelected(final AnActionEvent event, final boolean selected) { 67 | final Project project = PlatformDataKeys.PROJECT.getData(event.getDataContext()); 68 | if (project == null) { 69 | return; 70 | } 71 | 72 | final PylintPlugin pylintPlugin 73 | = project.getService(PylintPlugin.class); 74 | if (pylintPlugin == null) { 75 | throw new IllegalStateException("Couldn't get pylint plugin"); 76 | } 77 | 78 | final ToolWindow toolWindow = ToolWindowManager.getInstance( 79 | project).getToolWindow(PylintToolWindowPanel.ID_TOOLWINDOW); 80 | 81 | final Content content = toolWindow.getContentManager().getContent(0); 82 | if (content != null && content.getComponent() instanceof PylintToolWindowPanel) { 83 | ((PylintToolWindowPanel) content.getComponent()).setScrollToSource(selected); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/Settings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.AnActionEvent; 20 | import com.intellij.openapi.options.ShowSettingsUtil; 21 | import com.leinardi.pycharm.pylint.PylintConfigurable; 22 | import org.jetbrains.annotations.NotNull; 23 | 24 | /** 25 | * Action to close the tool window. 26 | */ 27 | public class Settings extends BaseAction { 28 | 29 | @Override 30 | public void actionPerformed(final @NotNull AnActionEvent event) { 31 | project(event).ifPresent(project -> ShowSettingsUtil.getInstance().showSettingsDialog(project, 32 | PylintConfigurable.class)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/StopCheck.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.actionSystem.ActionUpdateThread; 20 | import com.intellij.openapi.actionSystem.AnActionEvent; 21 | import com.intellij.openapi.actionSystem.Presentation; 22 | import com.intellij.openapi.wm.ToolWindow; 23 | import com.leinardi.pycharm.pylint.PylintPlugin; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | import static com.leinardi.pycharm.pylint.actions.ToolWindowAccess.toolWindow; 27 | 28 | /** 29 | * Action to stop a check in progress. 30 | */ 31 | public class StopCheck extends BaseAction { 32 | 33 | @Override 34 | public @NotNull ActionUpdateThread getActionUpdateThread() { 35 | return ActionUpdateThread.BGT; 36 | } 37 | 38 | @Override 39 | public void actionPerformed(final @NotNull AnActionEvent event) { 40 | project(event).ifPresent(project -> { 41 | try { 42 | final ToolWindow toolWindow = toolWindow(project); 43 | toolWindow.activate(() -> { 44 | setProgressText(toolWindow, "plugin.status.in-progress.current"); 45 | final PylintPlugin mypyPlugin 46 | = project.getService(PylintPlugin.class); 47 | if (mypyPlugin == null) { 48 | throw new IllegalStateException("Couldn't get mypy plugin"); 49 | } 50 | mypyPlugin.stopChecks(); 51 | 52 | setProgressText(toolWindow, "plugin.status.aborted"); 53 | }); 54 | 55 | } catch (Throwable e) { 56 | PylintPlugin.processErrorAndLog("Abort Scan", e); 57 | } 58 | }); 59 | } 60 | 61 | @Override 62 | public void update(final @NotNull AnActionEvent event) { 63 | final Presentation presentation = event.getPresentation(); 64 | project(event).ifPresentOrElse(project -> { 65 | final PylintPlugin mypyPlugin = project.getService(PylintPlugin.class); 66 | if (mypyPlugin == null) { 67 | throw new IllegalStateException("Couldn't get mypy plugin"); 68 | } 69 | try { 70 | presentation.setEnabled(mypyPlugin.isScanInProgress()); 71 | } catch (Throwable e) { 72 | PylintPlugin.processErrorAndLog("Abort button update", e); 73 | } 74 | }, () -> presentation.setEnabled(false)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/actions/ToolWindowAccess.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.actions; 18 | 19 | import com.intellij.openapi.project.Project; 20 | import com.intellij.openapi.wm.ToolWindow; 21 | import com.intellij.openapi.wm.ToolWindowManager; 22 | import com.intellij.ui.content.Content; 23 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 24 | 25 | import java.util.function.Consumer; 26 | import java.util.function.Function; 27 | 28 | public final class ToolWindowAccess { 29 | private ToolWindowAccess() { 30 | } 31 | 32 | static void actOnToolWindowPanel(final ToolWindow toolWindow, final Consumer action) { 33 | final Content content = toolWindow.getContentManager().getContent(0); 34 | // the content instance will be a JLabel while the component initialises 35 | if (content != null && content.getComponent() instanceof PylintToolWindowPanel) { 36 | action.accept((PylintToolWindowPanel) content.getComponent()); 37 | } 38 | } 39 | 40 | static R getFromToolWindowPanel(final ToolWindow toolWindow, final Function action) { 41 | final Content content = toolWindow.getContentManager().getContent(0); 42 | // the content instance will be a JLabel while the component initialises 43 | if (content != null && content.getComponent() instanceof PylintToolWindowPanel) { 44 | return action.apply((PylintToolWindowPanel) content.getComponent()); 45 | } 46 | return null; 47 | } 48 | 49 | static ToolWindow toolWindow(final Project project) { 50 | return ToolWindowManager 51 | .getInstance(project) 52 | .getToolWindow(PylintToolWindowPanel.ID_TOOLWINDOW); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/checker/CreateScannableFileAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.checker; 18 | 19 | import com.intellij.psi.PsiFile; 20 | import com.intellij.util.ThrowableRunnable; 21 | import org.jetbrains.annotations.NotNull; 22 | 23 | import java.io.IOException; 24 | 25 | /** 26 | * Action to read the file to a temporary file. 27 | */ 28 | class CreateScannableFileAction implements ThrowableRunnable { 29 | 30 | /** 31 | * Any failure that occurred on the thread. 32 | */ 33 | private IOException failure; 34 | 35 | private final PsiFile psiFile; 36 | 37 | /** 38 | * The created temporary file. 39 | */ 40 | private ScannableFile file; 41 | 42 | /** 43 | * Create a thread to read the given file to a temporary file. 44 | * 45 | * @param psiFile the file to read. 46 | */ 47 | CreateScannableFileAction(@NotNull final PsiFile psiFile) { 48 | this.psiFile = psiFile; 49 | } 50 | 51 | /** 52 | * Get any failure that occurred in this thread. 53 | * 54 | * @return the failure, if any. 55 | */ 56 | public IOException getFailure() { 57 | return failure; 58 | } 59 | 60 | /** 61 | * Get the scannable file. 62 | * 63 | * @return the scannable file. 64 | */ 65 | public ScannableFile getFile() { 66 | return file; 67 | } 68 | 69 | @Override 70 | public void run() { 71 | try { 72 | file = new ScannableFile(psiFile); 73 | 74 | } catch (IOException e) { 75 | failure = e; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/checker/Problem.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.checker; 18 | 19 | import com.intellij.lang.annotation.AnnotationBuilder; 20 | import com.intellij.lang.annotation.AnnotationHolder; 21 | import com.intellij.lang.annotation.HighlightSeverity; 22 | import com.intellij.psi.PsiElement; 23 | import com.leinardi.pycharm.pylint.PylintBundle; 24 | import com.leinardi.pycharm.pylint.plapi.SeverityLevel; 25 | import org.jetbrains.annotations.NotNull; 26 | 27 | import java.util.Objects; 28 | 29 | public class Problem { 30 | private final PsiElement target; 31 | private final SeverityLevel severityLevel; 32 | private final int line; 33 | private final int column; 34 | private final String symbol; 35 | private final String message; 36 | private final String messageId; 37 | private final boolean afterEndOfLine; 38 | private final boolean suppressErrors; 39 | 40 | public Problem(@NotNull final PsiElement target, 41 | @NotNull final String message, 42 | @NotNull final String messageId, 43 | final SeverityLevel severityLevel, 44 | final int line, 45 | final int column, 46 | final String symbol, 47 | final boolean afterEndOfLine, 48 | final boolean suppressErrors) { 49 | this.target = target; 50 | this.message = message; 51 | this.messageId = messageId; 52 | this.severityLevel = severityLevel; 53 | this.line = line; 54 | this.column = column; 55 | this.symbol = symbol; 56 | this.afterEndOfLine = afterEndOfLine; 57 | this.suppressErrors = suppressErrors; 58 | } 59 | 60 | public void createAnnotation(@NotNull AnnotationHolder holder, @NotNull HighlightSeverity severity) { 61 | String message = PylintBundle.message("inspection.message", getMessage()); 62 | AnnotationBuilder annotation = holder 63 | .newAnnotation(severity, message) 64 | .range(target.getTextRange()); 65 | if (isAfterEndOfLine()) { 66 | annotation = annotation.afterEndOfLine(); 67 | } 68 | annotation.create(); 69 | } 70 | 71 | public SeverityLevel severityLevel() { 72 | return severityLevel; 73 | } 74 | 75 | public int line() { 76 | return line; 77 | } 78 | 79 | public int column() { 80 | return column; 81 | } 82 | 83 | public String getSymbol() { 84 | return symbol; 85 | } 86 | 87 | public String getMessage() { 88 | return message; 89 | } 90 | 91 | public String getMessageId() { 92 | return messageId; 93 | } 94 | 95 | public boolean isAfterEndOfLine() { 96 | return afterEndOfLine; 97 | } 98 | 99 | public boolean isSuppressErrors() { 100 | return suppressErrors; 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | return "Problem{" + 106 | "target=" + target + 107 | ", severityLevel=" + severityLevel + 108 | ", line=" + line + 109 | ", column=" + column + 110 | ", symbol='" + symbol + '\'' + 111 | ", message='" + message + '\'' + 112 | ", messageId='" + messageId + '\'' + 113 | ", afterEndOfLine=" + afterEndOfLine + 114 | ", suppressErrors=" + suppressErrors + 115 | '}'; 116 | } 117 | 118 | @Override 119 | public boolean equals(Object o) { 120 | if (this == o) { 121 | return true; 122 | } 123 | if (!(o instanceof Problem)) { 124 | return false; 125 | } 126 | Problem problem = (Problem) o; 127 | return line == problem.line && 128 | column == problem.column && 129 | afterEndOfLine == problem.afterEndOfLine && 130 | suppressErrors == problem.suppressErrors && 131 | Objects.equals(target, problem.target) && 132 | severityLevel == problem.severityLevel && 133 | Objects.equals(symbol, problem.symbol) && 134 | Objects.equals(message, problem.message) && 135 | Objects.equals(messageId, problem.messageId); 136 | } 137 | 138 | @Override 139 | public int hashCode() { 140 | return Objects.hash( 141 | target, 142 | severityLevel, 143 | line, 144 | column, 145 | symbol, 146 | message, 147 | messageId, 148 | afterEndOfLine, 149 | suppressErrors 150 | ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/checker/PsiFileValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.checker; 18 | 19 | import com.intellij.openapi.project.Project; 20 | import com.intellij.openapi.roots.ProjectFileIndex; 21 | import com.intellij.psi.PsiDocumentManager; 22 | import com.intellij.psi.PsiFile; 23 | import com.leinardi.pycharm.pylint.util.FileTypes; 24 | import org.jetbrains.annotations.Nullable; 25 | 26 | final class PsiFileValidator { 27 | 28 | private PsiFileValidator() { 29 | } 30 | 31 | public static boolean isScannable(@Nullable final PsiFile psiFile, final Project project) { 32 | ProjectFileIndex projectFileIndex = ProjectFileIndex.SERVICE.getInstance(project); 33 | return psiFile != null 34 | && psiFile.isValid() 35 | && psiFile.isPhysical() 36 | && hasDocument(psiFile) 37 | && isInSource(psiFile, projectFileIndex) 38 | && isValidFileType(psiFile); 39 | } 40 | 41 | private static boolean hasDocument(final PsiFile psiFile) { 42 | return PsiDocumentManager.getInstance(psiFile.getProject()).getDocument(psiFile) != null; 43 | } 44 | 45 | private static boolean isValidFileType(final PsiFile psiFile) { 46 | return FileTypes.isPython(psiFile.getFileType()); 47 | } 48 | 49 | private static boolean isInSource(final PsiFile psiFile, final ProjectFileIndex projectFileIndex) { 50 | return !projectFileIndex.isExcluded(psiFile.getVirtualFile()); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/checker/ScanFiles.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.checker; 18 | 19 | import com.intellij.openapi.application.ApplicationManager; 20 | import com.intellij.openapi.application.ReadAction; 21 | import com.intellij.openapi.diagnostic.Logger; 22 | import com.intellij.openapi.vfs.VfsUtilCore; 23 | import com.intellij.openapi.vfs.VirtualFile; 24 | import com.intellij.openapi.vfs.VirtualFileVisitor; 25 | import com.intellij.psi.PsiFile; 26 | import com.intellij.psi.PsiManager; 27 | import com.leinardi.pycharm.pylint.PylintPlugin; 28 | import com.leinardi.pycharm.pylint.exception.PylintPluginException; 29 | import com.leinardi.pycharm.pylint.plapi.Issue; 30 | import com.leinardi.pycharm.pylint.plapi.ProcessResultsThread; 31 | import com.leinardi.pycharm.pylint.plapi.PylintRunner; 32 | import com.leinardi.pycharm.pylint.util.Notifications; 33 | import org.jetbrains.annotations.NotNull; 34 | 35 | import java.io.InterruptedIOException; 36 | import java.util.ArrayList; 37 | import java.util.HashMap; 38 | import java.util.HashSet; 39 | import java.util.List; 40 | import java.util.Map; 41 | import java.util.Set; 42 | import java.util.concurrent.Callable; 43 | 44 | import static java.util.Collections.emptyMap; 45 | 46 | public class ScanFiles implements Callable>> { 47 | 48 | private static final Logger LOG = Logger.getInstance(ScanFiles.class); 49 | 50 | private final List files; 51 | private final Set listeners = new HashSet<>(); 52 | private final PylintPlugin plugin; 53 | 54 | public ScanFiles(@NotNull final PylintPlugin pylintPlugin, 55 | @NotNull final List virtualFiles) { 56 | this.plugin = pylintPlugin; 57 | 58 | files = findAllFilesFor(virtualFiles); 59 | } 60 | 61 | private List findAllFilesFor(@NotNull final List virtualFiles) { 62 | final List childFiles = new ArrayList<>(); 63 | final PsiManager psiManager = PsiManager.getInstance(this.plugin.getProject()); 64 | for (final VirtualFile virtualFile : virtualFiles) { 65 | childFiles.addAll(buildFilesList(psiManager, virtualFile)); 66 | } 67 | return childFiles; 68 | } 69 | 70 | @Override 71 | public final Map> call() { 72 | try { 73 | fireCheckStarting(files); 74 | return scanCompletedSuccessfully(checkFiles(new HashSet<>(files))); 75 | } catch (final InterruptedIOException | InterruptedException e) { 76 | LOG.debug("Scan cancelled by PyCharm", e); 77 | return scanCompletedSuccessfully(emptyMap()); 78 | } catch (final PylintPluginException e) { 79 | LOG.warn("An error occurred while scanning a file.", e); 80 | return scanFailedWithError(e); 81 | } catch (final Throwable e) { 82 | LOG.warn("An error occurred while scanning a file.", e); 83 | return scanFailedWithError(new PylintPluginException("An error occurred while scanning a file.", e)); 84 | } 85 | } 86 | 87 | private Map mapFilesToElements(final List filesToScan) { 88 | final Map filePathsToElements = new HashMap<>(); 89 | for (ScannableFile scannableFile : filesToScan) { 90 | filePathsToElements.put(scannableFile.getAbsolutePath(), scannableFile.getPsiFile()); 91 | } 92 | return filePathsToElements; 93 | } 94 | 95 | private Map> checkFiles(final Set filesToScan) 96 | throws InterruptedIOException, InterruptedException { 97 | final List scannableFiles = new ArrayList<>(); 98 | try { 99 | scannableFiles.addAll(ScannableFile.createAndValidate(filesToScan, plugin)); 100 | return scan(scannableFiles); 101 | } finally { 102 | scannableFiles.forEach(ScannableFile::deleteIfRequired); 103 | } 104 | } 105 | 106 | private Map> scan(final List filesToScan) 107 | throws InterruptedIOException, InterruptedException { 108 | Map fileNamesToPsiFiles = mapFilesToElements(filesToScan); 109 | List errors = PylintRunner.scan(plugin.getProject(), fileNamesToPsiFiles.keySet()); 110 | String baseDir = plugin.getProject().getBasePath(); 111 | int tabWidth = 4; 112 | final ProcessResultsThread findThread = new ProcessResultsThread(false, tabWidth, baseDir, 113 | errors, fileNamesToPsiFiles); 114 | 115 | ReadAction.run(findThread); 116 | return findThread.getProblems(); 117 | } 118 | 119 | private Map> scanFailedWithError(final PylintPluginException e) { 120 | Notifications.showException(plugin.getProject(), e); 121 | fireScanFailedWithError(e); 122 | 123 | return emptyMap(); 124 | } 125 | 126 | private Map> scanCompletedSuccessfully(final Map> filesToProblems) { 127 | fireScanCompletedSuccessfully(filesToProblems); 128 | return filesToProblems; 129 | } 130 | 131 | public void addListener(final ScannerListener listener) { 132 | listeners.add(listener); 133 | } 134 | 135 | private void fireCheckStarting(final List filesToScan) { 136 | listeners.forEach(listener -> listener.scanStarting(filesToScan)); 137 | } 138 | 139 | private void fireScanCompletedSuccessfully( 140 | final Map> fileResults) { 141 | listeners.forEach(listener -> listener.scanCompletedSuccessfully(fileResults)); 142 | } 143 | 144 | private void fireScanFailedWithError(final PylintPluginException error) { 145 | listeners.forEach(listener -> listener.scanFailedWithError(error)); 146 | } 147 | 148 | private List buildFilesList(final PsiManager psiManager, final VirtualFile virtualFile) { 149 | final List allChildFiles = new ArrayList<>(); 150 | ApplicationManager.getApplication().runReadAction(() -> { 151 | final FindChildFiles visitor = new FindChildFiles(virtualFile, psiManager); 152 | VfsUtilCore.visitChildrenRecursively(virtualFile, visitor); 153 | allChildFiles.addAll(visitor.locatedFiles); 154 | }); 155 | return allChildFiles; 156 | } 157 | 158 | private static class FindChildFiles extends VirtualFileVisitor { 159 | 160 | private final VirtualFile virtualFile; 161 | private final PsiManager psiManager; 162 | 163 | final List locatedFiles = new ArrayList<>(); 164 | 165 | FindChildFiles(final VirtualFile virtualFile, final PsiManager psiManager) { 166 | this.virtualFile = virtualFile; 167 | this.psiManager = psiManager; 168 | } 169 | 170 | @Override 171 | public boolean visitFile(@NotNull final VirtualFile file) { 172 | if (!file.isDirectory()) { 173 | final PsiFile psiFile = psiManager.findFile(virtualFile); 174 | if (psiFile != null) { 175 | locatedFiles.add(psiFile); 176 | } 177 | } 178 | return true; 179 | } 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/checker/ScannerListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.checker; 18 | 19 | import com.intellij.psi.PsiFile; 20 | import com.leinardi.pycharm.pylint.exception.PylintPluginException; 21 | 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | public interface ScannerListener { 26 | 27 | void scanStarting(List filesToScan); 28 | 29 | void filesScanned(int count); 30 | 31 | void scanCompletedSuccessfully( 32 | Map> scanResults); 33 | 34 | void scanFailedWithError(PylintPluginException error); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/checker/UiFeedbackScannerListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.checker; 18 | 19 | import com.intellij.psi.PsiFile; 20 | import com.leinardi.pycharm.pylint.PylintPlugin; 21 | import com.leinardi.pycharm.pylint.exception.PylintPluginException; 22 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 23 | import org.jetbrains.annotations.Nullable; 24 | 25 | import javax.swing.SwingUtilities; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | public class UiFeedbackScannerListener implements ScannerListener { 30 | private final PylintPlugin plugin; 31 | 32 | public UiFeedbackScannerListener(final PylintPlugin plugin) { 33 | this.plugin = plugin; 34 | } 35 | 36 | @Override 37 | public void scanStarting(final List filesToScan) { 38 | SwingUtilities.invokeLater(() -> { 39 | final PylintToolWindowPanel toolWindowPanel = toolWindowPanel(); 40 | if (toolWindowPanel != null) { 41 | toolWindowPanel.displayInProgress(filesToScan.size()); 42 | } 43 | }); 44 | } 45 | 46 | @Override 47 | public void filesScanned(final int count) { 48 | SwingUtilities.invokeLater(() -> { 49 | final PylintToolWindowPanel toolWindowPanel = PylintToolWindowPanel.panelFor(plugin.getProject()); 50 | if (toolWindowPanel != null) { 51 | toolWindowPanel.incrementProgressBarBy(count); 52 | } 53 | }); 54 | } 55 | 56 | @Override 57 | public void scanCompletedSuccessfully( 58 | final Map> scanResults) { 59 | SwingUtilities.invokeLater(() -> { 60 | final PylintToolWindowPanel toolWindowPanel = toolWindowPanel(); 61 | if (toolWindowPanel != null) { 62 | toolWindowPanel.displayResults(scanResults); 63 | } 64 | }); 65 | } 66 | 67 | @Override 68 | public void scanFailedWithError(final PylintPluginException error) { 69 | SwingUtilities.invokeLater(() -> { 70 | final PylintToolWindowPanel toolWindowPanel = toolWindowPanel(); 71 | if (toolWindowPanel != null) { 72 | toolWindowPanel.displayErrorResult(error); 73 | } 74 | }); 75 | } 76 | 77 | @Nullable 78 | private PylintToolWindowPanel toolWindowPanel() { 79 | return PylintToolWindowPanel.panelFor(plugin.getProject()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/exception/PylintPluginException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.exception; 18 | 19 | /** 20 | * Common exception thrown anywhere in this plugin. 21 | */ 22 | public class PylintPluginException extends RuntimeException { 23 | 24 | private static final long serialVersionUID = 2L; 25 | 26 | public PylintPluginException(final String message) { 27 | super(message); 28 | } 29 | 30 | public PylintPluginException(final String message, final Throwable cause) { 31 | super(message, cause); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/exception/PylintPluginParseException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.exception; 18 | 19 | public class PylintPluginParseException extends PylintPluginException { 20 | private static final long serialVersionUID = -2138216104879079892L; 21 | 22 | public PylintPluginParseException(final String message, final Throwable cause) { 23 | super(message, cause); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/exception/PylintServiceException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.exception; 18 | 19 | /** 20 | * An exception that originates with the Pylint access layer (aka Pylint plugin service), but is not 21 | * a native PylintException. 22 | *

Important: Be sure to throw it only from the 'csaccess' sourceset!

23 | */ 24 | public class PylintServiceException extends PylintPluginException { 25 | 26 | public PylintServiceException(final String message) { 27 | super(message); 28 | } 29 | 30 | public PylintServiceException(final String message, final Throwable cause) { 31 | super(message, cause); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/exception/PylintToolException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.exception; 18 | 19 | /** 20 | * Wrapper for an exception that occurred in the Pylint tool itself. 21 | *

Important: Be sure to throw it only from the 'csaccess' sourceset!

22 | */ 23 | public class PylintToolException extends PylintServiceException { 24 | 25 | public PylintToolException(String message) { 26 | super(message); 27 | } 28 | 29 | public PylintToolException(String message, Throwable cause) { 30 | super(message, cause); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/handlers/ScanFilesBeforeCheckinHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.handlers; 18 | 19 | import com.intellij.CommonBundle; 20 | import com.intellij.openapi.diagnostic.Logger; 21 | import com.intellij.openapi.progress.ProcessCanceledException; 22 | import com.intellij.openapi.progress.ProgressIndicator; 23 | import com.intellij.openapi.progress.Task; 24 | import com.intellij.openapi.project.Project; 25 | import com.intellij.openapi.ui.Messages; 26 | import com.intellij.openapi.vcs.CheckinProjectPanel; 27 | import com.intellij.openapi.vcs.changes.CommitExecutor; 28 | import com.intellij.openapi.vcs.checkin.CheckinHandler; 29 | import com.intellij.openapi.vcs.ui.RefreshableOnComponent; 30 | import com.intellij.psi.PsiFile; 31 | import com.intellij.util.PairConsumer; 32 | import com.intellij.util.ui.UIUtil; 33 | import com.leinardi.pycharm.pylint.PylintConfigService; 34 | import com.leinardi.pycharm.pylint.PylintPlugin; 35 | import com.leinardi.pycharm.pylint.checker.Problem; 36 | import com.leinardi.pycharm.pylint.toolwindow.PylintToolWindowPanel; 37 | import org.jetbrains.annotations.NotNull; 38 | import org.jetbrains.annotations.Nullable; 39 | 40 | import javax.swing.JCheckBox; 41 | import javax.swing.JComponent; 42 | import javax.swing.JPanel; 43 | import java.awt.BorderLayout; 44 | import java.util.ArrayList; 45 | import java.util.HashMap; 46 | import java.util.List; 47 | import java.util.Map; 48 | 49 | import static com.intellij.openapi.vcs.checkin.CheckinHandler.ReturnResult.CANCEL; 50 | import static com.intellij.openapi.vcs.checkin.CheckinHandler.ReturnResult.CLOSE_WINDOW; 51 | import static com.intellij.openapi.vcs.checkin.CheckinHandler.ReturnResult.COMMIT; 52 | import static com.leinardi.pycharm.pylint.PylintBundle.message; 53 | import static java.util.stream.Collectors.toList; 54 | 55 | public class ScanFilesBeforeCheckinHandler extends CheckinHandler { 56 | private static final Logger LOG = Logger.getInstance(ScanFilesBeforeCheckinHandler.class); 57 | 58 | private final CheckinProjectPanel checkinPanel; 59 | private final PylintConfigService pylintConfigService; 60 | 61 | public ScanFilesBeforeCheckinHandler(@NotNull final CheckinProjectPanel myCheckinPanel) { 62 | this.checkinPanel = myCheckinPanel; 63 | pylintConfigService = PylintConfigService.getInstance(checkinPanel.getProject()); 64 | } 65 | 66 | @Nullable 67 | @Override 68 | public RefreshableOnComponent getBeforeCheckinConfigurationPanel() { 69 | final JCheckBox checkBox = new JCheckBox(message("handler.before.checkin.checkbox")); 70 | 71 | return new RefreshableOnComponent() { 72 | @Override 73 | public JComponent getComponent() { 74 | final JPanel panel = new JPanel(new BorderLayout()); 75 | panel.add(checkBox); 76 | return panel; 77 | } 78 | 79 | @Override 80 | public void saveState() { 81 | pylintConfigService.setScanBeforeCheckin(checkBox.isSelected()); 82 | } 83 | 84 | @Override 85 | public void restoreState() { 86 | checkBox.setSelected(pylintConfigService.isScanBeforeCheckin()); 87 | } 88 | }; 89 | } 90 | 91 | @Override 92 | public ReturnResult beforeCheckin(@Nullable final CommitExecutor executor, 93 | final PairConsumer additionalDataConsumer) { 94 | final Project project = checkinPanel.getProject(); 95 | if (project == null) { 96 | LOG.warn("Could not get project for check-in panel, skipping"); 97 | return COMMIT; 98 | } 99 | 100 | final PylintPlugin plugin = project.getService(PylintPlugin.class); 101 | if (plugin == null) { 102 | LOG.warn("Could not get Pylint Plug-in, skipping"); 103 | return COMMIT; 104 | } 105 | 106 | if (pylintConfigService.isScanBeforeCheckin()) { 107 | try { 108 | final Map> scanResults = new HashMap<>(); 109 | new Task.Modal(project, message("handler.before.checkin.scan.text"), false) { 110 | @Override 111 | public void run(@NotNull final ProgressIndicator progressIndicator) { 112 | progressIndicator.setText(message("handler.before.checkin.scan.in-progress")); 113 | progressIndicator.setIndeterminate(true); 114 | scanResults.putAll(plugin.scanFiles(new ArrayList<>(checkinPanel.getVirtualFiles()))); 115 | } 116 | }.queue(); 117 | 118 | return processScanResults(scanResults, executor, plugin); 119 | 120 | } catch (ProcessCanceledException e) { 121 | return CANCEL; 122 | } 123 | 124 | } else { 125 | return COMMIT; 126 | } 127 | } 128 | 129 | private ReturnResult processScanResults(final Map> results, 130 | final CommitExecutor executor, 131 | final PylintPlugin plugin) { 132 | final int errorCount = errorCountOf(results); 133 | if (errorCount == 0) { 134 | return COMMIT; 135 | } 136 | 137 | final int answer = promptUser(plugin, errorCount, executor); 138 | if (answer == Messages.OK) { 139 | showResultsInToolWindow(results, plugin); 140 | return CLOSE_WINDOW; 141 | 142 | } else if (answer == Messages.CANCEL || answer < 0) { 143 | return CANCEL; 144 | } 145 | 146 | return COMMIT; 147 | } 148 | 149 | private int errorCountOf(final Map> results) { 150 | return results.entrySet().stream() 151 | .filter(this::hasProblemsThatAreNotIgnored) 152 | .collect(toList()) 153 | .size(); 154 | } 155 | 156 | private boolean hasProblemsThatAreNotIgnored(final Map.Entry> entry) { 157 | return entry.getValue().size() > 0; 158 | } 159 | 160 | private int promptUser(final PylintPlugin plugin, 161 | final int errorCount, 162 | final CommitExecutor executor) { 163 | String commitButtonText; 164 | if (executor != null) { 165 | commitButtonText = executor.getActionText(); 166 | } else { 167 | commitButtonText = checkinPanel.getCommitActionName(); 168 | } 169 | 170 | if (commitButtonText.endsWith("...")) { 171 | commitButtonText = commitButtonText.substring(0, commitButtonText.length() - 3); 172 | } 173 | 174 | final String[] buttons = new String[]{ 175 | message("handler.before.checkin.error.review"), 176 | commitButtonText, 177 | CommonBundle.getCancelButtonText()}; 178 | 179 | return Messages.showDialog(plugin.getProject(), message("handler.before.checkin.error.text", errorCount), 180 | message("handler.before.checkin.error.title"), 181 | buttons, 0, UIUtil.getWarningIcon()); 182 | } 183 | 184 | private void showResultsInToolWindow(final Map> results, 185 | final PylintPlugin plugin) { 186 | final PylintToolWindowPanel toolWindowPanel = PylintToolWindowPanel.panelFor(plugin.getProject()); 187 | if (toolWindowPanel != null) { 188 | toolWindowPanel.displayResults(results); 189 | toolWindowPanel.showToolWindow(); 190 | } 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/handlers/ScanFilesBeforeCheckinHandlerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.handlers; 18 | 19 | import com.intellij.openapi.vcs.CheckinProjectPanel; 20 | import com.intellij.openapi.vcs.changes.CommitContext; 21 | import com.intellij.openapi.vcs.checkin.CheckinHandler; 22 | import com.intellij.openapi.vcs.checkin.CheckinHandlerFactory; 23 | import org.jetbrains.annotations.NotNull; 24 | 25 | public class ScanFilesBeforeCheckinHandlerFactory extends CheckinHandlerFactory { 26 | 27 | @NotNull 28 | @Override 29 | public CheckinHandler createHandler(@NotNull final CheckinProjectPanel checkinProjectPanel, 30 | @NotNull final CommitContext commitContext) { 31 | return new ScanFilesBeforeCheckinHandler(checkinProjectPanel); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/plapi/Issue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.plapi; 18 | 19 | import com.squareup.moshi.Json; 20 | 21 | import java.util.Objects; 22 | 23 | /** 24 | * An issue as reported by the Pylint tool. 25 | */ 26 | public class Issue { 27 | 28 | @Json(name = "type") 29 | private SeverityLevel severityLevel; 30 | @Json(name = "module") 31 | private String module; 32 | @Json(name = "obj") 33 | private String obj; 34 | @Json(name = "line") 35 | private int line; 36 | @Json(name = "column") 37 | private int column; 38 | @Json(name = "path") 39 | private String path; 40 | @Json(name = "symbol") 41 | private String symbol; 42 | @Json(name = "message") 43 | private String message; 44 | @Json(name = "message-id") 45 | private String messageId; 46 | 47 | public SeverityLevel getSeverityLevel() { 48 | return severityLevel; 49 | } 50 | 51 | public void setSeverityLevel(SeverityLevel severityLevel) { 52 | this.severityLevel = severityLevel; 53 | } 54 | 55 | public String getModule() { 56 | return module; 57 | } 58 | 59 | public void setModule(String module) { 60 | this.module = module; 61 | } 62 | 63 | public String getObj() { 64 | return obj; 65 | } 66 | 67 | public void setObj(String obj) { 68 | this.obj = obj; 69 | } 70 | 71 | public int getLine() { 72 | return line; 73 | } 74 | 75 | public void setLine(int line) { 76 | this.line = line; 77 | } 78 | 79 | public int getColumn() { 80 | return column; 81 | } 82 | 83 | public void setColumn(int column) { 84 | this.column = column; 85 | } 86 | 87 | public String getPath() { 88 | return path; 89 | } 90 | 91 | public void setPath(String path) { 92 | this.path = path; 93 | } 94 | 95 | public String getSymbol() { 96 | return symbol; 97 | } 98 | 99 | public void setSymbol(String symbol) { 100 | this.symbol = symbol; 101 | } 102 | 103 | public String getMessage() { 104 | return message; 105 | } 106 | 107 | public void setMessage(String message) { 108 | this.message = message; 109 | } 110 | 111 | public String getMessageId() { 112 | return messageId; 113 | } 114 | 115 | public void setMessageId(String messageId) { 116 | this.messageId = messageId; 117 | } 118 | 119 | @Override 120 | public String toString() { 121 | return "Issue{" + 122 | "severityLevel=" + severityLevel + 123 | ", module='" + module + '\'' + 124 | ", obj='" + obj + '\'' + 125 | ", line=" + line + 126 | ", column=" + column + 127 | ", path='" + path + '\'' + 128 | ", symbol='" + symbol + '\'' + 129 | ", message='" + message + '\'' + 130 | ", messageId='" + messageId + '\'' + 131 | '}'; 132 | } 133 | 134 | @Override 135 | public int hashCode() { 136 | return Objects.hash(severityLevel, module, obj, line, column, path, symbol, message, messageId); 137 | } 138 | 139 | @Override 140 | public boolean equals(Object o) { 141 | if (this == o) { 142 | return true; 143 | } 144 | if (!(o instanceof Issue)) { 145 | return false; 146 | } 147 | Issue issue = (Issue) o; 148 | return line == issue.line && 149 | column == issue.column && 150 | severityLevel == issue.severityLevel && 151 | Objects.equals(module, issue.module) && 152 | Objects.equals(obj, issue.obj) && 153 | Objects.equals(path, issue.path) && 154 | Objects.equals(symbol, issue.symbol) && 155 | Objects.equals(message, issue.message) && 156 | Objects.equals(messageId, issue.messageId); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/plapi/SeverityLevel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.plapi; 18 | 19 | import com.squareup.moshi.Json; 20 | 21 | /** 22 | * Pylint violation severity levels supported by this plugin. 23 | */ 24 | public enum SeverityLevel { 25 | @Json(name = "fatal") 26 | FATAL, 27 | @Json(name = "error") 28 | ERROR, 29 | @Json(name = "warning") 30 | WARNING, 31 | @Json(name = "convention") 32 | CONVENTION, 33 | @Json(name = "refactor") 34 | REFACTOR, 35 | @Json(name = "info") 36 | INFO 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/toolwindow/PylintToolWindowFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.toolwindow; 18 | 19 | import com.intellij.openapi.project.DumbAware; 20 | import com.intellij.openapi.project.Project; 21 | import com.intellij.openapi.wm.ToolWindow; 22 | import com.intellij.openapi.wm.ToolWindowFactory; 23 | import com.intellij.openapi.wm.ToolWindowType; 24 | import com.intellij.ui.content.Content; 25 | import com.leinardi.pycharm.pylint.PylintBundle; 26 | import org.jetbrains.annotations.NotNull; 27 | 28 | public class PylintToolWindowFactory implements ToolWindowFactory, DumbAware { 29 | 30 | @Override 31 | public void createToolWindowContent(@NotNull final Project project, @NotNull final ToolWindow toolWindow) { 32 | final Content toolContent = toolWindow.getContentManager().getFactory().createContent( 33 | new PylintToolWindowPanel(toolWindow, project), 34 | PylintBundle.message("plugin.toolwindow.action"), 35 | false); 36 | toolWindow.getContentManager().addContent(toolContent); 37 | 38 | toolWindow.setTitle(PylintBundle.message("plugin.toolwindow.name")); 39 | toolWindow.setType(ToolWindowType.DOCKED, null); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/toolwindow/ResultTreeNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.toolwindow; 18 | 19 | import com.intellij.psi.PsiFile; 20 | import com.leinardi.pycharm.pylint.PylintBundle; 21 | import com.leinardi.pycharm.pylint.checker.Problem; 22 | import com.leinardi.pycharm.pylint.plapi.SeverityLevel; 23 | import com.leinardi.pycharm.pylint.util.Icons; 24 | import com.leinardi.pycharm.pylint.util.Strings; 25 | import org.jetbrains.annotations.NotNull; 26 | 27 | import javax.swing.Icon; 28 | 29 | /** 30 | * The user object for meta-data on tree nodes in the tool window. 31 | */ 32 | public class ResultTreeNode { 33 | 34 | private PsiFile file; 35 | private Problem problem; 36 | private Icon icon; 37 | private String text; 38 | private String tooltip; 39 | private String description; 40 | private SeverityLevel severity; 41 | 42 | /** 43 | * Construct a informational node. 44 | * 45 | * @param text the information text. 46 | */ 47 | public ResultTreeNode(final String text) { 48 | if (text == null) { 49 | throw new IllegalArgumentException("Text may not be null"); 50 | } 51 | 52 | this.text = text; 53 | icon = Icons.icon("/general/information.png"); 54 | } 55 | 56 | /** 57 | * Construct a file node. 58 | * 59 | * @param fileName the name of the file. 60 | * @param problemCounts the number of problems in the file. 61 | */ 62 | public ResultTreeNode(final String fileName, final int[] problemCounts) { 63 | if (fileName == null) { 64 | throw new IllegalArgumentException("Filename may not be null"); 65 | } 66 | 67 | this.text = PylintBundle.message("plugin.results.scan-file-result", 68 | fileName, 69 | ResultTreeModel.concatProblems(problemCounts)); 70 | icon = Icons.icon("/com/leinardi/pycharm/pylint/images/pythonFile.png"); 71 | } 72 | 73 | /** 74 | * Construct a node for a given problem. 75 | * 76 | * @param file the file the problem exists in. 77 | * @param problem the problem. 78 | */ 79 | public ResultTreeNode(@NotNull final PsiFile file, 80 | @NotNull final Problem problem) { 81 | this.file = file; 82 | this.problem = problem; 83 | 84 | severity = problem.severityLevel(); 85 | 86 | updateIconsForProblem(); 87 | } 88 | 89 | private void updateIconsForProblem() { 90 | if (SeverityLevel.FATAL.equals(severity)) { 91 | icon = Icons.icon("/general/exclMark.png"); 92 | } else if (SeverityLevel.ERROR.equals(severity)) { 93 | icon = Icons.icon("/general/error.png"); 94 | } else if (SeverityLevel.WARNING.equals(severity)) { 95 | icon = Icons.icon("/general/warning.png"); 96 | } else if (SeverityLevel.CONVENTION.equals(severity)) { 97 | icon = Icons.icon("/nodes/class.png"); 98 | } else if (SeverityLevel.REFACTOR.equals(severity)) { 99 | icon = Icons.icon("/actions/forceRefresh.png"); 100 | } else if (SeverityLevel.INFO.equals(severity)) { 101 | icon = Icons.icon("/general/information.png"); 102 | } else { 103 | throw new IllegalStateException("Unknown severity level " + severity.name()); 104 | } 105 | } 106 | 107 | /** 108 | * Get the severity of the problem. 109 | * 110 | * @return the severity, or null if not applicable. 111 | */ 112 | public SeverityLevel getSeverity() { 113 | return severity; 114 | } 115 | 116 | /** 117 | * Get the problem associated with this node. 118 | * 119 | * @return the problem associated with this node. 120 | */ 121 | public Problem getProblem() { 122 | return problem; 123 | } 124 | 125 | /** 126 | * Get the node's icon when in an expanded state. 127 | * 128 | * @return the node's icon when in an expanded state. 129 | */ 130 | public Icon getExpandedIcon() { 131 | return icon; 132 | } 133 | 134 | /** 135 | * Get the node's icon when in an collapsed state. 136 | * 137 | * @return the node's icon when in an collapsed state. 138 | */ 139 | public Icon getCollapsedIcon() { 140 | return icon; 141 | } 142 | 143 | /** 144 | * Get the file the node represents. 145 | * 146 | * @return the file the node represents. 147 | */ 148 | public String getText() { 149 | return text; 150 | } 151 | 152 | public Icon getIcon() { 153 | return icon; 154 | } 155 | 156 | public void setIcon(final Icon icon) { 157 | this.icon = icon; 158 | } 159 | 160 | /** 161 | * Set the file the node represents. 162 | * 163 | * @param text the file the node represents. 164 | */ 165 | public void setText(final String text) { 166 | if (Strings.isBlank(text)) { 167 | throw new IllegalArgumentException("Text may not be null/empty"); 168 | } 169 | this.text = text; 170 | } 171 | 172 | /** 173 | * Get the file associated with this node. 174 | * 175 | * @return the file associated with this node. 176 | */ 177 | public PsiFile getFile() { 178 | return file; 179 | } 180 | 181 | /** 182 | * Get the tooltip for this node, if any. 183 | * 184 | * @return the tooltip for this node, or null if none. 185 | */ 186 | public String getTooltip() { 187 | return tooltip; 188 | } 189 | 190 | /** 191 | * Set the tooltip for this node, if any. 192 | * 193 | * @param tooltip the tooltip for this node, or null if none. 194 | */ 195 | public void setTooltip(final String tooltip) { 196 | this.tooltip = tooltip; 197 | } 198 | 199 | /** 200 | * Get the description of this node, if any. 201 | * 202 | * @return the description of this node, or null if none. 203 | */ 204 | public String getDescription() { 205 | return description; 206 | } 207 | 208 | /** 209 | * Get the description of this node, if any. 210 | * 211 | * @param description the description of this node, or null if none. 212 | */ 213 | public void setDescription(final String description) { 214 | this.description = description; 215 | } 216 | 217 | @Override 218 | public String toString() { 219 | if (text != null) { 220 | return text; 221 | } 222 | 223 | return PylintBundle.message( 224 | "plugin.results.file-result", 225 | problem.getMessage(), 226 | problem.line(), 227 | Integer.toString(problem.column()), 228 | problem.getSymbol() 229 | ); 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/toolwindow/ResultTreeRenderer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.toolwindow; 18 | 19 | import com.intellij.ui.JBColor; 20 | 21 | import javax.swing.JLabel; 22 | import javax.swing.JTree; 23 | import javax.swing.tree.DefaultMutableTreeNode; 24 | import javax.swing.tree.TreeCellRenderer; 25 | import java.awt.Component; 26 | import java.awt.Font; 27 | import java.awt.Graphics; 28 | 29 | /** 30 | * The cell renderer for tree nodes in the tool window. 31 | */ 32 | public class ResultTreeRenderer extends JLabel 33 | implements TreeCellRenderer { 34 | 35 | private boolean selected; 36 | 37 | /** 38 | * Create a new cell renderer. 39 | */ 40 | public ResultTreeRenderer() { 41 | super(); 42 | setOpaque(false); 43 | } 44 | 45 | @Override 46 | public void paintComponent(final Graphics g) { 47 | g.setColor(getBackground()); 48 | 49 | int offset = 0; 50 | if (getIcon() != null) { 51 | offset = getIcon().getIconWidth() + getIconTextGap(); 52 | } 53 | 54 | g.fillRect(offset, 0, getWidth() - 1 - offset, getHeight() - 1); 55 | 56 | if (selected) { 57 | g.setColor(JBColor.getColor("Tree.selectionBorderColor")); 58 | g.drawRect(offset, 0, getWidth() - 1 - offset, getHeight() - 1); 59 | } 60 | 61 | super.paintComponent(g); 62 | } 63 | 64 | @Override 65 | public Component getTreeCellRendererComponent(final JTree tree, 66 | final Object value, 67 | final boolean selected, 68 | final boolean expanded, 69 | final boolean leaf, 70 | final int row, 71 | final boolean hasFocus) { 72 | this.selected = selected; 73 | 74 | final DefaultMutableTreeNode node = (DefaultMutableTreeNode) value; 75 | if (node != null) { 76 | final Object userObject = node.getUserObject(); 77 | if (userObject instanceof ResultTreeNode) { 78 | final ResultTreeNode treeNode 79 | = (ResultTreeNode) userObject; 80 | 81 | if (expanded) { 82 | setIcon(treeNode.getExpandedIcon()); 83 | } else { 84 | setIcon(treeNode.getCollapsedIcon()); 85 | } 86 | 87 | setToolTipText(treeNode.getTooltip()); 88 | setText(treeNode.toString()); 89 | validate(); 90 | 91 | } else { 92 | setIcon(null); 93 | } 94 | } 95 | if (row == 0 && !leaf) { 96 | Font f = tree.getFont(); 97 | setFont(f.deriveFont(f.getStyle() | Font.BOLD)); 98 | } else { 99 | setFont(tree.getFont()); 100 | } 101 | 102 | setForeground(JBColor.getColor(selected 103 | ? "Tree.selectionForeground" : "Tree.textForeground")); 104 | setBackground(JBColor.getColor(selected 105 | ? "Tree.selectionBackground" : "Tree.textBackground")); 106 | 107 | return this; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/toolwindow/TogglableTreeNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.toolwindow; 18 | 19 | import javax.swing.tree.DefaultMutableTreeNode; 20 | import javax.swing.tree.TreeNode; 21 | import java.util.List; 22 | import java.util.stream.Collectors; 23 | 24 | /** 25 | * Tree node with togglable visibility. 26 | */ 27 | public class TogglableTreeNode extends DefaultMutableTreeNode { 28 | private static final long serialVersionUID = -4490734768175672868L; 29 | 30 | private boolean visible = true; 31 | 32 | public TogglableTreeNode() { 33 | } 34 | 35 | public TogglableTreeNode(final Object userObject) { 36 | super(userObject); 37 | } 38 | 39 | public boolean isVisible() { 40 | return visible; 41 | } 42 | 43 | public void setVisible(final boolean visible) { 44 | this.visible = visible; 45 | } 46 | 47 | List getAllChildren() { 48 | return children.stream() 49 | .map(child -> (TogglableTreeNode) child) 50 | .collect(Collectors.toList()); 51 | } 52 | 53 | @Override 54 | public TreeNode getChildAt(final int index) { 55 | int realIndex = -1; 56 | int visibleIndex = -1; 57 | 58 | for (final Object child : children) { 59 | final TogglableTreeNode node = (TogglableTreeNode) child; 60 | if (node.isVisible()) { 61 | ++visibleIndex; 62 | } 63 | ++realIndex; 64 | if (visibleIndex == index) { 65 | return children.get(realIndex); 66 | } 67 | } 68 | 69 | throw new ArrayIndexOutOfBoundsException("Invalid index: " + index); 70 | } 71 | 72 | @Override 73 | public int getChildCount() { 74 | if (children == null) { 75 | return 0; 76 | } 77 | 78 | int count = 0; 79 | for (final Object child : children) { 80 | final TogglableTreeNode node = (TogglableTreeNode) child; 81 | if (node.isVisible()) { 82 | ++count; 83 | } 84 | } 85 | 86 | return count; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/ui/PylintConfigPanel.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/ui/PylintConfigPanel.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.ui; 18 | 19 | import com.intellij.openapi.fileChooser.FileChooserDescriptor; 20 | import com.intellij.openapi.project.Project; 21 | import com.intellij.openapi.ui.TextComponentAccessor; 22 | import com.intellij.openapi.ui.TextFieldWithBrowseButton; 23 | import com.intellij.ui.components.JBTextField; 24 | import com.leinardi.pycharm.pylint.PylintBundle; 25 | import com.leinardi.pycharm.pylint.PylintConfigService; 26 | import com.leinardi.pycharm.pylint.plapi.PylintRunner; 27 | import com.leinardi.pycharm.pylint.util.Icons; 28 | import com.leinardi.pycharm.pylint.util.Notifications; 29 | 30 | import javax.swing.AbstractAction; 31 | import javax.swing.Action; 32 | import javax.swing.JButton; 33 | import javax.swing.JPanel; 34 | import java.awt.event.ActionEvent; 35 | 36 | public class PylintConfigPanel { 37 | private JPanel rootPanel; 38 | private JButton testButton; 39 | private com.intellij.openapi.ui.TextFieldWithBrowseButton pylintPathField; 40 | private com.intellij.openapi.ui.TextFieldWithBrowseButton pylintrcPathField; 41 | private JBTextField argumentsField; 42 | private Project project; 43 | 44 | public PylintConfigPanel(Project project) { 45 | this.project = project; 46 | PylintConfigService pylintConfigService = PylintConfigService.getInstance(project); 47 | if (pylintConfigService == null) { 48 | throw new IllegalStateException("PylintConfigService is null"); 49 | } 50 | testButton.setAction(new TestAction()); 51 | pylintPathField.setText(pylintConfigService.getCustomPylintPath()); 52 | FileChooserDescriptor fileChooserDescriptor = new FileChooserDescriptor( 53 | true, false, false, false, false, false); 54 | pylintPathField.addBrowseFolderListener( 55 | "", 56 | PylintBundle.message("config.pylint.path.tooltip"), 57 | null, 58 | fileChooserDescriptor, 59 | TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT); 60 | pylintrcPathField.setText(pylintConfigService.getPylintrcPath()); 61 | pylintrcPathField.addBrowseFolderListener( 62 | "", 63 | PylintBundle.message("config.pylintrc.path.tooltip"), 64 | null, 65 | fileChooserDescriptor, 66 | TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT); 67 | argumentsField.setText(pylintConfigService.getPylintArguments()); 68 | argumentsField.getEmptyText().setText(PylintBundle.message("config.optional")); 69 | } 70 | 71 | public JPanel getPanel() { 72 | return rootPanel; 73 | } 74 | 75 | public String getPylintPath() { 76 | return getPylintPath(false); 77 | } 78 | 79 | public String getPylintPath(boolean autodetect) { 80 | String path = pylintPathField.getText(); 81 | if (path.isEmpty() && autodetect) { 82 | return PylintRunner.getPylintPath(project, false); 83 | } 84 | return path; 85 | } 86 | 87 | public String getPylintrcPath() { 88 | return pylintrcPathField.getText(); 89 | } 90 | 91 | public String getPylintArguments() { 92 | return argumentsField.getText(); 93 | } 94 | 95 | @SuppressWarnings("unused") 96 | private void createUIComponents() { 97 | JBTextField autodetectTextField = new JBTextField(); 98 | autodetectTextField.getEmptyText() 99 | .setText(PylintBundle.message("config.auto-detect", PylintRunner.getPylintPath(project, false))); 100 | pylintPathField = new TextFieldWithBrowseButton(autodetectTextField); 101 | JBTextField optionalTextField = new JBTextField(); 102 | optionalTextField.getEmptyText().setText(PylintBundle.message("config.optional")); 103 | pylintrcPathField = new TextFieldWithBrowseButton(optionalTextField); 104 | } 105 | 106 | private final class TestAction extends AbstractAction { 107 | 108 | TestAction() { 109 | putValue(Action.NAME, PylintBundle.message( 110 | "config.pylint.path.test")); 111 | } 112 | 113 | @Override 114 | public void actionPerformed(final ActionEvent e) { 115 | String pathToPylint = getPylintPath(true); 116 | if (PylintRunner.isPylintPathValid(pathToPylint, project)) { 117 | testButton.setIcon(Icons.icon("/general/inspectionsOK.png")); 118 | Notifications.showInfo( 119 | project, 120 | PylintBundle.message("config.pylint.path.success.message") 121 | ); 122 | } else { 123 | testButton.setIcon(Icons.icon("/general/error.png")); 124 | Notifications.showError( 125 | project, 126 | PylintBundle.message("config.pylint.path.failure.message", pathToPylint) 127 | ); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/Async.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | import com.intellij.openapi.diagnostic.Logger; 20 | import com.intellij.openapi.application.ApplicationManager; 21 | import com.intellij.openapi.progress.ProcessCanceledException; 22 | import com.intellij.openapi.progress.ProgressManager; 23 | import org.jetbrains.annotations.NotNull; 24 | import org.jetbrains.annotations.Nullable; 25 | 26 | import java.util.concurrent.Callable; 27 | import java.util.concurrent.Future; 28 | 29 | public final class Async { 30 | private static final Logger LOG = Logger.getInstance(Async.class); 31 | 32 | private static final int FIFTY_MS = 50; 33 | 34 | private Async() { 35 | } 36 | 37 | @Nullable 38 | public static T asyncResultOf(@NotNull final Callable callable, 39 | @Nullable final T defaultValue, 40 | final long timeoutInMs) { 41 | try { 42 | return whenFinished(executeOnPooledThread(callable), timeoutInMs).get(); 43 | 44 | } catch (Exception e) { 45 | return defaultValue; 46 | } 47 | } 48 | 49 | public static Future executeOnPooledThread(final Callable callable) { 50 | return ApplicationManager.getApplication().executeOnPooledThread(callable); 51 | } 52 | 53 | public static Future whenFinished(final Future future, 54 | final long timeoutInMs) { 55 | long elapsedTime = 0; 56 | while (!future.isDone() && !future.isCancelled()) { 57 | ProgressManager.checkCanceled(); 58 | elapsedTime += waitFor(FIFTY_MS); 59 | 60 | if (timeoutInMs > 0 && elapsedTime >= timeoutInMs) { 61 | LOG.debug("Async task exhausted timeout of " + timeoutInMs + "ms, cancelling."); 62 | future.cancel(true); 63 | throw new ProcessCanceledException(); 64 | } 65 | } 66 | return future; 67 | } 68 | 69 | private static long waitFor(final int millis) { 70 | try { 71 | Thread.sleep(millis); 72 | } catch (InterruptedException ignored) { 73 | Thread.currentThread().interrupt(); 74 | } 75 | return millis; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/Exceptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | public final class Exceptions { 20 | 21 | private Exceptions() { 22 | } 23 | 24 | public static Throwable rootCauseOf(final Throwable t) { 25 | if (t.getCause() != null && t.getCause() != t) { 26 | return rootCauseOf(t.getCause()); 27 | } 28 | return t; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/FileTypes.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | import com.intellij.openapi.fileTypes.FileType; 20 | import com.jetbrains.python.PythonFileType; 21 | import org.jdesktop.swingx.util.OS; 22 | 23 | import java.nio.file.Files; 24 | import java.nio.file.Paths; 25 | 26 | public final class FileTypes { 27 | 28 | private FileTypes() { 29 | } 30 | 31 | public static boolean isPython(FileType fileType) { 32 | return fileType == PythonFileType.INSTANCE; 33 | } 34 | 35 | public static boolean isWindowsExecutable(String path) { 36 | return OS.isWindows() && Files.isExecutable(Paths.get(path)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/Icons.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | import javax.swing.ImageIcon; 20 | import java.net.URL; 21 | 22 | public final class Icons { 23 | 24 | private Icons() { 25 | } 26 | 27 | public static ImageIcon icon(final String iconPath) { 28 | final URL url = Icons.class.getResource(iconPath); 29 | if (url != null) { 30 | return new ImageIcon(url); 31 | } 32 | 33 | return null; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/OS.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | import java.util.Locale; 20 | 21 | public class OS { 22 | 23 | private static final String OPERATING_SYSTEM = System.getProperty("os.name").toLowerCase(Locale.ROOT); 24 | 25 | private OS() { 26 | } 27 | 28 | public static boolean isWindows() { 29 | return OPERATING_SYSTEM.contains("win"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/PyPackageManagerUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | import com.intellij.openapi.project.Project; 20 | import com.intellij.openapi.roots.ProjectRootManager; 21 | import com.intellij.webcore.packaging.PackageManagementService; 22 | import com.intellij.webcore.packaging.RepoPackage; 23 | import com.jetbrains.python.packaging.PyPackageManagers; 24 | 25 | @SuppressWarnings("SameParameterValue") 26 | public class PyPackageManagerUtil { 27 | private PyPackageManagerUtil() { 28 | } 29 | 30 | static void install(Project project, String packageName, PackageManagementService.Listener listener) { 31 | final PyPackageManagers packageManagers = PyPackageManagers.getInstance(); 32 | PackageManagementService managementService = packageManagers.getManagementService(project, 33 | ProjectRootManager.getInstance(project).getProjectSdk()); 34 | 35 | managementService.installPackage( 36 | new RepoPackage(packageName, null), 37 | null, 38 | false, 39 | null, 40 | listener, 41 | false); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/Strings.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | public final class Strings { 20 | 21 | private Strings() { 22 | } 23 | 24 | public static boolean isBlank(final String value) { 25 | return value == null || value.trim().isEmpty(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/TempDirProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | import com.intellij.openapi.project.Project; 20 | import com.intellij.openapi.project.ProjectUtil; 21 | import com.intellij.openapi.vfs.VirtualFile; 22 | import com.intellij.psi.PsiFile; 23 | import org.jetbrains.annotations.NotNull; 24 | 25 | import java.io.File; 26 | import java.nio.file.Path; 27 | import java.nio.file.Paths; 28 | import java.util.Optional; 29 | 30 | /** 31 | * Locate and/or create temporary directories for use by this plugin. 32 | */ 33 | public class TempDirProvider { 34 | public String forPersistedPsiFile(final PsiFile tempPsiFile) { 35 | String systemTempDir = System.getProperty("java.io.tmpdir"); 36 | if (OS.isWindows() && driveLetterOf(systemTempDir) != driveLetterOf(pathOf(tempPsiFile))) { 37 | // Some tool on Windows requires the files to be on the same drive 38 | final File projectTempDir = temporaryDirectoryLocationFor(tempPsiFile.getProject()); 39 | if (projectTempDir.exists() || projectTempDir.mkdirs()) { 40 | projectTempDir.deleteOnExit(); 41 | return projectTempDir.getAbsolutePath(); 42 | } 43 | } 44 | return systemTempDir; 45 | } 46 | 47 | @NotNull 48 | private File temporaryDirectoryLocationFor(final Project project) { 49 | return getIdeaFolder(project).map(vf -> new File(vf.getPath(), "pylintpylint.tmp")) 50 | .orElse(new File(project.getBasePath(), "pylintpylint.tmp")); 51 | } 52 | 53 | Optional getIdeaFolder(@NotNull final Project project) { 54 | VirtualFile projectDir = ProjectUtil.guessProjectDir(project); 55 | if (projectDir != null) { 56 | final VirtualFile ideaStorageDir = projectDir.findChild(Project.DIRECTORY_STORE_FOLDER); 57 | if (ideaStorageDir != null && ideaStorageDir.exists() && ideaStorageDir.isDirectory()) { 58 | return Optional.of(ideaStorageDir); 59 | } 60 | } 61 | return Optional.empty(); 62 | } 63 | 64 | private char driveLetterOf(final String windowsPath) { 65 | if (windowsPath != null && windowsPath.length() > 0) { 66 | final Path normalisedPath = Paths.get(windowsPath).normalize().toAbsolutePath(); 67 | return normalisedPath.toFile().toString().charAt(0); 68 | } 69 | return '?'; 70 | } 71 | 72 | private String pathOf(@NotNull final PsiFile file) { 73 | return virtualFileOf(file).map(VirtualFile::getPath).orElseThrow(() -> new IllegalStateException("PSIFile " + 74 | "does not have associated virtual file: " + file)); 75 | } 76 | 77 | private Optional virtualFileOf(final PsiFile file) { 78 | return Optional.ofNullable(file.getVirtualFile()); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/leinardi/pycharm/pylint/util/VfUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.leinardi.pycharm.pylint.util; 18 | 19 | import com.intellij.openapi.project.Project; 20 | import com.intellij.openapi.roots.ProjectFileIndex; 21 | import com.intellij.openapi.vfs.VfsUtilCore; 22 | import com.intellij.openapi.vfs.VirtualFile; 23 | import com.intellij.openapi.vfs.VirtualFileVisitor; 24 | import org.jetbrains.annotations.NotNull; 25 | 26 | import java.util.ArrayList; 27 | import java.util.Arrays; 28 | import java.util.Collection; 29 | import java.util.List; 30 | 31 | public class VfUtil { 32 | private VfUtil() { 33 | } 34 | 35 | public static VirtualFile findVfUp(VirtualFile item, String searchItemName) { 36 | VirtualFile parent = item.getParent(); 37 | if (parent != null) { 38 | VirtualFile vf = VfsUtilCore.findRelativeFile(searchItemName, parent); 39 | if (vf != null && !vf.isDirectory()) { 40 | return vf; 41 | } 42 | } 43 | return findVfUp(parent, searchItemName); 44 | } 45 | 46 | public static Collection getAllSubFiles(VirtualFile virtualFile) { 47 | Collection list = new ArrayList<>(); 48 | 49 | VfsUtilCore.visitChildrenRecursively(virtualFile, new VirtualFileVisitor() { 50 | @Override 51 | public boolean visitFile(@NotNull VirtualFile file) { 52 | if (!file.isDirectory()) { 53 | list.add(file); 54 | } 55 | return super.visitFile(file); 56 | } 57 | }); 58 | 59 | return list; 60 | } 61 | 62 | public static List filterOnlyPythonProjectFiles(Project project, VirtualFile[] virtualFiles) { 63 | return filterOnlyPythonProjectFiles(project, Arrays.asList(virtualFiles)); 64 | } 65 | 66 | public static List filterOnlyPythonProjectFiles(Project project, List virtualFiles) { 67 | List list = new ArrayList<>(); 68 | ProjectFileIndex projectFileIndex = ProjectFileIndex.SERVICE.getInstance(project); 69 | for (VirtualFile file : virtualFiles) { 70 | if (FileTypes.isPython(file.getFileType()) 71 | && !projectFileIndex.isExcluded(file) 72 | && !projectFileIndex.isInLibraryClasses(file)) { 73 | list.add(file); 74 | } 75 | } 76 | return list; 77 | } 78 | 79 | public static List flattenFiles(final VirtualFile[] files) { 80 | final List flattened = new ArrayList<>(); 81 | 82 | if (files != null) { 83 | for (final VirtualFile file : files) { 84 | flattened.addAll(VfUtil.getAllSubFiles(file)); 85 | } 86 | } 87 | return flattened; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/resources/com/leinardi/pycharm/pylint/PylintBundle.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023 Roberto Leinardi. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | plugin.class=com.leinardi.pycharm.pylint.PylintPlugin 17 | plugin.name=Pylint Plugin 18 | plugin.configuration-name=Pylint 19 | plugin.toolwindow.name=Scan 20 | plugin.toolwindow.action=Scan 21 | plugin.toolwindow.override=Rules: 22 | plugin.toolwindow.default-file= 23 | plugin.results.no-scan=No scan has been run as yet 24 | plugin.results.in-progress=A scan is in progress 25 | plugin.results.error=The scan failed due to an error - please see the event log \ 26 | for more information 27 | plugin.results.error.missing-property=Please define the property ''{0}'' in \ 28 | the Pylint settings panel 29 | plugin.results.error.instantiation-failed=The module {0} could not be loaded - \ 30 | please check you have added any prerequisites to the classpath 31 | plugin.results.scan-no-results=Pylint found no problems 32 | plugin.results.scan-results=Pylint found {0} in {1} file 33 | plugin.results.scan-results.fatal={0} fatal 34 | plugin.results.scan-results.error={0} error 35 | plugin.results.scan-results.warning={0} warning 36 | plugin.results.scan-results.convention={0} convention 37 | plugin.results.scan-results.refactor={0} refactor 38 | plugin.results.scan-results.info={0} info 39 | plugin.results.scan-file-result={0} : {1} 40 | plugin.results.file-result={0} ({1}:{2}) [{3}] 41 | plugin.results.unknown-source=unknown 42 | plugin.status.in-progress.current=Scanning current file... 43 | plugin.status.in-progress.module=Scanning current module... 44 | plugin.status.in-progress.no-file=No file is open for editing 45 | plugin.status.in-progress.no-module=The current file being edited does not belong to a module 46 | plugin.status.in-progress.project=Scanning current project... 47 | plugin.status.aborted=Check was aborted 48 | plugin.Pylint-PyCharm.description=

This plugin provides both real-time \ 49 | and on-demand scanning of Python files with Pylint from within the PyCharm IDE.

50 | plugin.notification.alerts=Pylint Alerts 51 | plugin.notification.logging=Pylint Logging 52 | plugin.notification.unable-to-run-pylint.subtitle=Unable to run Pylint 53 | plugin.notification.unable-to-run-pylint.content=Pylint is installed inside the project environment but the plugin \ 54 | is not able to run it. If you just installed it try to run File -> Synchronize or restart your IDE. \ 55 | If the problem persists you may need to manually enter the path to the Pylint executable inside the Plugin settings. 56 | plugin.notification.abnormal-exit.subtitle=Pylint exited abnormally 57 | plugin.notification.action.plugin-settings=Plugin settings 58 | plugin.notification.install-pylint.subtitle=Pylint missing 59 | plugin.notification.install-pylint.content=The project interpreter is missing Pylint, which is needed \ 60 | to properly check the imports. 61 | plugin.notification.no-python-interpreter.content=No Python interpreter configured for the project. 62 | plugin.notification.action.configure-python-interpreter=Configure Python interpreter 63 | plugin.notification.action.install-pylint=Install Pylint 64 | plugin.exception=Unexpected Exception Caught 65 | inspection.group=Pylint 66 | inspection.display-name=Pylint real-time scan 67 | inspection.message=Pylint: {0} 68 | pylint.file-io-failed=The Pylint rules file could not be read 69 | pylint.exception=The scan failed due to an exception: {0} 70 | pylint.exception-with-root-cause=The scan failed due to an exception: {0}
Root cause:
{1} 71 | config.pylint.path=Path to Pylint executable: 72 | config.pylint.path.warning=Must be installed on and selected from the project environment 73 | config.pylint.path.test=Test 74 | config.pylint.path.success.message=Success: executable found! 75 | config.pylint.path.failure.message=Failure: executable "{0}" not found 76 | config.pylint.path.tooltip=Pylint executable path 77 | config.pylint.arguments=Arguments: 78 | config.pylintrc.path=Path to pylintrc: 79 | config.pylintrc.path.tooltip=Pylintrc file path 80 | config.optional=Optional 81 | config.auto-detect=Auto-detected: {0} 82 | handler.before.checkin.checkbox=Scan with Pylint 83 | handler.before.checkin.error.text={0} files contain problems 84 | handler.before.checkin.error.title=Pylint Scan 85 | handler.before.checkin.error.review=Review 86 | handler.before.checkin.scan.in-progress=Scanning... 87 | handler.before.checkin.scan.text=Pylint is Scanning 88 | -------------------------------------------------------------------------------- /src/main/resources/com/leinardi/pycharm/pylint/images/pylint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/src/main/resources/com/leinardi/pycharm/pylint/images/pylint.png -------------------------------------------------------------------------------- /src/main/resources/com/leinardi/pycharm/pylint/images/pylint@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/src/main/resources/com/leinardi/pycharm/pylint/images/pylint@2x.png -------------------------------------------------------------------------------- /src/main/resources/com/leinardi/pycharm/pylint/images/pylint@2x_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/src/main/resources/com/leinardi/pycharm/pylint/images/pylint@2x_dark.png -------------------------------------------------------------------------------- /src/main/resources/com/leinardi/pycharm/pylint/images/pylint_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/src/main/resources/com/leinardi/pycharm/pylint/images/pylint_dark.png -------------------------------------------------------------------------------- /src/main/resources/com/leinardi/pycharm/pylint/images/pythonFile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/src/main/resources/com/leinardi/pycharm/pylint/images/pythonFile.png -------------------------------------------------------------------------------- /src/main/resources/com/leinardi/pycharm/pylint/images/pythonFile@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leinardi/pylint-pycharm/0db20e71511920c98713b5156d99b2421549ac2c/src/main/resources/com/leinardi/pycharm/pylint/images/pythonFile@2x.png -------------------------------------------------------------------------------- /src/main/resources/inspectionDescriptions/Pylint.html: -------------------------------------------------------------------------------- 1 | 16 | 17 | This inspection integrates Pylint and reports in real-time 18 | on problems against the current Pylint profile.
19 | Warning: PyLint real-time inspection is disabled by default as numerous users find running it in the 20 | background has a negative impact on their system performance. 21 | 22 | 23 | -------------------------------------------------------------------------------- /versions-plugin.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Roberto Leinardi. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | dependencyUpdates.resolutionStrategy = { 18 | componentSelection { rules -> 19 | rules.all { ComponentSelection selection -> 20 | boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm'].any { qualifier -> 21 | selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-]*/ 22 | } 23 | if (rejected) { 24 | selection.reject('Release candidate') 25 | } 26 | } 27 | } 28 | } 29 | --------------------------------------------------------------------------------