├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── release.yml │ └── run-ui-tests.yml ├── .gitignore ├── .run ├── Run Plugin Tests.run.xml ├── Run Plugin.run.xml └── Run Verifications.run.xml ├── CHANGELOG.md ├── CONTRIBUTION_GUIDE.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── qodana.yml ├── settings.gradle.kts └── src └── main ├── grammars ├── bitbucket │ ├── Codeowners.bnf │ └── CodeownersLexer.flex └── github │ ├── Codeowners.bnf │ └── CodeownersLexer.flex ├── kotlin └── com │ └── github │ └── fantom │ └── codeowners │ ├── CodeownersBarPanel.kt │ ├── CodeownersBarWidgetFactory.kt │ ├── CodeownersBundle.kt │ ├── CodeownersException.kt │ ├── CodeownersFileTypeDetector.kt │ ├── CodeownersIcons.kt │ ├── CodeownersManager.kt │ ├── CommonRunnableListeners.kt │ ├── Types.kt │ ├── codeInspection │ ├── CodeownersCoverPatternInspection.kt │ ├── CodeownersIncorrectEntryInspection.kt │ ├── CodeownersMetasymbolsUsageInspection.kt │ ├── CodeownersRelativeEntryFix.kt │ ├── CodeownersRelativeEntryInspection.kt │ ├── CodeownersRemoveRuleFix.kt │ └── CodeownersUnusedPatternInspection.kt │ ├── commenting │ └── CodeownersCommenter.kt │ ├── daemon │ └── CodeownersDirectoryMarkerProvider.kt │ ├── file │ └── type │ │ ├── CodeownersFileType.kt │ │ └── kind │ │ ├── BitbucketFileType.kt │ │ └── GithubFileType.kt │ ├── grouping │ ├── changes │ │ ├── CodeownersChangesGroupingPolicy.kt │ │ ├── MovedVirtualFile.kt │ │ └── SetCodeownersChangesGroupingAction.kt │ └── usage │ │ ├── CodeownersGroupingRule.kt │ │ ├── CodeownersUsageGroupingRuleProvider.kt │ │ ├── CodeownersUsageViewSettings.kt │ │ └── GroupByCodeownerAction.kt │ ├── highlighter │ ├── CodeownersColorSettingsPage.kt │ └── CodeownersHighlighterColors.kt │ ├── indexing │ ├── AbstractCodeownersFilesIndex.kt │ ├── CodeownersEntryOccurrence.kt │ ├── CodeownersFilesIndex.kt │ └── CodeownersSearchScope.kt │ ├── lang │ ├── CodeownersElementImpl.kt │ ├── CodeownersElementType.kt │ ├── CodeownersFile.kt │ ├── CodeownersLanguage.kt │ ├── CodeownersPatternBase.kt │ ├── CodeownersRuleBase.kt │ ├── CodeownersTokenType.kt │ ├── CodeownersVisitor.kt │ └── kind │ │ ├── bitbucket │ │ ├── BitbucketLanguage.kt │ │ ├── CodeownersFindUsagesProvider.kt │ │ ├── CodeownersLexerAdapter.kt │ │ ├── CodeownersParserDefinition.kt │ │ ├── highlighter │ │ │ ├── CodeownersHighlighter.kt │ │ │ └── CodeownersHighlighterFactory.kt │ │ └── psi │ │ │ ├── CodeownersElementType.kt │ │ │ ├── CodeownersEntryManipulator.kt │ │ │ ├── CodeownersRuleBase.kt │ │ │ ├── CodeownersTeamNameNamedElement.kt │ │ │ ├── CodeownersTokenType.kt │ │ │ └── impl │ │ │ ├── CodeownersNamedOwnerExtImpl.kt │ │ │ ├── CodeownersPatternExtImpl.kt │ │ │ ├── CodeownersRuleExtImpl.kt │ │ │ └── CodeownersTeamDefinitionExtImpl.kt │ │ └── github │ │ ├── CodeownersLexerAdapter.kt │ │ ├── CodeownersParserDefinition.kt │ │ ├── GithubLanguage.kt │ │ ├── highlighter │ │ ├── CodeownersHighlighter.kt │ │ └── CodeownersHighlighterFactory.kt │ │ └── psi │ │ ├── CodeownersElementType.kt │ │ ├── CodeownersNamedOwnerManipulator.kt │ │ ├── CodeownersPatternManipulator.kt │ │ ├── CodeownersRuleBase.kt │ │ ├── CodeownersTokenType.kt │ │ └── impl │ │ ├── CodeownersNamedOwnerExtImpl.kt │ │ ├── CodeownersPatternExtImpl.kt │ │ └── CodeownersRuleExtImpl.kt │ ├── reference │ ├── CodeownersGithubOwnerDocumentationProvider.kt │ ├── CodeownersGithubOwnerReference.kt │ ├── CodeownersPatternReferenceSetRecursiveReverse.kt │ ├── CodeownersPatternsMatchedFilesCache.kt │ └── CodeownersReferenceContributor.kt │ ├── search │ ├── CodeownersSearchFilter.kt │ ├── CodeownersSearchScope.kt │ ├── CodeownersSearchScopeDescriptor.kt │ ├── CodeownersSearchScopeDescriptorProvider.kt │ └── ui │ │ ├── CodeownersSearchFilterDialog.kt │ │ └── CodeownersSearchFilterDialogPanels.kt │ ├── services │ ├── CodeownersMatcher.kt │ └── PatternCache.kt │ ├── settings │ ├── CodeownersSettings.kt │ └── CodeownersSettingsConfigurable.kt │ ├── structureview │ ├── StructureViewElement.kt │ ├── StructureViewFactory.kt │ └── StructureViewModel.kt │ ├── ui │ ├── CodeownersSettingsPanel.form │ └── CodeownersSettingsPanel.kt │ └── util │ ├── CachedConcurrentMap.kt │ ├── Constants.kt │ ├── Debounced.kt │ ├── ExpiringMap.kt │ ├── Glob.kt │ ├── Listenable.kt │ ├── MatcherUtil.kt │ ├── Resources.kt │ ├── TimeTracer.kt │ └── Utils.kt └── resources ├── META-INF ├── plugin.xml └── pluginIcon.svg ├── colorSchemes ├── CodeownersDarcula.xml └── CodeownersDefault.xml ├── icons ├── icon.svg └── icon_dark.svg ├── inspectionDescriptions ├── CodeownersCoverPattern.html ├── CodeownersDuplicateEntry.html ├── CodeownersIncorrectEntry.html ├── CodeownersMetasymbolsUsage.html ├── CodeownersRelativeEntry.html └── CodeownersUnusedPattern.html ├── messages └── CodeownersBundle.properties └── sample.codeowners /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | tab_width = 4 8 | 9 | [*.{kt,kts}] 10 | disabled_rules = no-wildcard-imports 11 | 12 | [*.java] 13 | ij_java_doc_do_not_wrap_if_one_line = true 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | # Maintain dependencies for GitHub Actions 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. 2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. 3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. 4 | 5 | name: Release 6 | on: 7 | release: 8 | types: [prereleased, released] 9 | 10 | jobs: 11 | 12 | # Prepare and publish the plugin to JetBrains Marketplace repository 13 | release: 14 | name: Publish Plugin 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | steps: 20 | 21 | # Free GitHub Actions Environment Disk Space 22 | - name: Maximize Build Space 23 | uses: jlumbroso/free-disk-space@main 24 | with: 25 | tool-cache: false 26 | large-packages: false 27 | 28 | # Check out the current repository 29 | - name: Fetch Sources 30 | uses: actions/checkout@v4 31 | with: 32 | ref: ${{ github.event.release.tag_name }} 33 | 34 | # Set up Java environment for the next steps 35 | - name: Setup Java 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: zulu 39 | java-version: 21 40 | 41 | # Setup Gradle 42 | - name: Setup Gradle 43 | uses: gradle/actions/setup-gradle@v4 44 | with: 45 | gradle-home-cache-cleanup: true 46 | 47 | # Set environment variables 48 | - name: Export Properties 49 | id: properties 50 | shell: bash 51 | run: | 52 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 53 | ${{ github.event.release.body }} 54 | EOM 55 | )" 56 | 57 | echo "changelog<> $GITHUB_OUTPUT 58 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 59 | echo "EOF" >> $GITHUB_OUTPUT 60 | 61 | # Update the Unreleased section with the current release note 62 | - name: Patch Changelog 63 | if: ${{ steps.properties.outputs.changelog != '' }} 64 | env: 65 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 66 | run: | 67 | ./gradlew patchChangelog --release-note="$CHANGELOG" 68 | cat CHANGELOG.md || true 69 | 70 | # Publish the plugin to JetBrains Marketplace 71 | - name: Publish Plugin 72 | env: 73 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 74 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 75 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 76 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 77 | run: ./gradlew publishPlugin 78 | 79 | # Upload artifact as a release asset 80 | - name: Upload Release Asset 81 | env: 82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 83 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 84 | 85 | # Create a pull request 86 | - name: Create Pull Request 87 | if: ${{ steps.properties.outputs.changelog != '' }} 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 90 | run: | 91 | VERSION="${{ github.event.release.tag_name }}" 92 | BRANCH="changelog-update-$VERSION" 93 | LABEL="release changelog" 94 | 95 | git config user.email "action@github.com" 96 | git config user.name "GitHub Action" 97 | 98 | git checkout -b $BRANCH 99 | git commit -am "Changelog update - $VERSION" 100 | git push --set-upstream origin $BRANCH 101 | 102 | gh label create "$LABEL" \ 103 | --description "Pull requests with release changelog update" \ 104 | --force \ 105 | || true 106 | 107 | gh pr create \ 108 | --title "Changelog update - \`$VERSION\`" \ 109 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 110 | --label "$LABEL" \ 111 | --head $BRANCH 112 | -------------------------------------------------------------------------------- /.github/workflows/run-ui-tests.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: 2 | # - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI. 3 | # - Wait for IDE to start. 4 | # - Run UI tests with a separate Gradle task. 5 | # 6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform. 7 | # 8 | # Workflow is triggered manually. 9 | 10 | name: Run UI Tests 11 | on: 12 | workflow_dispatch 13 | 14 | jobs: 15 | 16 | testUI: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | runIde: | 24 | export DISPLAY=:99.0 25 | Xvfb -ac :99 -screen 0 1920x1080x16 & 26 | gradle runIdeForUiTests & 27 | - os: windows-latest 28 | runIde: start gradlew.bat runIdeForUiTests 29 | - os: macos-latest 30 | runIde: ./gradlew runIdeForUiTests & 31 | 32 | steps: 33 | 34 | # Free GitHub Actions Environment Disk Space 35 | - name: Maximize Build Space 36 | uses: jlumbroso/free-disk-space@main 37 | with: 38 | tool-cache: false 39 | large-packages: false 40 | 41 | # Check out the current repository 42 | - name: Fetch Sources 43 | uses: actions/checkout@v4 44 | 45 | # Set up Java environment for the next steps 46 | - name: Setup Java 47 | uses: actions/setup-java@v4 48 | with: 49 | distribution: zulu 50 | java-version: 21 51 | 52 | # Setup Gradle 53 | - name: Setup Gradle 54 | uses: gradle/actions/setup-gradle@v4 55 | with: 56 | gradle-home-cache-cleanup: true 57 | 58 | # Run IDEA prepared for UI testing 59 | - name: Run IDE 60 | run: ${{ matrix.runIde }} 61 | 62 | # Wait for IDEA to be started 63 | - name: Health Check 64 | uses: jtalk/url-health-check-action@v4 65 | with: 66 | url: http://127.0.0.1:8082 67 | max-attempts: 15 68 | retry-delay: 30s 69 | 70 | # Run tests 71 | - name: Tests 72 | run: ./gradlew test 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .qodana 4 | build 5 | /src/main/gen/ 6 | /.intellijPlatform/ 7 | -------------------------------------------------------------------------------- /.run/Run Plugin Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Verifications.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | false 23 | 24 | 25 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # intellij-codeowners Changelog 4 | 5 | ## Unreleased 6 | 7 | ## [v0.9.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.9.0) (2024-08-24) 8 | 9 | ### Added 10 | 11 | - Support IDEA 2024.2 12 | 13 | ### Fixed 14 | 15 | - Sync configuration files with template repository 16 | 17 | ### Removed 18 | 19 | - Support for IDEA versions older than 2024.2 20 | 21 | ## [v0.8.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.8.0) (2024-04-21) 22 | 23 | ### Added 24 | 25 | - Support IDEA 2024.1 26 | 27 | ### Fixed 28 | 29 | - Duplicate groups when grouping files by ownership in changelist, Pull Requests and usage views 30 | - Empty groups when grouping by ownership is combined with grouping by e.g. module or directory, in changelist or Pull Requests views 31 | - Opening `Search Structurally` dialog automatically opened `File ownership` scope dialog 32 | 33 | ### Removed 34 | 35 | - Support for IDEA versions older than 2024.1 36 | 37 | ## [v0.7.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.7.0) (2023-12-08) 38 | 39 | ### Added 40 | 41 | - Implement ownership-based search scope: build an ownership predicate in a DNF and filter files during search 42 | - Support IDEA 2023.3 43 | 44 | ## [v0.6.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.6.0) (2023-08-13) 45 | 46 | ### Added 47 | 48 | - Support IDEA 2023.2 49 | - Show ownership changes in Local Changes panel, after file/dir is moved/renamed 50 | 51 | ### Fixed 52 | 53 | - Exception on showing quick documentation (`F1`) for GitHub user name (`@user`) / team name (`@org/team`) 54 | - Allow trailing comments for GitHub syntax 55 | - AlreadyDisposedException was thrown when closing a project 56 | 57 | ### Removed 58 | 59 | - Support for IDEA versions older than 2023.2 60 | 61 | ## [v0.5.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.5.0) (2022-12-06) 62 | 63 | ### Added 64 | 65 | - Pattern overlap inspection: detect patterns that override other patterns earlier in file 66 | - Support IDEA 2022.3 67 | 68 | ### Fixed 69 | 70 | - Order of grouping by code owner and by file in search results 71 | 72 | ### Removed 73 | 74 | - Support for IDEA versions older than 2022.3 75 | 76 | ## [v0.4.1](https://github.com/fan-tom/intellij-codeowners/tree/v0.4.1) (2022-11-03) 77 | 78 | ### Fixed 79 | 80 | - Resolving files from file patterns when CODEOWNERS file not in the repository root 81 | - Proper translation of file patterns starting with `**/` into regex on pattern cache cleanup 82 | 83 | ## [v0.4.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.4.0) (2022-09-15) 84 | 85 | ### Added 86 | 87 | - Support of file paths with spaces and `@` for GitHub syntax 88 | 89 | ### Fixed 90 | 91 | - Incorrect parsing of paths without owners (reset ownership) for GitHub syntax 92 | - Resolving files from file patterns 93 | 94 | ### Removed 95 | 96 | - Support for IDEA versions older than 2022.1 97 | 98 | ## [v0.3.5](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.5) (2022-07-05) 99 | 100 | ### Added 101 | 102 | - Support codeowners unsetting for Github files, see https://github.community/t/codeowners-file-with-a-not-file-type-condition/1423/9 103 | - Support IDEA 2022.2 104 | 105 | ### Fixed 106 | 107 | - Speedup file references resolution (navigation through file tree using CTRL-click on CODEOWNERS file paths parts) 108 | 109 | ## [v0.3.4](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.4) (2022-03-29) 110 | 111 | ### Added 112 | 113 | - Structure view for Bitbucket files 114 | - Comment/uncomment actions 115 | 116 | ## [v0.3.3](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.3) (2022-03-20) 117 | 118 | ### Added 119 | 120 | - GoTo Team declaration for BitBucket files 121 | - Support IDEA 2022.1 122 | 123 | ## [v0.3.2](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.2) (2021-12-13) 124 | 125 | ### Added 126 | 127 | - Support `docs`, `.github`, `.bitibucket` dirs as CODEOWNERS file locations 128 | - Support bitbucket config lines 129 | 130 | ## [v0.3.1](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.1) (2021-11-25) 131 | 132 | ### Fixed 133 | 134 | - Bitbucket filetype detection 135 | 136 | ## [v0.3.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.3.0) (2021-08-09) 137 | 138 | ### Added 139 | 140 | - Group by owner in usage find results 141 | 142 | ## [v0.2.1](https://github.com/fan-tom/intellij-codeowners/tree/v0.2.1) (2021-08-01) 143 | 144 | ### Fixed 145 | 146 | - Support IDEA 2021.2 147 | 148 | ## [v0.2.0](https://github.com/fan-tom/intellij-codeowners/tree/v0.2.0) (2021-05-25) 149 | 150 | ### Added 151 | 152 | - Navigate from status bar to the line in CODEOWNERS file to know where code ownership is assigned 153 | 154 | ## [v0.1.0-eap.1](https://github.com/fan-tom/intellij-codeowners/tree/v0.1.0) (2021-05-24) 155 | 156 | ### Added 157 | 158 | - Files syntax highlight (lexical) 159 | - Show owner of currently opened file in IDE status bar 160 | - Group file changes by owners 161 | - Navigation to entries in Project view 162 | - Navigation to Github user/team by ctrl-click on owner 163 | -------------------------------------------------------------------------------- /CONTRIBUTION_GUIDE.md: -------------------------------------------------------------------------------- 1 | ### Terms: 2 | CODEOWNERS file - file that contains codeowners rules 3 | Rule - a record in the CODEOWNERS file, that consists of file path pattern and (optional) owners list 4 | File path pattern - a string that describes paths to files and folders, covered by corresponding rule, usually a glob 5 | 6 | Pseudo BNF: 7 | ```bnf 8 | File ::= { Rule } 9 | Rule ::= Assign | Reset 10 | Assign ::= Pattern {Owner} 11 | Reset ::= ... 12 | Pattern ::= Glob | ... 13 | Owner ::= Email | Username | Team | ... 14 | ``` 15 | Ellipsis (`...`) there says that different implementations of CODEOWNERS technology may use different syntaxes 16 | to represent corresponding terms 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Aleksey Kladov, Evgeny Kurbatsky, Alexey Kudinkin and contributors 4 | Copyright (c) 2016 JetBrains 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # intellij-codeowners 2 | 3 | ![Build](https://github.com/fan-tom/intellij-codeowners/workflows/Build/badge.svg) 4 | [![Version](https://img.shields.io/jetbrains/plugin/v/16811.svg)](https://plugins.jetbrains.com/plugin/16811) 5 | [![Downloads](https://img.shields.io/jetbrains/plugin/d/16811.svg)](https://plugins.jetbrains.com/plugin/16811) 6 | 7 | Introduction 8 | ------------ 9 | 10 | 11 | 12 | **CODEOWNERS** is a plugin for CODEOWNERS files in your project. 13 | 14 | Features: 15 | --------- 16 | 17 | - Files syntax highlight (lexical) 18 | - Show owner of currently opened file in IDE status bar 19 | - Group file changes by owners 20 | - Group usages by owners 21 | - Comments support 22 | - Navigation to entries in Project view 23 | - Navigation to Github user/team by ctrl-click on owner 24 | - Navigate from status bar to the line in CODEOWNERS file to know where code ownership is assigned 25 | - CODEOWNERS file structure view 26 | - Comment/uncomment actions 27 | 28 | TODO: 29 | ----- 30 | - Proper syntax-aware highlighting 31 | - Entries inspection (duplicated, covered, unused, incorrect syntax) with quick-fix actions 32 | - Support spaces in file paths for Bitbucket syntax 33 | - Tests 34 | 35 | Supported syntaxes: 36 | - [Github][github-syntax] 37 | - [Bitbucket][bitbucket-syntax] 38 | 39 | [github-syntax]: https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax 40 | [bitbucket-syntax]: https://mibexsoftware.atlassian.net/wiki/spaces/CODEOWNERS/pages/222822413/Usage 41 | 42 | 43 | 44 | ## Installation 45 | 46 | - Using IDE built-in plugin system: 47 | 48 | Preferences > Plugins > Marketplace > Search for "CODEOWNERS" > 49 | Install Plugin 50 | 51 | - Manually: 52 | 53 | Download the [latest release](https://github.com/fan-tom/intellij-codeowners/releases/latest) and install it manually using 54 | Preferences > Plugins > ⚙️ > Install plugin from disk... 55 | 56 | 57 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 2 | 3 | pluginGroup = com.github.fantom.codeowners 4 | pluginName = CODEOWNERS 5 | pluginRepositoryUrl = https://github.com/fan-tom/intellij-codeowners 6 | # SemVer format -> https://semver.org 7 | pluginVersion = 0.9.0 8 | 9 | # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 10 | pluginSinceBuild = 242 11 | #pluginUntilBuild = 242.*, let it work until it works 12 | 13 | # IntelliJ Platform Properties -> https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#configuration-intellij-extension 14 | platformType = IC 15 | platformVersion = 2024.2 16 | # https://github.com/JetBrains/intellij-platform-gradle-plugin/blob/main/CHANGELOG.md#200-beta2---2024-05-14 17 | # without this snapshot versions don't work, gradle cannot find them as binary releases 18 | # org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false 19 | 20 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 21 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 22 | platformPlugins = 23 | # Example: platformBundledPlugins = com.intellij.java 24 | platformBundledPlugins = 25 | 26 | # https://plugins.jetbrains.com/docs/intellij/plugin-compatibility.html 27 | # Don't forget to add module dependency to plugin.xml 28 | # VCS module has different names in plugin.xml and build script, see 29 | # https://plugins.jetbrains.com/docs/intellij/tools-intellij-platform-gradle-plugin-faq.html#pluginxml-cannot-resolve-plugin-comintellijmodulesvcs 30 | bundledModules = intellij.platform.vcs.impl 31 | 32 | # Gradle Releases -> https://github.com/gradle/gradle/releases 33 | gradleVersion = 8.9 34 | 35 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 36 | # suppress inspection "UnusedProperty" 37 | kotlin.stdlib.default.dependency = false 38 | 39 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 40 | # suppress inspection "UnusedProperty" 41 | org.gradle.unsafe.configuration-cache = true 42 | 43 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 44 | org.gradle.caching = true 45 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | junit = "4.13.2" 4 | 5 | # plugins 6 | kotlin = "2.1.20" 7 | intelliJPlatform = "2.4.0" 8 | changelog = "2.2.1" 9 | qodana = "2024.3.4" 10 | kover = "0.9.1" 11 | grammarkit = "2022.3.2.2" 12 | 13 | [libraries] 14 | junit = { group = "junit", name = "junit", version.ref = "junit" } 15 | 16 | [plugins] 17 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 18 | intelliJPlatform = { id = "org.jetbrains.intellij.platform", version.ref = "intelliJPlatform" } 19 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 20 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } 21 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 22 | grammarkit = { id = "org.jetbrains.grammarkit", version.ref = "grammarkit"} 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fan-tom/intellij-codeowners/648c39f5d2e48b0004d6df759591a8174ebc0a5b/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.9-bin.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 | -------------------------------------------------------------------------------- /qodana.yml: -------------------------------------------------------------------------------- 1 | # Qodana configuration: 2 | # https://www.jetbrains.com/help/qodana/qodana-yaml.html 3 | 4 | version: 1.0 5 | linter: jetbrains/qodana-jvm-community:latest 6 | projectJDK: "21" 7 | profile: 8 | name: qodana.recommended 9 | exclude: 10 | - name: All 11 | paths: 12 | - .qodana 13 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "intellij-codeowners" 2 | -------------------------------------------------------------------------------- /src/main/grammars/bitbucket/Codeowners.bnf: -------------------------------------------------------------------------------- 1 | // https://mibexsoftware.atlassian.net/wiki/spaces/CODEOWNERS/pages/222822413/Usage 2 | // playground: https://mibexsoftware.bitbucket.io/codeowners-playground/ 3 | { 4 | parserClass = "com.github.fantom.codeowners.lang.kind.bitbucket.parser.CodeownersParser" 5 | extends = "com.github.fantom.codeowners.lang.CodeownersElementImpl" 6 | 7 | psiClassPrefix = "Codeowners" 8 | psiImplClassSuffix = "Impl" 9 | psiPackage = "com.github.fantom.codeowners.lang.kind.bitbucket.psi" 10 | psiImplPackage = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl" 11 | 12 | elementTypeHolderClass = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTypes" 13 | elementTypeClass = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersElementType" 14 | tokenTypeClass = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTokenType" 15 | // tokenTypeClass = "com.github.fantom.codeowners.lang.CodeownersTokenType" 16 | 17 | tokens = [ 18 | WSS = "regexp:\s+" 19 | CRLF = "regexp:[\s\r\n]+" 20 | HEADER = "regexp:###.*" 21 | SECTION = "regexp:##.*" 22 | COMMENT = "regexp:#.*" 23 | SLASH = "/" 24 | AT = "@" 25 | // TEAMNAME = "regexp:[\w-]+" 26 | // USERNAME = "regexp:[\w\d-]+" 27 | // DOMAIN = "regexp:\w+(\.\w+)+" 28 | // VALUE = "regexp:[^@/\s]+" 29 | VALUE = "regexp:(?![!#\s])(?![\[\]])(?:\\[\[\]]|//|[^\[\]/\s])+" 30 | NAME_ = "regexp:[^@/\s]+" 31 | // TODO maybe make it more precise, like https://stackoverflow.com/a/12093994/7286194 32 | BRANCH_PATTERN = "regexp:\S+" 33 | //QUOTED_VALUE = 'regexp:"([^@/]+)+"' 34 | // VALUES_LIST = 'regexp:[^@/]+' 35 | // SPACES = 'regexp:\s+' 36 | ] 37 | 38 | name("Pattern.*") = "pattern" 39 | implements("Pattern") = "com.github.fantom.codeowners.lang.CodeownersPatternBase" 40 | mixin("Pattern") = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersPatternExtImpl" 41 | implements("Rule") = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersRuleBase" 42 | mixin("Rule") = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersRuleExtImpl" 43 | // mixin("NamedOwner") = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersNamedOwnerExtImpl" 44 | } 45 | 46 | codeownersFile ::= item_ * 47 | private item_ ::= HEADER | SECTION | COMMENT | ConfigurationLine | value_item_ | CRLF 48 | private value_item_ ::= (Rule | TeamDefinition) WSS? COMMENT? 49 | 50 | ConfigurationLine ::= DestinationBranchConfig | SubdirectoryOverridesConfig | CreatePullRequestCommentConfig 51 | DestinationBranchConfig ::= DESTINATION_BRANCH WSS BRANCH_PATTERN 52 | SubdirectoryOverridesConfig ::= SUBDIRECTORY_OVERRIDES WSS EnableOrDisable 53 | CreatePullRequestCommentConfig ::= CREATE_PULL_REQUEST_COMMENT WSS EnableOrDisable 54 | EnableOrDisable ::= ENABLE | DISABLE 55 | 56 | NEGATION ::= "!" 57 | Reset ::= NEGATION Pattern 58 | 59 | TeamDefinition ::= '@''@''@' TeamName WSS Owners { 60 | mixin = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersTeamDefinitionExtImpl" 61 | } 62 | 63 | Assign ::= Pattern /*SPACES*/ WSS Owners /*CRLF*/ 64 | 65 | Rule ::= Reset | Assign 66 | 67 | Pattern ::= PatternDirectory | PatternFile 68 | 69 | //QuotedEntry ::= '"' <> '"' 70 | //private meta QuotedEntry::= '"' <

> '"' 71 | //private meta entry_macro::= <> >> | <

> 72 | 73 | //Entry ::= '/' ? <> 74 | //private meta entry_file_raw ::= '/' ? <>>> 75 | //private meta entry_dir_raw ::= '/' ? <>>> '/' 76 | 77 | PatternDirectory ::= '/' ? <> '/' //| ('"' '/' ? <> '/' '"') //{ extends = "EntryFile"} 78 | PatternFile ::= '/' ? <> //| ('"' '/' ? <> '"') //{ extends = "Entry"} 79 | //EntryDirectory ::= ('/' ? <> '/') | <> '/'>> //{ extends = "EntryFile"} 80 | //EntryFile ::= ('/' ? <>) | <> >> //{ extends = "Entry"} 81 | 82 | Owners ::= Owner (WSS Owner)* 83 | Owner ::= Email | NamedOwner 84 | NamedOwner ::= Team | User 85 | 86 | Team ::= '@''@' TeamName { 87 | // mixin = "CodeownersTeamNameNamedElementImpl" 88 | // implements = "CodeownersTeamNameNamedElement" 89 | mixin = "com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersNamedOwnerExtImpl" 90 | // methods = [ getName setName getNameIdentifier ] 91 | } 92 | User ::= '@' UserName 93 | 94 | Email ::= UserName '@' Domain 95 | 96 | // or make it token? 97 | //QuotedValue ::= '"'(CRLF VALUE)*'"' 98 | 99 | UserName ::= NAME_ //| QUOTED_VALUE 100 | TeamName ::= NAME_ //| QUOTED_VALUE 101 | Domain ::= NAME_ 102 | 103 | private meta list_macro ::= <

> + ('/' <

> +) * -------------------------------------------------------------------------------- /src/main/grammars/bitbucket/CodeownersLexer.flex: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.lexer; 2 | 3 | import com.intellij.lexer.*; 4 | import com.intellij.psi.tree.IElementType; 5 | 6 | import static com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTypes.*; 7 | 8 | %% 9 | 10 | %{ 11 | public CodeownersLexer() { 12 | this((java.io.Reader)null); 13 | } 14 | %} 15 | 16 | %public 17 | %class CodeownersLexer 18 | %implements FlexLexer 19 | %function advance 20 | %type IElementType 21 | %unicode 22 | 23 | //EOL=\R 24 | //WHITE_SPACE={SPACES} 25 | 26 | CRLF = "\r" | "\n" | "\r\n" 27 | LINE_WS = [\ \t\f] 28 | WHITE_SPACE = ({LINE_WS}*{CRLF}+)+ 29 | 30 | HEADER = ###[^\r\n]* 31 | SECTION = ##[^\r\n]* 32 | COMMENT = #[^\r\n]* 33 | NEGATION = \! 34 | SLASH = \/ 35 | ATATAT = @@@ 36 | AT = @ 37 | QUOTE = \" 38 | 39 | DESTINATION_BRANCH_PATTERN = CODEOWNERS.destination_branch_pattern 40 | CREATE_PULL_REQUEST_COMMENT = CODEOWNERS.toplevel.create_pull_request_comment 41 | SUBDIRECTORY_OVERRIDES = CODEOWNERS.toplevel.subdirectory_overrides 42 | ENABLE = enable 43 | DISABLE = disable 44 | BRANCH_PATTERN = [^\s]+ 45 | 46 | 47 | RULE_FIRST_CHARACTER = [^#\ ] 48 | //VALUE=[^@/\s\/]+ 49 | VALUE = ("\\\["|"\\\]"|"\\\/"|[^\[\]\r\n\/\s])+ 50 | NAME_ = [^#@/\s\/]+ 51 | //VALUES_LIST=[^@/]+ 52 | SPACES = \s+ 53 | 54 | %state IN_BRANCH_PATTERN, IN_TOPLEVEL_CONFIG, IN_PATTERN, IN_OWNERS, IN_TEAM_DEFINITION 55 | 56 | %% 57 | { 58 | {WHITE_SPACE} { yybegin(YYINITIAL); return CRLF; } 59 | {LINE_WS}+ { return WSS; } 60 | {HEADER} { return HEADER; } 61 | {SECTION} { return SECTION; } 62 | {COMMENT} { return COMMENT; } 63 | {NEGATION} { return NEGATION; } 64 | 65 | {ATATAT} { yypushback(yylength()); yybegin(IN_TEAM_DEFINITION); } 66 | 67 | {DESTINATION_BRANCH_PATTERN} { yybegin(IN_BRANCH_PATTERN); return DESTINATION_BRANCH; } 68 | {SUBDIRECTORY_OVERRIDES} { yybegin(IN_TOPLEVEL_CONFIG); return SUBDIRECTORY_OVERRIDES; } 69 | {CREATE_PULL_REQUEST_COMMENT} { yybegin(IN_TOPLEVEL_CONFIG); return CREATE_PULL_REQUEST_COMMENT; } 70 | 71 | {RULE_FIRST_CHARACTER} { yypushback(1); yybegin(IN_PATTERN); } 72 | } 73 | 74 | { 75 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; } 76 | {LINE_WS}+ { yybegin(IN_BRANCH_PATTERN); return WSS; } 77 | {BRANCH_PATTERN} { yybegin(IN_BRANCH_PATTERN); return BRANCH_PATTERN; } 78 | } 79 | 80 | { 81 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; } 82 | {LINE_WS}+ { yybegin(IN_TOPLEVEL_CONFIG); return WSS; } 83 | {ENABLE} { yybegin(IN_TOPLEVEL_CONFIG); return ENABLE; } 84 | {DISABLE} { yybegin(IN_TOPLEVEL_CONFIG); return DISABLE; } 85 | } 86 | 87 | { 88 | {AT} { yybegin(IN_TEAM_DEFINITION); return AT; } 89 | {NAME_} { yybegin(IN_TEAM_DEFINITION); return NAME_; } 90 | // {QUOTED_VALUE} { return QUOTED_VALUE; } 91 | {LINE_WS}+ { yybegin(IN_OWNERS); return WSS; } 92 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; } 93 | } 94 | 95 | { 96 | // {QUOTE} { yybegin(IN_WS_ENTRY); return QUOTE; } 97 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; } 98 | {LINE_WS}+ { yybegin(IN_OWNERS); return WSS; } 99 | {SLASH} { yybegin(IN_PATTERN); return SLASH; } 100 | 101 | // "@" { return AT; } 102 | 103 | {VALUE} { yybegin(IN_PATTERN); return VALUE; } 104 | // {SPACES} { return SPACES; } 105 | } 106 | 107 | // { 108 | // {LINE_WS}+ { yybegin(IN_OWNERS); return CRLF; } 109 | // {SLASH} { yybegin(IN_ENTRY); return SLASH; } 110 | // {VALUES_LIST} { yybegin(IN_WS_ENTRY); return VALUES_LIST; } 111 | //} 112 | 113 | { 114 | {COMMENT} { yybegin(YYINITIAL); return COMMENT; } 115 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; } 116 | {LINE_WS}+ { yybegin(IN_OWNERS); return WSS; } 117 | {NAME_} { yybegin(IN_OWNERS); return NAME_; } 118 | // {SLASH} { return SLASH; } 119 | {AT} { yybegin(IN_OWNERS); return AT; } 120 | } 121 | 122 | //[^] { return BAD_CHARACTER; } 123 | -------------------------------------------------------------------------------- /src/main/grammars/github/Codeowners.bnf: -------------------------------------------------------------------------------- 1 | // https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax 2 | { 3 | parserClass = "com.github.fantom.codeowners.lang.kind.github.parser.CodeownersParser" 4 | extends = "com.github.fantom.codeowners.lang.CodeownersElementImpl" 5 | 6 | psiClassPrefix = "Codeowners" 7 | psiImplClassSuffix = "Impl" 8 | psiPackage = "com.github.fantom.codeowners.lang.kind.github.psi" 9 | psiImplPackage = "com.github.fantom.codeowners.lang.kind.github.psi.impl" 10 | 11 | elementTypeHolderClass = "com.github.fantom.codeowners.lang.kind.github.psi.CodeownersTypes" 12 | elementTypeClass = "com.github.fantom.codeowners.lang.kind.github.psi.CodeownersElementType" 13 | // elementTypeClass = "com.github.fantom.codeowners.lang.CodeownersElementType" 14 | tokenTypeClass = "com.github.fantom.codeowners.lang.kind.github.psi.CodeownersTokenType" 15 | // tokenTypeClass = "com.github.fantom.codeowners.lang.CodeownersTokenType" 16 | 17 | tokens = [ 18 | CRLF = "regexp:[\s\r\n]+" 19 | HEADER = "regexp:###.*" 20 | SECTION = "regexp:##.*" 21 | COMMENT = "regexp:#.*" 22 | SLASH = "/" 23 | AT = "@" 24 | // TEAMNAME = "regexp:[\w-]+" 25 | // USERNAME = "regexp:[\w\d-]+" 26 | // DOMAIN = "regexp:\w+(\.\w+)+" 27 | VALUE = "regexp:[^@\s/]+" 28 | PATHNAME = "regexp:([^\s/]|\\\s)+" 29 | SPACES = 'regexp:[\s\t\f]+' 30 | ] 31 | 32 | name("Pattern.*") = "pattern" 33 | mixin("Pattern") = "com.github.fantom.codeowners.lang.kind.github.psi.impl.CodeownersPatternExtImpl" 34 | mixin("NamedOwner") = "com.github.fantom.codeowners.lang.kind.github.psi.impl.CodeownersNamedOwnerExtImpl" 35 | implements("Pattern") = "com.github.fantom.codeowners.lang.CodeownersPatternBase" 36 | implements("Rule") = "com.github.fantom.codeowners.lang.kind.github.psi.CodeownersRuleBase" 37 | mixin("Rule") = "com.github.fantom.codeowners.lang.kind.github.psi.impl.CodeownersRuleExtImpl" 38 | } 39 | 40 | codeownersFile ::= item_ * 41 | private item_ ::= HEADER | SECTION | COMMENT | value_item_ | CRLF 42 | private value_item_ ::= Rule SPACES? COMMENT? 43 | 44 | Reset ::= (PatternDirectory | PatternFile) 45 | 46 | Assign ::= (PatternDirectory | PatternFile) SPACES Owners 47 | 48 | Rule ::= Assign | Reset 49 | 50 | Pattern ::= '/' ? <> 51 | PatternDirectory ::= '/' ? <> '/' { extends = "PatternFile"} 52 | PatternFile ::= '/' ? <> { extends = "Pattern"} 53 | 54 | Owners ::= Owner (SPACES Owner)* 55 | Owner ::= Email | NamedOwner 56 | NamedOwner ::= '@' OwnerName 57 | 58 | OwnerName ::= Team | UserName 59 | Team ::= OrgName '/' TeamName 60 | 61 | Email ::= UserName '@' Domain 62 | 63 | UserName ::= VALUE 64 | OrgName ::= VALUE 65 | TeamName ::= VALUE 66 | Domain ::= VALUE 67 | 68 | private meta list_macro ::= <

> + ('/' <

> +) * 69 | -------------------------------------------------------------------------------- /src/main/grammars/github/CodeownersLexer.flex: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.lexer; 2 | 3 | import com.intellij.lexer.*; 4 | import com.intellij.psi.tree.IElementType; 5 | 6 | import static com.github.fantom.codeowners.lang.kind.github.psi.CodeownersTypes.*; 7 | 8 | %% 9 | 10 | %{ 11 | public CodeownersLexer() { 12 | this((java.io.Reader)null); 13 | } 14 | %} 15 | 16 | %public 17 | %class CodeownersLexer 18 | %implements FlexLexer 19 | %function advance 20 | %type IElementType 21 | %unicode 22 | 23 | CRLF = "\r" | "\n" | "\r\n" 24 | LINE_WS = [\ \t\f] 25 | WHITE_SPACE = ({LINE_WS}*{CRLF}+)+ 26 | 27 | HEADER = ###[^\r\n]* 28 | SECTION = ##[^\r\n]* 29 | COMMENT = #[^\r\n]* 30 | SLASH = \/ 31 | AT = @ 32 | 33 | 34 | FIRST_CHARACTER = [^#\s] 35 | VALUE = [^@\s/]+ 36 | PATHNAME = ([^\s/]|\\\s)+ 37 | 38 | %state IN_PATTERN, IN_OWNERS 39 | 40 | %% 41 | { 42 | {WHITE_SPACE}+ { yybegin(YYINITIAL); return CRLF; } 43 | {LINE_WS}+ { return SPACES; } 44 | {HEADER} { return HEADER; } 45 | {SECTION} { return SECTION; } 46 | {COMMENT} { return COMMENT; } 47 | 48 | {FIRST_CHARACTER} { yypushback(1); yybegin(IN_PATTERN); } 49 | } 50 | 51 | { 52 | {LINE_WS}+ { yybegin(IN_OWNERS); return SPACES; } 53 | {SLASH} { yybegin(IN_PATTERN); return SLASH; } 54 | 55 | {PATHNAME} { yybegin(IN_PATTERN); return PATHNAME; } 56 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; } 57 | } 58 | 59 | { 60 | {COMMENT} { yybegin(YYINITIAL); return COMMENT; } 61 | {CRLF}+ { yybegin(YYINITIAL); return CRLF; } 62 | {LINE_WS}+ { yybegin(IN_OWNERS); return SPACES; } 63 | {VALUE} { yybegin(IN_OWNERS); return VALUE; } 64 | {SLASH} { yybegin(IN_OWNERS); return SLASH; } 65 | {AT} { yybegin(IN_OWNERS); return AT; } 66 | } 67 | 68 | //[^] { return BAD_CHARACTER; } 69 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/CodeownersBarWidgetFactory.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.util.Disposer 5 | import com.intellij.openapi.wm.StatusBarWidget 6 | import com.intellij.openapi.wm.impl.status.widget.StatusBarEditorBasedWidgetFactory 7 | import org.jetbrains.annotations.Nls 8 | 9 | class CodeownersBarWidgetFactory : StatusBarEditorBasedWidgetFactory() { 10 | override fun getId() = "CodeownersPanel" 11 | 12 | @Nls 13 | override fun getDisplayName() = CodeownersBundle.message("status.bar.codeowners.widget.name") 14 | 15 | override 16 | fun createWidget(project: Project) = CodeownersBarPanel(project) 17 | 18 | override fun disposeWidget(widget: StatusBarWidget) { 19 | Disposer.dispose(widget) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/CodeownersBundle.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersLanguage 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.BitbucketLanguage 5 | import com.github.fantom.codeowners.lang.kind.github.GithubLanguage 6 | import com.intellij.AbstractBundle 7 | import org.jetbrains.annotations.NonNls 8 | import org.jetbrains.annotations.PropertyKey 9 | import java.util.ArrayList 10 | import java.util.ResourceBundle 11 | 12 | /** 13 | * [ResourceBundle]/localization utils for the CODEOWNERS support plugin. 14 | */ 15 | object CodeownersBundle : AbstractBundle("messages.CodeownersBundle") { 16 | 17 | @NonNls 18 | const val BUNDLE_NAME = "messages.CodeownersBundle" 19 | 20 | private val BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME) 21 | 22 | val LANGUAGES = CodeownersLanguages(listOf(GithubLanguage.INSTANCE, BitbucketLanguage.INSTANCE)) 23 | 24 | /** 25 | * Loads a [String] from the [.BUNDLE] [ResourceBundle]. 26 | * 27 | * @param key the key of the resource 28 | * @param params the optional parameters for the specific resource 29 | * @return the [String] value or `null` if no resource found for the key 30 | */ 31 | fun message(@PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg params: Any?) = message(BUNDLE, key, *params) 32 | 33 | /** 34 | * Loads a [String] from the [.BUNDLE] [ResourceBundle]. 35 | * 36 | * @param key the key of the resource 37 | * @param params the optional parameters for the specific resource 38 | * @return the [String] value or `null` if no resource found for the key 39 | */ 40 | fun messagePointer(@PropertyKey(resourceBundle = BUNDLE_NAME) key: String, vararg params: Any?) = getLazyMessage(key, *params) 41 | 42 | /** 43 | * Simple [ArrayList] with method to find [CodeownersLanguage] by its name. 44 | */ 45 | class CodeownersLanguages(languages: List) : ArrayList(languages) { 46 | 47 | operator fun get(id: String) = find { id == it.id } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/CodeownersException.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners 2 | 3 | /** 4 | * Ignore [Exception] definition. 5 | */ 6 | open class CodeownersException(message: String? = null) : RuntimeException(message) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/CodeownersFileTypeDetector.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners 2 | 3 | import com.github.fantom.codeowners.file.type.kind.BitbucketFileType 4 | import com.github.fantom.codeowners.file.type.kind.GithubFileType 5 | import com.github.fantom.codeowners.lang.CodeownersLanguage 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.fileTypes.FileType 8 | import com.intellij.openapi.fileTypes.FileTypeRegistry 9 | import com.intellij.openapi.util.io.ByteSequence 10 | import com.intellij.openapi.vfs.VirtualFile 11 | 12 | class CodeownersFileTypeDetector : FileTypeRegistry.FileTypeDetector { 13 | companion object { 14 | private val LOGGER = Logger.getInstance(CodeownersFileTypeDetector::class.java) 15 | } 16 | 17 | private fun detectBitbucketFileType(line: String): Boolean { 18 | return line.startsWith("@@@") || // team definition 19 | line.startsWith("CODEOWNERS.") || // settings 20 | line.startsWith("!") // negation. TODO WARN, may be imprecise 21 | } 22 | 23 | /** 24 | * [com.intellij.openapi.fileTypes.ex.FileTypeIdentifiableByVirtualFile.isMyFileType] is already called here, 25 | * so we check only file name and content 26 | */ 27 | override fun detect(file: VirtualFile, firstBytes: ByteSequence, firstCharsIfText: CharSequence?): FileType? { 28 | LOGGER.trace("Detecting lang: ${file.path}") 29 | return if (file.name == CodeownersLanguage.INSTANCE.filename) { 30 | if (firstCharsIfText != null) { 31 | if (firstCharsIfText.lineSequence().any(::detectBitbucketFileType)) { 32 | LOGGER.trace("Detected lang using firstCharsIfText: bb") 33 | BitbucketFileType 34 | // firstCharsIfText may be not enough to find bb-specific pattern, so check whole file content 35 | } else if (file.inputStream.reader().useLines { it.any(::detectBitbucketFileType) }) { 36 | LOGGER.trace("Detected lang using file content: bb") 37 | BitbucketFileType 38 | } else { 39 | LOGGER.trace("Detected lang: gh") 40 | GithubFileType 41 | } 42 | } else { 43 | // CODEOWNERS file must be text, not binary 44 | LOGGER.trace("Detected lang: non-text") 45 | null 46 | } 47 | } else { 48 | LOGGER.trace("Detected lang: non-codeowners") 49 | null 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/CodeownersIcons.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners 2 | 3 | import com.intellij.openapi.util.IconLoader 4 | 5 | internal object CodeownersIcons { 6 | val FILE = IconLoader.getIcon("/icons/icon.svg", CodeownersIcons::class.java) 7 | } 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/CommonRunnableListeners.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners 2 | 3 | import com.intellij.openapi.module.Module 4 | import com.intellij.openapi.project.ModuleListener 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.roots.ModuleRootEvent 7 | import com.intellij.openapi.roots.ModuleRootListener 8 | import com.intellij.util.Function 9 | 10 | /** 11 | * Wrapper for common listeners. 12 | */ 13 | class CommonRunnableListeners(private val task: Runnable) : CodeownersManager.RefreshStatusesListener, ModuleRootListener, ModuleListener { 14 | 15 | override fun refresh() = task.run() 16 | 17 | override fun beforeRootsChange(event: ModuleRootEvent) = Unit 18 | 19 | override fun rootsChanged(event: ModuleRootEvent) = task.run() 20 | 21 | override fun modulesAdded(project: Project, module: List) = task.run() 22 | 23 | override fun beforeModuleRemoved(project: Project, module: Module) = Unit 24 | 25 | override fun moduleRemoved(project: Project, module: Module) = task.run() 26 | 27 | override fun modulesRenamed( 28 | project: Project, 29 | modules: MutableList, 30 | oldNameProvider: Function 31 | ) = task.run() 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/Types.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/codeInspection/CodeownersCoverPatternInspection.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.codeInspection 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.lang.CodeownersFile 5 | import com.github.fantom.codeowners.services.PatternCache 6 | import com.github.fantom.codeowners.util.Utils 7 | import com.intellij.codeInspection.InspectionManager 8 | import com.intellij.codeInspection.LocalInspectionTool 9 | import com.intellij.codeInspection.ProblemDescriptor 10 | import com.intellij.codeInspection.ProblemsHolder 11 | import com.intellij.openapi.progress.ProgressManager 12 | import com.intellij.openapi.util.io.FileUtil 13 | import com.intellij.openapi.vfs.VirtualFile 14 | import com.intellij.psi.PsiElement 15 | import com.intellij.psi.PsiFile 16 | import dk.brics.automaton.BasicOperations 17 | 18 | /** 19 | * Inspection tool that checks if earlier entries are covered by later ones. 20 | */ 21 | class CodeownersCoverPatternInspection : LocalInspectionTool() { 22 | 23 | /** 24 | * Reports problems at file level. Checks if entries are covered by other entries. 25 | * 26 | * @param file current working file to check 27 | * @param manager [InspectionManager] to ask for [ProblemDescriptor]'s from 28 | * @param isOnTheFly true if called during on the fly editor highlighting. Called from Inspect Code action otherwise 29 | * @return `null` if no problems found or not applicable at file level 30 | */ 31 | @Suppress("ComplexMethod", "NestedBlockDepth", "ReturnCount") 32 | override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { 33 | val virtualFile = file.virtualFile 34 | if (!Utils.isInProject(virtualFile, file.project)) { 35 | return null 36 | } 37 | val codeownersFile = file as? CodeownersFile ?: return null 38 | 39 | val problemsHolder = ProblemsHolder(manager, file, isOnTheFly) 40 | 41 | val cache = PatternCache.getInstance() 42 | 43 | val rules = codeownersFile.getRules() 44 | val compiledRegexes = 45 | rules.map { rulePsi -> 46 | val glob = rulePsi.pattern.value 47 | cache.getOrCreateGlobRegexes2(glob) 48 | } 49 | 50 | val regexes = rules.zip(compiledRegexes) 51 | 52 | for ((idx, pivot) in regexes.withIndex()) { 53 | for ((rulePsi, current) in regexes.drop(idx + 1)) { 54 | ProgressManager.checkCanceled() 55 | // improper subsetOf 56 | if (BasicOperations.subsetOf(pivot.second, current)) { 57 | val coveredPattern = pivot.first.pattern 58 | val coveringPattern = rulePsi.pattern 59 | problemsHolder.registerProblem( 60 | coveredPattern, 61 | message(coveringPattern, virtualFile) 62 | ) 63 | } 64 | } 65 | } 66 | 67 | return problemsHolder.resultsArray 68 | } 69 | 70 | override fun runForWholeFile() = true 71 | 72 | /** 73 | * Helper for inspection message generating. 74 | * 75 | * @param coveringPattern entry that covers message related 76 | * @param virtualFile current working file 77 | * otherwise 78 | * @return generated message [String] 79 | */ 80 | private fun message( 81 | coveringPattern: PsiElement, 82 | virtualFile: VirtualFile 83 | ): String { 84 | val path = FileUtil.toSystemIndependentName(virtualFile.path) 85 | return CodeownersBundle.message( 86 | "codeInspection.coverPattern.message", 87 | """${coveringPattern.text}""" 88 | ) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/codeInspection/CodeownersIncorrectEntryInspection.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.codeInspection 2 | 3 | /** 4 | * Inspection tool that checks if entry has correct form in specific according to the specific [ ]. 5 | */ 6 | //class CodeownersIncorrectEntryInspection : LocalInspectionTool() { 7 | // override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean) = 8 | // object : com.github.fantom.codeowners.lang.kind.github.psi.CodeownersVisitor() { 9 | // override fun visitEntry(entry: com.github.fantom.codeowners.lang.kind.github.psi.CodeownersEntry) { 10 | // val regex = entry.regex(false) 11 | // 12 | // try { 13 | // Pattern.compile(regex) 14 | // } catch (e: PatternSyntaxException) { 15 | // holder.registerProblem( 16 | // entry, 17 | // CodeownersBundle.message("codeInspection.incorrectEntry.message", e.description) 18 | // ) 19 | // } 20 | // } 21 | // } 22 | //} 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/codeInspection/CodeownersMetasymbolsUsageInspection.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.codeInspection 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.file.type.CodeownersFileType 5 | import com.github.fantom.codeowners.lang.CodeownersRuleBase 6 | import com.github.fantom.codeowners.lang.CodeownersVisitor 7 | import com.intellij.codeInspection.InspectionManager 8 | import com.intellij.codeInspection.LocalInspectionTool 9 | import com.intellij.codeInspection.ProblemHighlightType 10 | import com.intellij.codeInspection.ProblemsHolder 11 | import com.intellij.openapi.util.TextRange 12 | import com.intellij.psi.PsiElementVisitor 13 | 14 | /** 15 | * Inspection tool that checks incorrect usage of metasymbols: 16 | * - two double stars in a row 17 | * - doublestars at the beginning of the pattern if there are no other slashes 18 | */ 19 | class CodeownersMetasymbolsUsageInspection : LocalInspectionTool() { 20 | 21 | override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { 22 | // val matcher = holder.project.service() 23 | val inspectionManager = InspectionManager.getInstance(holder.project) 24 | val visitor = object : CodeownersVisitor() { 25 | override fun visitRule(rule: CodeownersRuleBase<*, *>) { 26 | val pattern = rule.pattern 27 | val text = pattern.text 28 | if (text.startsWith("**/")) { 29 | when (text.indexOf('/', 3)) { 30 | -1, text.indices.last -> 31 | holder.registerProblem( 32 | pattern.findElementAt(1)!!, 33 | CodeownersBundle.message("codeInspection.metasymbolsUsage.leadingDoubleStar"), 34 | ) 35 | } 36 | } 37 | val doubleStars = "**/**" 38 | val idx = text.indexOf(doubleStars) 39 | if (idx != -1) { 40 | holder.registerProblem( 41 | inspectionManager.createProblemDescriptor( 42 | pattern, 43 | TextRange(idx, idx + doubleStars.length), 44 | CodeownersBundle.message("codeInspection.metasymbolsUsage.consecutiveDoubleStars"), 45 | ProblemHighlightType.WEAK_WARNING, 46 | isOnTheFly 47 | ) 48 | ) 49 | } 50 | } 51 | } 52 | 53 | return (holder.file.fileType as CodeownersFileType).codeownersLanguage.getVisitor(visitor)!! 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/codeInspection/CodeownersRelativeEntryFix.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.codeInspection 2 | 3 | //import com.github.fantom.codeowners.CodeownersBundle 4 | //import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersEntry 5 | //import com.intellij.codeInspection.LocalQuickFixOnPsiElement 6 | //import com.intellij.openapi.project.Project 7 | //import com.intellij.psi.PsiDocumentManager 8 | //import com.intellij.psi.PsiElement 9 | //import com.intellij.psi.PsiFile 10 | //import java.net.URI 11 | //import java.net.URISyntaxException 12 | // 13 | ///** 14 | // * QuickFix action that removes relative parts of the entry [CodeownersRelativeEntryInspection]. 15 | // */ 16 | //class CodeownersRelativeEntryFix(entry: CodeownersEntry) : LocalQuickFixOnPsiElement(entry) { 17 | // 18 | // override fun invoke(project: Project, psiFile: PsiFile, startElement: PsiElement, endElement: PsiElement) { 19 | // if (startElement is CodeownersEntry) { 20 | // val document = PsiDocumentManager.getInstance(project).getDocument(psiFile) 21 | // if (document != null) { 22 | // val start = startElement.getStartOffsetInParent() 23 | // val text = startElement.getText() 24 | // val fixed = getFixedPath(text) 25 | // document.replaceString(start, start + text.length, fixed) 26 | // } 27 | // } 28 | // } 29 | // 30 | // private fun getFixedPath(path: String) = path 31 | // .run { replace("/".toRegex(), "/").replace("\\\\\\.".toRegex(), ".") } 32 | // .run { 33 | // try { 34 | // URI(path).normalize().path 35 | // } catch (e: URISyntaxException) { 36 | // e.printStackTrace() 37 | // this 38 | // } 39 | // } 40 | // .run { replace("/\\.{1,2}/".toRegex(), "/").replace("^\\.{0,2}/".toRegex(), "") } 41 | // 42 | // override fun getText(): String = CodeownersBundle.message("quick.fix.relative.entry") 43 | // 44 | // override fun getFamilyName(): String = CodeownersBundle.message("codeInspection.group") 45 | //} 46 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/codeInspection/CodeownersRelativeEntryInspection.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.codeInspection 2 | 3 | //import com.github.fantom.codeowners.CodeownersBundle 4 | //import com.github.fantom.codeowners.lang.CodeownersFile 5 | //import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersEntry 6 | //import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersVisitor 7 | //import com.intellij.codeInspection.InspectionManager 8 | //import com.intellij.codeInspection.LocalInspectionTool 9 | //import com.intellij.codeInspection.ProblemDescriptor 10 | //import com.intellij.codeInspection.ProblemsHolder 11 | //import com.intellij.psi.PsiFile 12 | // 13 | ///** 14 | // * Inspection tool that checks if entry is relative. 15 | // */ 16 | //class CodeownersRelativeEntryInspection : LocalInspectionTool() { 17 | // 18 | // override fun checkFile(file: PsiFile, manager: InspectionManager, isOnTheFly: Boolean): Array? { 19 | // if (file !is CodeownersFile) { 20 | // return null 21 | // } 22 | // 23 | // val problemsHolder = ProblemsHolder(manager, file, isOnTheFly) 24 | // file.acceptChildren( 25 | // object : CodeownersVisitor() { 26 | // override fun visitEntry(entry: CodeownersEntry) { 27 | // val path = entry.text.replace("\\\\(.)".toRegex(), "$1") 28 | // if (path.contains("./")) { 29 | // problemsHolder.registerProblem( 30 | // entry, 31 | // CodeownersBundle.message("codeInspection.relativeEntry.message"), 32 | // CodeownersRelativeEntryFix(entry) 33 | // ) 34 | // } 35 | // super.visitEntry(entry) 36 | // } 37 | // } 38 | // ) 39 | // return problemsHolder.resultsArray 40 | // } 41 | // 42 | // override fun runForWholeFile() = true 43 | //} 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/codeInspection/CodeownersRemoveRuleFix.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.codeInspection 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.file.type.CodeownersFileType 5 | import com.github.fantom.codeowners.lang.CodeownersRuleBase 6 | import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement 7 | import com.intellij.openapi.editor.Editor 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.psi.PsiElement 10 | import com.intellij.psi.PsiFile 11 | import com.intellij.psi.impl.source.tree.TreeUtil 12 | import com.intellij.psi.tree.IElementType 13 | 14 | /** 15 | * QuickFix action that removes specified entry handled by code inspections like [CodeownersCoverPatternInspection], 16 | * [CodeownersUnusedPatternInspection]. 17 | */ 18 | class CodeownersRemoveRuleFix(rule: CodeownersRuleBase<*, *>) : LocalQuickFixAndIntentionActionOnPsiElement(rule) { 19 | 20 | override fun invoke( 21 | project: Project, 22 | file: PsiFile, 23 | editor: Editor?, 24 | startElement: PsiElement, 25 | endElement: PsiElement 26 | ) { 27 | val crlfToken = (file.fileType as CodeownersFileType).codeownersLanguage.getCrlfToken() 28 | if (startElement is CodeownersRuleBase<*, *>) { 29 | removeCrlf(startElement, crlfToken) 30 | startElement.delete() 31 | } 32 | } 33 | 34 | private fun removeCrlf(startElement: PsiElement, crlfToken: IElementType) { 35 | ( 36 | TreeUtil.findSibling( 37 | startElement.node, 38 | crlfToken 39 | ) 40 | ?: TreeUtil.findSiblingBackward( 41 | startElement.node, 42 | crlfToken 43 | ) 44 | )?.psi?.delete() 45 | } 46 | 47 | override fun getText(): String = CodeownersBundle.message("quick.fix.remove.rule") 48 | 49 | override fun getFamilyName(): String = CodeownersBundle.message("codeInspection.group") 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/codeInspection/CodeownersUnusedPatternInspection.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.codeInspection 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.file.type.CodeownersFileType 5 | import com.github.fantom.codeowners.lang.CodeownersRuleBase 6 | import com.github.fantom.codeowners.lang.CodeownersVisitor 7 | import com.intellij.codeInspection.LocalInspectionTool 8 | import com.intellij.codeInspection.ProblemHighlightType 9 | import com.intellij.codeInspection.ProblemsHolder 10 | import com.intellij.psi.PsiElementVisitor 11 | import com.intellij.psi.PsiPolyVariantReference 12 | import com.intellij.psi.impl.source.resolve.reference.impl.providers.FileReferenceOwner 13 | 14 | /** 15 | * Inspection tool that checks if entries are unused - does not cover any file or directory. 16 | */ 17 | class CodeownersUnusedPatternInspection : LocalInspectionTool() { 18 | 19 | /** 20 | * Checks if entries are related to any file. 21 | * 22 | * @param holder where visitor will register problems found. 23 | * @param isOnTheFly true if inspection was run in non-batch mode 24 | * @return not-null visitor for this inspection 25 | */ 26 | override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { 27 | val visitor = object : CodeownersVisitor() { 28 | override fun visitRule(rule: CodeownersRuleBase<*, *>) { 29 | val pattern = rule.pattern 30 | val lastReference = pattern.references.lastOrNull { 31 | it is FileReferenceOwner 32 | } ?: return 33 | val hasNoTargets = (lastReference as PsiPolyVariantReference).multiResolve(false).isEmpty() 34 | if (hasNoTargets) { 35 | holder.registerProblem( 36 | pattern, 37 | CodeownersBundle.message("codeInspection.unusedPattern.message"), 38 | ProblemHighlightType.LIKE_UNUSED_SYMBOL, 39 | CodeownersRemoveRuleFix(rule) 40 | ) 41 | } 42 | } 43 | } 44 | 45 | return (holder.file.fileType as CodeownersFileType).codeownersLanguage.getVisitor(visitor)!! 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/commenting/CodeownersCommenter.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.commenting 2 | 3 | import com.intellij.lang.Commenter 4 | class CodeownersCommenter : Commenter { 5 | override fun getLineCommentPrefix() = "#" 6 | 7 | override fun getBlockCommentPrefix() = null 8 | 9 | override fun getBlockCommentSuffix() = null 10 | 11 | override fun getCommentedBlockCommentPrefix() = null 12 | 13 | override fun getCommentedBlockCommentSuffix() = null 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/daemon/CodeownersDirectoryMarkerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.daemon 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.lang.CodeownersPatternBase 5 | import com.github.fantom.codeowners.services.CodeownersMatcher 6 | import com.github.fantom.codeowners.util.Glob 7 | import com.github.fantom.codeowners.util.Utils 8 | import com.intellij.codeInsight.daemon.LineMarkerInfo 9 | import com.intellij.codeInsight.daemon.LineMarkerProvider 10 | import com.intellij.openapi.components.service 11 | import com.intellij.openapi.editor.markup.GutterIconRenderer 12 | import com.intellij.psi.PsiElement 13 | import com.intellij.psi.util.nextLeaf 14 | import com.intellij.util.PlatformIcons 15 | 16 | /** 17 | * [LineMarkerProvider] that marks entry lines with directory icon if they point to the directory in virtual system. 18 | */ 19 | class CodeownersDirectoryMarkerProvider : LineMarkerProvider { 20 | 21 | private val cache = mutableMapOf() 22 | 23 | /** 24 | * Returns [LineMarkerInfo] with set [PlatformIcons.FOLDER_ICON] if entry points to the directory. 25 | * 26 | * @param element current element 27 | * @return `null` if entry is not a directory 28 | */ 29 | @Suppress("ReturnCount") 30 | override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { 31 | if (element !is CodeownersPatternBase) { 32 | return null 33 | } 34 | var isDirectory = element.isDirectory 35 | 36 | if (!isDirectory) { 37 | val key = element.getText() 38 | if (cache.containsKey(key)) { 39 | isDirectory = cache[key] ?: false 40 | } else { 41 | val parent = element.getContainingFile().virtualFile.parent ?: return null 42 | val project = element.getProject() 43 | Utils.getModuleForFile(parent, project) ?: return null 44 | 45 | val matcher = service() 46 | val file = Glob.findOne(parent, element, matcher) 47 | cache[key] = file != null && file.isDirectory.also { isDirectory = it } 48 | } 49 | } 50 | 51 | return if (isDirectory) LineMarkerInfo( 52 | element.nextLeaf() ?: element, 53 | element.getTextRange(), 54 | PlatformIcons.FOLDER_ICON, 55 | null, 56 | null, 57 | GutterIconRenderer.Alignment.CENTER, 58 | CodeownersBundle.messagePointer("daemon.lineMarker.directory") 59 | ) 60 | else null 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/file/type/CodeownersFileType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.file.type 2 | 3 | import com.github.fantom.codeowners.CodeownersIcons 4 | import com.github.fantom.codeowners.lang.CodeownersLanguage 5 | import com.intellij.openapi.fileTypes.LanguageFileType 6 | import com.intellij.openapi.vcs.VcsRoot 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import org.jetbrains.annotations.NonNls 9 | 10 | open class CodeownersFileType protected constructor( 11 | val codeownersLanguage: CodeownersLanguage = CodeownersLanguage.INSTANCE 12 | ) : LanguageFileType(codeownersLanguage) { 13 | 14 | companion object { 15 | val INSTANCE = CodeownersFileType() 16 | } 17 | 18 | /** 19 | * Return directory we should treat as prefix for all the entries in given [codeownersFile] 20 | * relatively to given [vcsRoot]. 21 | * @return null if paths in given [codeownersFile] cannot be resolved relatively to given [vcsRoot], i.e. 22 | * provided vcsRoot doesn't contain this CODEONWERS file, 23 | * or if file is not in one of allowed places under repository root 24 | */ 25 | open fun getRoot(vcsRoot: VcsRoot, codeownersFile: VirtualFile): VirtualFile? { 26 | return if (vcsRoot.path == codeownersFile.parent) vcsRoot.path else null 27 | } 28 | 29 | /** 30 | * @return directory we should treat as prefix for all the entries in given [codeownersFile] 31 | * This method is imprecise and should be avoided if possible, consider using [getRoot] that takes [VcsRoot] 32 | */ 33 | open fun getRoot(codeownersFile: VirtualFile): VirtualFile { 34 | return codeownersFile.parent 35 | } 36 | 37 | @NonNls 38 | override fun getName() = "${codeownersLanguage.id} File" 39 | 40 | val languageName 41 | get() = codeownersLanguage.id 42 | 43 | override fun getDescription() = codeownersLanguage.displayName // "Codeowners file" 44 | 45 | override fun getDefaultExtension() = "CODEOWNERS" 46 | 47 | override fun getIcon() = CodeownersIcons.FILE 48 | 49 | override fun equals(other: Any?) = other is CodeownersFileType && languageName == other.languageName 50 | 51 | override fun hashCode() = codeownersLanguage.id.hashCode() 52 | 53 | override fun toString() = name 54 | } 55 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/file/type/kind/BitbucketFileType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.file.type.kind 2 | 3 | import com.github.fantom.codeowners.file.type.CodeownersFileType 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.BitbucketLanguage 5 | import com.intellij.openapi.vcs.VcsRoot 6 | import com.intellij.openapi.vfs.VirtualFile 7 | 8 | object BitbucketFileType : CodeownersFileType(BitbucketLanguage.INSTANCE) { 9 | private val subdirectories = setOf(".bitbucket") 10 | 11 | // TODO need to investigate why parent may be null 12 | private fun isInSubdir(codeownersFile: VirtualFile) = codeownersFile.parent?.let { it.name in subdirectories } ?: false 13 | 14 | override fun getRoot(vcsRoot: VcsRoot, codeownersFile: VirtualFile): VirtualFile? { 15 | return super.getRoot(vcsRoot, codeownersFile) ?: codeownersFile.parent?.let { codeownersParentDir -> 16 | if ( 17 | codeownersParentDir.parent == vcsRoot.path && // 18 | codeownersParentDir.name in subdirectories 19 | ) { 20 | vcsRoot.path 21 | } else { 22 | null 23 | // find root CODEOWNERS file 24 | // vcsRoot.findFileByRelativePath(filename)?.let { 25 | // PsiManager.getInstance(codeownersFile.project).findFile(it) 26 | // }?.let { 27 | // (it as? BitbucketFile) 28 | // }?.let { rootCodeownersFile -> 29 | // if (rootCodeownersFile.isSubdirectoryOverridesEnabled) { 30 | // codeownersParentDir 31 | // } else { 32 | // throw CodeownersException("Subdirectory overrides are not enabled") 33 | // } 34 | // } ?: throw CodeownersException("No Bitbucket CODEOWNERS file in the root of repository") 35 | } 36 | } 37 | } 38 | 39 | override fun getRoot(codeownersFile: VirtualFile): VirtualFile { 40 | return if (isInSubdir(codeownersFile)) codeownersFile.parent.parent else super.getRoot(codeownersFile) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/file/type/kind/GithubFileType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.file.type.kind 2 | 3 | import com.github.fantom.codeowners.file.type.CodeownersFileType 4 | import com.github.fantom.codeowners.lang.kind.github.GithubLanguage 5 | import com.intellij.openapi.vcs.VcsRoot 6 | import com.intellij.openapi.vfs.VirtualFile 7 | 8 | object GithubFileType : CodeownersFileType(GithubLanguage.INSTANCE) { 9 | private val subdirectories = setOf(".github", "docs") 10 | 11 | // TODO need to investigate why parent may be null 12 | private fun isInSubdir(codeownersFile: VirtualFile) = codeownersFile.parent?.let { it.name in subdirectories } ?: false 13 | 14 | override fun getRoot(vcsRoot: VcsRoot, codeownersFile: VirtualFile): VirtualFile? { 15 | return super.getRoot(vcsRoot, codeownersFile) // CODEOWNERS file is allowed in repo root 16 | ?: codeownersFile.parent?.run { 17 | name in subdirectories && // and in dirs with given name 18 | parent == vcsRoot.path // only if they are in the root 19 | }?.let { vcsRoot.path } 20 | } 21 | 22 | override fun getRoot(codeownersFile: VirtualFile): VirtualFile { 23 | return if (isInSubdir(codeownersFile)) codeownersFile.parent.parent else super.getRoot(codeownersFile) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/grouping/changes/MovedVirtualFile.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.grouping.changes 2 | 3 | import com.intellij.openapi.vcs.FilePath 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import com.intellij.openapi.vfs.VirtualFileSystem 6 | import com.intellij.testFramework.LightVirtualFile 7 | 8 | /** 9 | * A [VirtualFile] implementation that represents moved/renamed file 10 | * Minimal required set of properties is overridden 11 | * 12 | * @param path previous path of the file (moved from) 13 | * @param currentFile moved/renamed file itself, in its current state 14 | */ 15 | class MovedVirtualFile( 16 | private val path: FilePath, 17 | private val currentFile: VirtualFile, 18 | ) : LightVirtualFile(path.name, path.fileType, "", path.charset, 0) { 19 | init { 20 | isWritable = false 21 | } 22 | 23 | override fun getParent(): VirtualFile? { 24 | return path.virtualFileParent ?: path.parentPath?.let{ MovedVirtualDir(it, fileSystem) } 25 | } 26 | 27 | override fun getPath() = path.path 28 | 29 | /** File system must be the same as of the [currentFile]'s */ 30 | override fun getFileSystem() = currentFile.fileSystem 31 | } 32 | 33 | /** 34 | * A [VirtualFile] implementation that represents moved/renamed directory 35 | * Minimal required set of properties is overridden 36 | * This class must be instantiated only from itself and from [MovedVirtualFile] 37 | * 38 | * @param path previous path of the directory (moved from) 39 | * @param vfs file system of the files, moved otgether with this directory 40 | */ 41 | private class MovedVirtualDir( 42 | private val path: FilePath, 43 | private val vfs: VirtualFileSystem, 44 | ): LightVirtualFile(path.name) { 45 | init { 46 | isWritable = false 47 | } 48 | 49 | override fun getParent(): VirtualFile? { 50 | return path.virtualFileParent ?: path.parentPath?.let{ MovedVirtualDir(it, vfs) } 51 | } 52 | 53 | override fun getPath() = path.path 54 | 55 | override fun getFileSystem() = vfs 56 | 57 | override fun isDirectory() = true 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/grouping/changes/SetCodeownersChangesGroupingAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.grouping.changes 2 | 3 | import com.github.fantom.codeowners.CodeownersManager 4 | import com.intellij.openapi.actionSystem.ActionUpdateThread 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.components.service 7 | import com.intellij.openapi.vcs.changes.actions.SetChangesGroupingAction 8 | 9 | class SetCodeownersChangesGroupingAction : SetChangesGroupingAction() { 10 | override val groupingKey: String get() = "codeowners" 11 | 12 | override fun getActionUpdateThread() = ActionUpdateThread.BGT 13 | 14 | override fun update(e: AnActionEvent) { 15 | super.update(e) 16 | val manager = e.project?.service() 17 | e.presentation.isEnabledAndVisible = e.presentation.isEnabledAndVisible && manager?.isAvailable ?: false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/grouping/usage/CodeownersGroupingRule.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.grouping.usage 2 | 3 | import com.github.fantom.codeowners.CodeownersIcons 4 | import com.github.fantom.codeowners.CodeownersManager 5 | import com.github.fantom.codeowners.OwnersReference 6 | import com.intellij.injected.editor.VirtualFileWindow 7 | import com.intellij.openapi.components.service 8 | import com.intellij.openapi.project.DumbAware 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.usages.Usage 12 | import com.intellij.usages.UsageGroup 13 | import com.intellij.usages.UsageTarget 14 | import com.intellij.usages.impl.rules.UsageGroupBase 15 | import com.intellij.usages.impl.rules.UsageGroupingRulesDefaultRanks 16 | import com.intellij.usages.rules.SingleParentUsageGroupingRule 17 | import com.intellij.usages.rules.UsageGroupingRuleEx 18 | import com.intellij.usages.rules.UsageInFile 19 | import javax.swing.Icon 20 | 21 | class CodeownersGroupingRule(project: Project) : 22 | SingleParentUsageGroupingRule(), 23 | DumbAware, 24 | UsageGroupingRuleEx { 25 | private val codeownersManager = project.service() 26 | override fun getParentGroupFor(usage: Usage, targets: Array): UsageGroup? { 27 | return (usage as? UsageInFile)?.file 28 | ?.let { (it as? VirtualFileWindow)?.delegate ?: it } 29 | ?.let(::getGroupForFile) 30 | } 31 | 32 | private fun getGroupForFile(virtualFile: VirtualFile): UsageGroup { 33 | return codeownersManager.getFileOwners(virtualFile) 34 | // TODO handle error properly 35 | .getOrNull() 36 | ?.values 37 | ?.firstOrNull() 38 | ?.ref 39 | .let(::CodeownersGroup) 40 | } 41 | 42 | override fun getGroupingActionId(): String { 43 | return "UsageGrouping.Codeowner" 44 | } 45 | 46 | private class CodeownersGroup(private val ownersRef: OwnersReference?) : UsageGroupBase(1) { 47 | override fun getIcon(): Icon { 48 | return CodeownersIcons.FILE 49 | } 50 | 51 | override fun getPresentableGroupText(): String { 52 | val owners = ownersRef?.owners 53 | return when(owners?.isEmpty()) { 54 | null, true -> { 55 | "" 56 | } 57 | else -> { 58 | owners.joinToString(", ") 59 | } 60 | } 61 | } 62 | 63 | override fun equals(other: Any?): Boolean { 64 | if (this === other) return true 65 | if (javaClass != other?.javaClass) return false 66 | 67 | other as CodeownersGroup 68 | 69 | return when { 70 | ownersRef == null && other.ownersRef == null -> true 71 | ownersRef == null || other.ownersRef == null -> false 72 | else -> ownersRef.equalsIgnoringOffset(other.ownersRef) 73 | } 74 | } 75 | 76 | override fun hashCode(): Int { 77 | return ownersRef?.hashCodeIgnoringOffset() ?: ownersRef.hashCode() 78 | } 79 | } 80 | 81 | override fun getRank(): Int { 82 | return UsageGroupingRulesDefaultRanks.BEFORE_MODULE.absoluteRank 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/grouping/usage/CodeownersUsageGroupingRuleProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.grouping.usage 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.usages.UsageView 6 | import com.intellij.usages.UsageViewPresentation 7 | import com.intellij.usages.UsageViewSettings 8 | import com.intellij.usages.rules.UsageGroupingRule 9 | import com.intellij.usages.rules.UsageGroupingRuleProviderEx 10 | 11 | // TODO handle absence of CODEOWNERS file, so no need to group by owners 12 | class CodeownersUsageGroupingRuleProvider : UsageGroupingRuleProviderEx { 13 | override fun getActiveRules( 14 | project: Project, 15 | usageViewSettings: UsageViewSettings, 16 | presentation: UsageViewPresentation? 17 | ): Array { 18 | return if (CodeownersUsageViewSettings.instance.isGroupByCodeowner) { 19 | arrayOf(CodeownersGroupingRule(project)) 20 | } else { 21 | UsageGroupingRule.EMPTY_ARRAY 22 | } 23 | } 24 | 25 | override fun createGroupingActions(view: UsageView): Array { 26 | return arrayOf(GroupByCodeownerAction()) 27 | } 28 | 29 | override fun getAllRules(project: Project, usageView: UsageView?): Array { 30 | return arrayOf(CodeownersGroupingRule(project)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/grouping/usage/CodeownersUsageViewSettings.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.grouping.usage 2 | 3 | import com.intellij.openapi.application.ApplicationManager 4 | import com.intellij.openapi.components.BaseState 5 | import com.intellij.openapi.components.PersistentStateComponent 6 | import com.intellij.openapi.components.State 7 | import com.intellij.openapi.components.Storage 8 | import com.intellij.util.xmlb.annotations.OptionTag 9 | 10 | /** 11 | * Inspired by [com.intellij.usages.UsageViewSettings] 12 | */ 13 | @State(name = "CodeownersUsageViewSettings", storages = [Storage("usageView.xml")]) 14 | class CodeownersUsageViewSettings( 15 | isGroupByCodeowner: Boolean = false 16 | ) : BaseState(), PersistentStateComponent { 17 | companion object { 18 | @JvmStatic 19 | val instance: CodeownersUsageViewSettings 20 | get() = ApplicationManager.getApplication().getService(CodeownersUsageViewSettings::class.java) 21 | } 22 | 23 | @get:OptionTag("GROUP_BY_CODEOWNER") 24 | var isGroupByCodeowner by property(isGroupByCodeowner) 25 | 26 | override fun getState() = this 27 | 28 | override fun loadState(state: CodeownersUsageViewSettings) { 29 | copyFrom(state) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/grouping/usage/GroupByCodeownerAction.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.grouping.usage 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.CodeownersIcons 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.usages.impl.actions.RuleAction 7 | 8 | class GroupByCodeownerAction : 9 | RuleAction(CodeownersBundle.messagePointer("action.group.by.codeowner"), CodeownersIcons.FILE) { 10 | override fun getOptionValue(e: AnActionEvent): Boolean { 11 | return CodeownersUsageViewSettings.instance.isGroupByCodeowner 12 | } 13 | 14 | override fun setOptionValue(e: AnActionEvent, value: Boolean) { 15 | CodeownersUsageViewSettings.instance.isGroupByCodeowner = value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/highlighter/CodeownersColorSettingsPage.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.highlighter 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.highlighter.CodeownersHighlighter 5 | import com.github.fantom.codeowners.util.Resources 6 | import com.intellij.openapi.editor.colors.TextAttributesKey 7 | import com.intellij.openapi.options.colors.AttributesDescriptor 8 | import com.intellij.openapi.options.colors.ColorDescriptor 9 | import com.intellij.openapi.options.colors.ColorSettingsPage 10 | import org.jetbrains.annotations.NonNls 11 | import javax.swing.Icon 12 | 13 | /** 14 | * [ColorSettingsPage] that allows to modify color scheme. 15 | */ 16 | class CodeownersColorSettingsPage : ColorSettingsPage { 17 | 18 | companion object { 19 | @NonNls private val SAMPLE_CODEOWNERS_PATH = "/sample.codeowners" 20 | @NonNls private val DISPLAY_NAME = CodeownersBundle.message("codeowners.colorSettings.displayName") 21 | private val SAMPLE_CODEOWNERS = loadSampleCodeowners() 22 | private val DESCRIPTORS = arrayOf( 23 | AttributesDescriptor(CodeownersBundle.message("highlighter.header"), CodeownersHighlighterColors.HEADER), 24 | AttributesDescriptor(CodeownersBundle.message("highlighter.section"), CodeownersHighlighterColors.SECTION), 25 | AttributesDescriptor(CodeownersBundle.message("highlighter.comment"), CodeownersHighlighterColors.COMMENT), 26 | AttributesDescriptor(CodeownersBundle.message("highlighter.negation"), CodeownersHighlighterColors.NEGATION), 27 | // AttributesDescriptor(CodeownersBundle.message("highlighter.brackets"), CodeownersHighlighterColors.BRACKET), 28 | AttributesDescriptor(CodeownersBundle.message("highlighter.slash"), CodeownersHighlighterColors.SLASH), 29 | AttributesDescriptor(CodeownersBundle.message("highlighter.value"), CodeownersHighlighterColors.VALUE), 30 | AttributesDescriptor(CodeownersBundle.message("highlighter.configName"), CodeownersHighlighterColors.CONFIG_NAME), 31 | AttributesDescriptor(CodeownersBundle.message("highlighter.configValue"), CodeownersHighlighterColors.CONFIG_VALUE), 32 | AttributesDescriptor(CodeownersBundle.message("highlighter.unused"), CodeownersHighlighterColors.UNUSED), 33 | ) 34 | 35 | /** 36 | * Loads sample CODEOWNERS file 37 | * 38 | * @return the text loaded from [.SAMPLE_CODEOWNERS_PATH] 39 | * 40 | * @see .getDemoText 41 | * @see .SAMPLE_CODEOWNERS_PATH 42 | * @see .SAMPLE_CODEOWNERS 43 | */ 44 | private fun loadSampleCodeowners() = Resources.getResourceContent(SAMPLE_CODEOWNERS_PATH) ?: "" 45 | } 46 | 47 | override fun getIcon(): Icon? = null 48 | 49 | // we use bitbucket file type because it is more feature-rich 50 | override fun getHighlighter() = CodeownersHighlighter() 51 | 52 | override fun getDemoText() = SAMPLE_CODEOWNERS 53 | 54 | override fun getAdditionalHighlightingTagToDescriptorMap(): Map? = null 55 | 56 | override fun getAttributeDescriptors() = DESCRIPTORS 57 | 58 | override fun getColorDescriptors(): Array = ColorDescriptor.EMPTY_ARRAY 59 | 60 | override fun getDisplayName(): String = DISPLAY_NAME 61 | } 62 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/highlighter/CodeownersHighlighterColors.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.highlighter 2 | 3 | import com.intellij.openapi.editor.colors.TextAttributesKey 4 | 5 | /** 6 | * Contains highlighter attributes definitions. 7 | */ 8 | object CodeownersHighlighterColors { 9 | /** Default style for regular comment started with # */ 10 | val COMMENT = TextAttributesKey.createTextAttributesKey("CODEOWNERS.COMMENT") 11 | 12 | /** Default style for section comment started with ## */ 13 | val SECTION = TextAttributesKey.createTextAttributesKey("CODEOWNERS.SECTION") 14 | 15 | /** Default style for header comment started with ### */ 16 | val HEADER = TextAttributesKey.createTextAttributesKey("CODEOWNERS.HEADER") 17 | 18 | /** Default style for negation element - ! in the beginning of the entry */ 19 | val NEGATION = TextAttributesKey.createTextAttributesKey("CODEOWNERS.NEGATION") 20 | 21 | /** Default style for bracket element - [] in the entry */ 22 | // val BRACKET = TextAttributesKey.createTextAttributesKey("CODEOWNERS.BRACKET") 23 | 24 | /** Default style for slash - / in the entry */ 25 | val SLASH = TextAttributesKey.createTextAttributesKey("CODEOWNERS.SLASH") 26 | 27 | /** Default style for value element - part of entry */ 28 | val VALUE = TextAttributesKey.createTextAttributesKey("CODEOWNERS.VALUE") 29 | 30 | /** Default style for value element - part of entry */ 31 | val NAME = TextAttributesKey.createTextAttributesKey("CODEOWNERS.NAME") 32 | 33 | val CONFIG_NAME = TextAttributesKey.createTextAttributesKey("CODEOWNERS.CONFIG_NAME") 34 | val CONFIG_VALUE = TextAttributesKey.createTextAttributesKey("CODEOWNERS.CONFIG_VALUE") 35 | 36 | /** Default style for unused entry */ 37 | val UNUSED = TextAttributesKey.createTextAttributesKey("CODEOWNERS.UNUSED_PATTERN") 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/indexing/AbstractCodeownersFilesIndex.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.indexing 2 | 3 | import com.intellij.openapi.project.DumbAware 4 | import com.intellij.util.indexing.DataIndexer 5 | import com.intellij.util.indexing.FileBasedIndex.InputFilter 6 | import com.intellij.util.indexing.FileBasedIndexExtension 7 | import com.intellij.util.indexing.FileContent 8 | import com.intellij.util.io.KeyDescriptor 9 | 10 | /** 11 | * Abstract class of [FileBasedIndexExtension] that contains base configuration for [CodeownersFilesIndex]. 12 | */ 13 | abstract class AbstractCodeownersFilesIndex : 14 | FileBasedIndexExtension(), 15 | KeyDescriptor, 16 | DataIndexer, 17 | InputFilter, 18 | DumbAware { 19 | 20 | override fun getIndexer() = this 21 | 22 | override fun getKeyDescriptor() = this 23 | 24 | override fun isEqual(val1: K, val2: K) = val1 == val2 25 | 26 | override fun getHashCode(value: K): Int = value.hashCode() 27 | 28 | override fun dependsOnFileContent() = true 29 | 30 | override fun getInputFilter() = this 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/indexing/CodeownersEntryOccurrence.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.indexing 2 | 3 | import com.github.fantom.codeowners.OwnersReference 4 | import com.intellij.openapi.vfs.VirtualFile 5 | import com.intellij.openapi.vfs.VirtualFileManager 6 | import java.io.DataInput 7 | import java.io.DataOutput 8 | import java.io.IOException 9 | import java.io.Serializable 10 | import java.util.* 11 | 12 | @JvmInline 13 | value class RegexString(val regex: String) { 14 | override fun toString() = regex 15 | } 16 | 17 | @JvmInline 18 | value class OwnerString(val owner: String): Comparable { 19 | override fun compareTo(other: OwnerString): Int { 20 | return owner.compareTo(other.owner) 21 | } 22 | 23 | override fun toString() = owner 24 | } 25 | 26 | /** 27 | * Entry containing information about the [VirtualFile] instance of the codeowners file mapped with the collection 28 | * of codeowners entries with line numbers for better performance. Class is used for indexing. 29 | */ 30 | @Suppress("SerialVersionUIDInSerializableClass") 31 | class CodeownersEntryOccurrence(val url: String, val items: List>) : Serializable { 32 | 33 | /** 34 | * Returns current [VirtualFile]. 35 | * 36 | * @return current file 37 | */ 38 | var file: VirtualFile? = null 39 | get() { 40 | if (field == null && url.isNotEmpty()) { 41 | field = VirtualFileManager.getInstance().findFileByUrl(url) 42 | } 43 | return field 44 | } 45 | private set 46 | 47 | companion object { 48 | @Synchronized 49 | @Throws(IOException::class) 50 | fun serialize(out: DataOutput, entry: CodeownersEntryOccurrence) { 51 | out.run { 52 | writeUTF(entry.url) 53 | writeInt(entry.items.size) 54 | entry.items.forEach { 55 | writeUTF(it.first.regex) 56 | writeInt(it.second.offset) 57 | writeInt(it.second.owners.size) 58 | it.second.owners.forEach { owner -> 59 | writeUTF(owner.owner) 60 | } 61 | } 62 | } 63 | } 64 | 65 | @Synchronized 66 | @Throws(IOException::class) 67 | fun deserialize(input: DataInput): CodeownersEntryOccurrence { 68 | val url = input.readUTF() 69 | val items = mutableListOf>() 70 | 71 | if (url.isNotEmpty()) { 72 | val size = input.readInt() 73 | repeat((0 until size).count()) { 74 | val pattern = RegexString(input.readUTF()) 75 | val offset = input.readInt() 76 | val size = input.readInt() 77 | val owners = mutableListOf() 78 | repeat((0 until size).count()) { 79 | owners.add(OwnerString(input.readUTF())) 80 | } 81 | items.add(Pair(pattern, OwnersReference(owners, offset))) 82 | } 83 | } 84 | return CodeownersEntryOccurrence(url, items) 85 | } 86 | } 87 | 88 | override fun hashCode() = Objects.hash(url, items) 89 | 90 | override fun equals(other: Any?) = when { 91 | other !is CodeownersEntryOccurrence -> false 92 | url != other.url || items.size != other.items.size -> false 93 | else -> items.indices.find { items[it].toString() != other.items[it].toString() } == null 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/indexing/CodeownersFilesIndex.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.indexing 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.file.type.CodeownersFileType 5 | import com.github.fantom.codeowners.lang.CodeownersFile 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.diagnostic.Logger 8 | import com.intellij.openapi.project.DumbAware 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.util.indexing.* 12 | import com.intellij.util.indexing.FileBasedIndex.InputFilter 13 | import com.intellij.util.io.DataExternalizer 14 | import com.intellij.util.io.KeyDescriptor 15 | import java.io.DataInput 16 | import java.io.DataOutput 17 | import java.io.IOException 18 | import java.util.* 19 | 20 | /** 21 | * Implementation of [AbstractCodeownersFilesIndex] that allows to index all codeowners files content using native 22 | * IDE mechanisms and increase indexing performance. 23 | */ 24 | @Suppress("TooManyFunctions") 25 | class CodeownersFilesIndex : 26 | FileBasedIndexExtension(), 27 | KeyDescriptor, 28 | DataIndexer, 29 | InputFilter, 30 | DumbAware { 31 | override fun getIndexer() = this 32 | 33 | override fun getKeyDescriptor() = this 34 | 35 | override fun isEqual(val1: CodeownersFileType, val2: CodeownersFileType) = val1 == val2 36 | 37 | override fun getHashCode(value: CodeownersFileType): Int = value.hashCode() 38 | 39 | override fun dependsOnFileContent() = true 40 | 41 | override fun getInputFilter() = this 42 | 43 | companion object { 44 | private val LOGGER = Logger.getInstance(CodeownersFilesIndex::class.java) 45 | val KEY = ID.create("CodeownersFilesIndex") 46 | private const val VERSION = 2 47 | private val DATA_EXTERNALIZER = object : DataExternalizer { 48 | 49 | @Throws(IOException::class) 50 | override fun save(out: DataOutput, entry: CodeownersEntryOccurrence) = CodeownersEntryOccurrence.serialize(out, entry) 51 | 52 | @Throws(IOException::class) 53 | override fun read(input: DataInput) = CodeownersEntryOccurrence.deserialize(input) 54 | } 55 | 56 | /** 57 | * Returns collection of indexed [CodeownersEntryOccurrence] for given [Project] and [CodeownersFileType]. 58 | * 59 | * @param project current project 60 | * @param fileType filetype 61 | * @return [CodeownersEntryOccurrence] collection 62 | */ 63 | fun getEntries(project: Project, fileType: CodeownersFileType): List { 64 | LOGGER.trace(">getEntries for file type $fileType in project ${project.name}") 65 | // try { 66 | if (ApplicationManager.getApplication().isReadAccessAllowed) { 67 | val scope = CodeownersSearchScope[project] 68 | val res = FileBasedIndex.getInstance().getValues(KEY, fileType, scope) 69 | LOGGER.trace(" = KEY 80 | 81 | @Suppress("ReturnCount") 82 | override fun map(inputData: FileContent): Map { 83 | val inputDataPsi = try { 84 | inputData.psiFile as? CodeownersFile 85 | } catch (e: Exception) { 86 | // if there is some stale indices 87 | // inputData.getPsiFile() could throw exception that should be avoided 88 | return emptyMap() 89 | } ?: return emptyMap() 90 | 91 | // val items = mutableListOf>>() 92 | // inputDataPsi.acceptChildren( 93 | // object : com.github.fantom.codeowners.lang.kind.github.psi.CodeownersVisitor() { 94 | // override fun visitPattern(entry: com.github.fantom.codeowners.lang.kind.github.psi.CodeownersPattern) { 95 | // val regex = entry.entryFile.regex(false) 96 | // items.add(Pair(RegexString(regex), entry.owners.ownerList.map{ OwnerString(it.text) })) 97 | // } 98 | // } 99 | // ) 100 | 101 | val codeownersEntryOccurrence = CodeownersEntryOccurrence(inputData.file.url, inputDataPsi.getRulesList()) 102 | 103 | return mapOf( 104 | CodeownersFileType.INSTANCE to codeownersEntryOccurrence, 105 | (inputData.fileType as CodeownersFileType) to codeownersEntryOccurrence, 106 | ) 107 | } 108 | 109 | @Synchronized 110 | @Throws(IOException::class) 111 | override fun save(out: DataOutput, value: CodeownersFileType) { 112 | out.writeUTF(value.language.id) 113 | } 114 | 115 | @Synchronized 116 | @Throws(IOException::class) 117 | override fun read(input: DataInput): CodeownersFileType = // CodeownersFileType.INSTANCE 118 | input.readUTF().run { 119 | CodeownersBundle.LANGUAGES 120 | .asSequence() 121 | .map { it.fileType } 122 | .firstOrNull { it.languageName == this } 123 | .let { it ?: CodeownersFileType.INSTANCE } 124 | } 125 | 126 | override fun getValueExternalizer() = DATA_EXTERNALIZER 127 | 128 | override fun getVersion() = VERSION 129 | 130 | override fun acceptInput(file: VirtualFile) = 131 | file.fileType is CodeownersFileType // || CodeownersManager.FILE_TYPES_ASSOCIATION_QUEUE.containsKey(file.name) 132 | } 133 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/indexing/CodeownersSearchScope.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.indexing 2 | 3 | import com.github.fantom.codeowners.file.type.CodeownersFileType 4 | import com.intellij.openapi.module.Module 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.vfs.VirtualFile 7 | import com.intellij.psi.search.GlobalSearchScope 8 | import com.intellij.psi.search.SearchScope 9 | 10 | /** 11 | * Provides extended [GlobalSearchScope] with additional ignore files (i.e. outer gitignore files). 12 | */ 13 | class CodeownersSearchScope private constructor(project: Project) : GlobalSearchScope(project) { 14 | 15 | override fun contains(file: VirtualFile) = file.fileType is CodeownersFileType 16 | 17 | override fun isSearchInLibraries() = true 18 | 19 | override fun isForceSearchingInLibrarySources() = true 20 | 21 | override fun isSearchInModuleContent(aModule: Module) = true 22 | 23 | override fun union(scope: SearchScope) = this 24 | 25 | override fun intersectWith(scope2: SearchScope) = scope2 26 | 27 | companion object { 28 | /** 29 | * Returns [GlobalSearchScope.projectScope] instance united with additional files. 30 | * 31 | * @param project current project 32 | * @return extended instance of [GlobalSearchScope] 33 | */ 34 | operator fun get(project: Project) = CodeownersSearchScope(project) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/CodeownersElementImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang 2 | 3 | import com.intellij.extapi.psi.ASTWrapperPsiElement 4 | import com.intellij.lang.ASTNode 5 | import com.intellij.psi.PsiReference 6 | import com.intellij.psi.impl.source.resolve.reference.ReferenceProvidersRegistry 7 | 8 | /** 9 | * Definition of [ASTWrapperPsiElement]. 10 | */ 11 | open class CodeownersElementImpl(node: ASTNode) : ASTWrapperPsiElement(node) { 12 | 13 | override fun getReferences(): Array = ReferenceProvidersRegistry.getReferencesFromProviders(this) 14 | } 15 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/CodeownersElementType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang 2 | 3 | import com.intellij.psi.tree.IElementType 4 | import org.jetbrains.annotations.NonNls 5 | 6 | class CodeownersElementType(@NonNls debugName: String) : IElementType(debugName, CodeownersLanguage.INSTANCE) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/CodeownersFile.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang 2 | 3 | import com.github.fantom.codeowners.CodeownersException 4 | import com.github.fantom.codeowners.OwnersReference 5 | import com.github.fantom.codeowners.file.type.CodeownersFileType 6 | import com.github.fantom.codeowners.indexing.OwnerString 7 | import com.github.fantom.codeowners.indexing.RegexString 8 | import com.intellij.lang.Language 9 | import com.intellij.lang.LanguageParserDefinitions 10 | import com.intellij.psi.FileViewProvider 11 | import com.intellij.psi.PsiElementVisitor 12 | import com.intellij.psi.impl.source.PsiFileImpl 13 | 14 | class CodeownersFile(viewProvider: FileViewProvider, private val fileType: CodeownersFileType) : PsiFileImpl(viewProvider) { 15 | 16 | private val language = findLanguage(fileType.language, viewProvider) as CodeownersLanguage 17 | 18 | companion object { 19 | /** 20 | * Searches for the matching language in [FileViewProvider]. 21 | * 22 | * @param baseLanguage language to look for 23 | * @param viewProvider current [FileViewProvider] 24 | * @return matched [Language] 25 | */ 26 | private fun findLanguage(baseLanguage: Language, viewProvider: FileViewProvider): Language = viewProvider.languages.run { 27 | find { it.isKindOf(baseLanguage) }?.let { return it } 28 | find { it is CodeownersLanguage }?.let { return it } 29 | throw AssertionError("Language $baseLanguage doesn't participate in view provider $viewProvider: $this") 30 | } 31 | } 32 | 33 | init { 34 | LanguageParserDefinitions.INSTANCE.forLanguage(language).apply { 35 | init(fileNodeType, fileNodeType) 36 | } ?: throw CodeownersException("PsiFileBase: language.getParserDefinition() returned null for: $language") 37 | } 38 | 39 | override fun accept(visitor: PsiElementVisitor) { 40 | visitor.visitFile(this) 41 | } 42 | 43 | override fun getFileType() = fileType 44 | 45 | override fun getLanguage() = language 46 | 47 | override fun toString() = fileType.name 48 | 49 | fun getRulesList(): List> { 50 | val items = mutableListOf>() 51 | // language.getPatternsVisitor(items)?.let { acceptChildren(it) } 52 | language.getVisitor(object : CodeownersVisitor() { 53 | override fun visitRule(rule: CodeownersRuleBase<*, *>) { 54 | val regex = rule.pattern.regex(false) 55 | items.add( 56 | Pair( 57 | RegexString(regex), 58 | OwnersReference(rule.owners.map { OwnerString(it.text) }, rule.textOffset) 59 | ) 60 | ) 61 | } 62 | })?.let { acceptChildren(it) } 63 | return items 64 | } 65 | 66 | fun getRules(): List> { 67 | val result = mutableListOf>() 68 | language.getVisitor(object : CodeownersVisitor() { 69 | override fun visitRule(rule: CodeownersRuleBase<*, *>) { 70 | result.add(rule) 71 | } 72 | })?.let { acceptChildren(it) } 73 | return result 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/CodeownersLanguage.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang 2 | 3 | import com.github.fantom.codeowners.file.type.CodeownersFileType 4 | import com.github.fantom.codeowners.reference.CodeownersPatternReferenceSetRecursiveReverse 5 | import com.github.fantom.codeowners.util.TimeTracerKey 6 | import com.github.fantom.codeowners.util.withNullableCloseable 7 | import com.intellij.lang.Language 8 | import com.intellij.psi.FileViewProvider 9 | import com.intellij.psi.PsiElement 10 | import com.intellij.psi.PsiElementVisitor 11 | import com.intellij.psi.PsiReference 12 | import com.intellij.psi.tree.IElementType 13 | import com.intellij.util.ProcessingContext 14 | 15 | /** 16 | * Codeowners [Language] definition 17 | * We pass CODEOWNERS as a base language to implement default common functionality for all codeowners languages 18 | */ 19 | open class CodeownersLanguage protected constructor(name: String) : Language(INSTANCE, name) { 20 | val filename = "CODEOWNERS" 21 | 22 | override fun getDisplayName() = "$filename ($id)" 23 | 24 | constructor() : this("CODEOWNERS") 25 | 26 | open val fileType 27 | get() = CodeownersFileType.INSTANCE 28 | 29 | companion object { 30 | val INSTANCE: CodeownersLanguage = CodeownersLanguage() 31 | } 32 | 33 | fun createFile(viewProvider: FileViewProvider) = CodeownersFile(viewProvider, fileType) 34 | 35 | // TODO tech debt, find a way to have abstract/reused token types 36 | open fun getCrlfToken(): IElementType = TODO("This method should be overridden in class ${this::class}") 37 | 38 | // TODO check if we can simply throw exception from this method to not make return type nullable 39 | open fun getVisitor(visitor: CodeownersVisitor): PsiElementVisitor? = null 40 | // open fun getPatternsVisitor(items: MutableList>): PsiElementVisitor? = null 41 | open fun getReferencesByElement( 42 | psiElement: PsiElement, 43 | processingContext: ProcessingContext 44 | ): Array? = 45 | processingContext.get(TimeTracerKey).let { 46 | it?.start() 47 | withNullableCloseable(it) { 48 | when (psiElement) { 49 | is CodeownersPatternBase -> CodeownersPatternReferenceSetRecursiveReverse(psiElement, it?.nested()).allReferences 50 | else -> null 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/CodeownersPatternBase.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang 2 | 3 | import com.intellij.psi.PsiElement 4 | import java.util.regex.Pattern 5 | 6 | interface CodeownersPatternBase : PsiElement { 7 | /** 8 | * Returns current value. 9 | * 10 | * @return value 11 | */ 12 | val value: String 13 | 14 | val isDirectory: Boolean 15 | 16 | /** 17 | * Returns current pattern. 18 | * 19 | * @return pattern 20 | */ 21 | fun regex(acceptChildren: Boolean = false): String 22 | 23 | /** 24 | * Returns current pattern. 25 | * 26 | * @return pattern 27 | */ 28 | fun pattern(acceptChildren: Boolean = false): Pattern? 29 | } 30 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/CodeownersRuleBase.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang 2 | 3 | import com.intellij.psi.PsiElement 4 | 5 | interface CodeownersRuleBase : PsiElement { 6 | /** 7 | * Returns fs path. 8 | */ 9 | val pattern: PatternType 10 | 11 | /** 12 | * Returns non-empty list if this pattern assigns owners 13 | * and empty list if it resets them (negation case) 14 | */ 15 | val owners: List 16 | 17 | /** 18 | * Some CODEOWNERS implementations allow to unset any early assigned ownership for given file path 19 | */ 20 | val isExplicitlyUnowned: Boolean 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/CodeownersTokenType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang 2 | 3 | import com.intellij.psi.tree.IElementType 4 | import org.jetbrains.annotations.NonNls 5 | 6 | class CodeownersTokenType(@NonNls debugName: String) : IElementType(debugName, CodeownersLanguage.INSTANCE) { 7 | override fun toString(): String { 8 | return "CodeownersTokenType." + super.toString() 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/CodeownersVisitor.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang 2 | 3 | @Suppress("UnnecessaryAbstractClass") 4 | abstract class CodeownersVisitor { 5 | open fun visitRule(rule: CodeownersRuleBase<*, *>) {} 6 | } 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/BitbucketLanguage.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket 2 | 3 | import com.github.fantom.codeowners.file.type.kind.BitbucketFileType 4 | import com.github.fantom.codeowners.lang.CodeownersLanguage 5 | import com.github.fantom.codeowners.lang.CodeownersVisitor 6 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersRule 7 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTeam 8 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTeamReference 9 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTypes 10 | import com.intellij.openapi.diagnostic.Logger 11 | import com.intellij.psi.PsiElement 12 | import com.intellij.psi.PsiReference 13 | import com.intellij.psi.tree.IElementType 14 | import com.intellij.util.ProcessingContext 15 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersVisitor as BitbucketCodeownersVisitor 16 | 17 | class BitbucketLanguage private constructor() : CodeownersLanguage("Bitbucket") { 18 | companion object { 19 | private val LOGGER = Logger.getInstance(BitbucketLanguage::class.java) 20 | val INSTANCE = BitbucketLanguage() 21 | } 22 | 23 | override val fileType 24 | get() = BitbucketFileType 25 | 26 | override fun getCrlfToken(): IElementType { 27 | return CodeownersTypes.CRLF 28 | } 29 | 30 | override fun getVisitor(visitor: CodeownersVisitor) = 31 | object : BitbucketCodeownersVisitor() { 32 | override fun visitRule(rule: CodeownersRule) { 33 | visitor.visitRule(rule) 34 | } 35 | } 36 | 37 | // override fun getPatternsVisitor(items: MutableList>) = 38 | // object : BitbucketCodeownersVisitor() { 39 | // override fun visitRule(rule: CodeownersRule) { 40 | // val regex = rule.pattern.regex(false) 41 | // items.add( 42 | // Pair( 43 | // RegexString(regex), 44 | // OwnersReference(rule.owners.map { OwnerString(it.text) }, rule.textOffset) 45 | // ) 46 | // ) 47 | // } 48 | // } 49 | 50 | override fun getReferencesByElement( 51 | psiElement: PsiElement, 52 | processingContext: ProcessingContext 53 | ): Array? { 54 | LOGGER.trace("> getReferencesByElement bb for $psiElement") 55 | return if (psiElement is CodeownersTeam) { 56 | arrayOf(CodeownersTeamReference(psiElement)) 57 | } else { 58 | super.getReferencesByElement(psiElement, processingContext) 59 | }.also { 60 | LOGGER.trace("< getReferencesByElement bb: $it") 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/CodeownersFindUsagesProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket 2 | 3 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTeamDefinition 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTypes 5 | import com.intellij.lang.cacheBuilder.DefaultWordsScanner 6 | import com.intellij.lang.cacheBuilder.WordsScanner 7 | import com.intellij.lang.findUsages.FindUsagesProvider 8 | import com.intellij.openapi.diagnostic.Logger 9 | import com.intellij.psi.PsiElement 10 | import com.intellij.psi.tree.TokenSet 11 | 12 | class CodeownersFindUsagesProvider : FindUsagesProvider { 13 | companion object { 14 | private val LOGGER = Logger.getInstance(CodeownersFindUsagesProvider::class.java) 15 | } 16 | override fun getWordsScanner(): WordsScanner { 17 | return DefaultWordsScanner( 18 | CodeownersLexerAdapter(), 19 | TokenSet.create(CodeownersTypes.NAME_), 20 | TokenSet.create(CodeownersTypes.COMMENT), 21 | TokenSet.EMPTY 22 | ) 23 | } 24 | 25 | override fun canFindUsagesFor(psiElement: PsiElement): Boolean { 26 | LOGGER.trace("> canFindUsagesFor $psiElement") 27 | return psiElement is CodeownersTeamDefinition 28 | } 29 | 30 | override fun getHelpId(psiElement: PsiElement): String? { 31 | return null 32 | } 33 | 34 | override fun getType(element: PsiElement): String { 35 | return "team" 36 | } 37 | 38 | override fun getDescriptiveName(element: PsiElement): String { 39 | return (element as? CodeownersTeamDefinition)?.teamName?.text ?: "" 40 | } 41 | 42 | override fun getNodeText(element: PsiElement, useFullName: Boolean): String { 43 | return (element as? CodeownersTeamDefinition)?.text ?: "" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/CodeownersLexerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket 2 | 3 | import com.github.fantom.codeowners.lang.kind.bitbucket.lexer.CodeownersLexer 4 | import com.intellij.lexer.FlexAdapter 5 | import java.io.Reader 6 | 7 | class CodeownersLexerAdapter : FlexAdapter(CodeownersLexer(null as Reader?)) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/CodeownersParserDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket 2 | 3 | import com.github.fantom.codeowners.file.type.CodeownersFileType 4 | import com.github.fantom.codeowners.lang.CodeownersFile 5 | import com.github.fantom.codeowners.lang.CodeownersLanguage 6 | import com.github.fantom.codeowners.lang.kind.bitbucket.parser.CodeownersParser 7 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTypes 8 | import com.intellij.lang.ASTNode 9 | import com.intellij.lang.ParserDefinition 10 | import com.intellij.lang.ParserDefinition.SpaceRequirements 11 | import com.intellij.openapi.diagnostic.Logger 12 | import com.intellij.openapi.project.Project 13 | import com.intellij.psi.FileViewProvider 14 | import com.intellij.psi.PsiElement 15 | import com.intellij.psi.TokenType 16 | import com.intellij.psi.tree.IFileElementType 17 | import com.intellij.psi.tree.TokenSet 18 | 19 | class CodeownersParserDefinition : ParserDefinition { 20 | override fun createLexer(project: Project) = CodeownersLexerAdapter() 21 | 22 | override fun getWhitespaceTokens() = WHITE_SPACES 23 | 24 | override fun getCommentTokens() = COMMENTS 25 | 26 | override fun getStringLiteralElements(): TokenSet = TokenSet.EMPTY 27 | 28 | override fun createParser(project: Project) = CodeownersParser() 29 | 30 | override fun getFileNodeType() = FILE 31 | 32 | override fun createFile(viewProvider: FileViewProvider) = when (viewProvider.baseLanguage) { 33 | is CodeownersLanguage -> (viewProvider.baseLanguage as CodeownersLanguage).createFile(viewProvider) 34 | else -> { 35 | LOGGER.trace("Creating generic codeowners file") 36 | CodeownersFile(viewProvider, CodeownersFileType.INSTANCE) 37 | } 38 | } 39 | 40 | override fun spaceExistenceTypeBetweenTokens(left: ASTNode, right: ASTNode) = SpaceRequirements.MAY 41 | 42 | override fun createElement(node: ASTNode): PsiElement = CodeownersTypes.Factory.createElement(node) 43 | 44 | companion object { 45 | private val LOGGER = Logger.getInstance(CodeownersParserDefinition::class.java) 46 | /** Whitespaces. */ 47 | val WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE) 48 | 49 | /** Section comment started with ## */ 50 | val SECTIONS = TokenSet.create(CodeownersTypes.SECTION) 51 | 52 | /** Header comment started with ### */ 53 | val HEADERS = TokenSet.create(CodeownersTypes.HEADER) 54 | 55 | /** Negation element - ! in the beginning of the entry */ 56 | val NEGATIONS = TokenSet.create(CodeownersTypes.NEGATION) 57 | 58 | /** Brackets [] */ 59 | // val BRACKETS = TokenSet.create(CodeownersTypes.BRACKET_LEFT, CodeownersTypes.BRACKET_RIGHT) 60 | 61 | /** Slashes / */ 62 | val SLASHES = TokenSet.create(CodeownersTypes.SLASH) 63 | 64 | /** All values - parts of paths */ 65 | val VALUES = TokenSet.create(CodeownersTypes.VALUE) 66 | 67 | /** All values - parts of paths */ 68 | val NAMES = TokenSet.create(CodeownersTypes.NAME_) 69 | 70 | val CONFIG_NAMES = TokenSet.create( 71 | CodeownersTypes.DESTINATION_BRANCH, 72 | CodeownersTypes.CREATE_PULL_REQUEST_COMMENT, 73 | CodeownersTypes.SUBDIRECTORY_OVERRIDES, 74 | ) 75 | 76 | val CONFIG_VALUES = TokenSet.create( 77 | CodeownersTypes.BRANCH_PATTERN, 78 | CodeownersTypes.ENABLE, 79 | CodeownersTypes.DISABLE, 80 | ) 81 | 82 | /** Regular comment started with # */ 83 | val COMMENTS = TokenSet.create(CodeownersTypes.COMMENT) 84 | 85 | /** Element type of the node describing a file in the specified language. */ 86 | val FILE = IFileElementType(BitbucketLanguage.INSTANCE) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/highlighter/CodeownersHighlighter.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.highlighter 2 | 3 | import com.github.fantom.codeowners.highlighter.CodeownersHighlighterColors 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.CodeownersLexerAdapter 5 | import com.github.fantom.codeowners.lang.kind.bitbucket.CodeownersParserDefinition 6 | import com.intellij.openapi.editor.colors.TextAttributesKey 7 | import com.intellij.openapi.fileTypes.SyntaxHighlighterBase 8 | import com.intellij.psi.tree.IElementType 9 | 10 | /** 11 | * Syntax highlighter definition. 12 | */ 13 | @Suppress("UnusedPrivateMember") 14 | class CodeownersHighlighter : SyntaxHighlighterBase() { 15 | 16 | companion object { 17 | private val ATTRIBUTES = mutableMapOf() 18 | 19 | init { 20 | fillMap(ATTRIBUTES, CodeownersParserDefinition.COMMENTS, CodeownersHighlighterColors.COMMENT) 21 | fillMap(ATTRIBUTES, CodeownersParserDefinition.SECTIONS, CodeownersHighlighterColors.SECTION) 22 | fillMap(ATTRIBUTES, CodeownersParserDefinition.HEADERS, CodeownersHighlighterColors.HEADER) 23 | fillMap(ATTRIBUTES, CodeownersParserDefinition.NEGATIONS, CodeownersHighlighterColors.NEGATION) 24 | // fillMap(ATTRIBUTES, CodeownersParserDefinition.BRACKETS, CodeownersHighlighterColors.BRACKET) 25 | fillMap(ATTRIBUTES, CodeownersParserDefinition.SLASHES, CodeownersHighlighterColors.SLASH) 26 | fillMap(ATTRIBUTES, CodeownersParserDefinition.VALUES, CodeownersHighlighterColors.VALUE) 27 | fillMap(ATTRIBUTES, CodeownersParserDefinition.NAMES, CodeownersHighlighterColors.NAME) 28 | fillMap(ATTRIBUTES, CodeownersParserDefinition.CONFIG_NAMES, CodeownersHighlighterColors.CONFIG_NAME) 29 | fillMap(ATTRIBUTES, CodeownersParserDefinition.CONFIG_VALUES, CodeownersHighlighterColors.CONFIG_VALUE) 30 | } 31 | } 32 | 33 | override fun getHighlightingLexer() = CodeownersLexerAdapter() 34 | 35 | override fun getTokenHighlights(tokenType: IElementType): Array = pack(ATTRIBUTES[tokenType]) 36 | } 37 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/highlighter/CodeownersHighlighterFactory.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.highlighter 2 | 3 | import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vfs.VirtualFile 6 | 7 | /** 8 | * [SyntaxHighlighterFactory] class definition. 9 | */ 10 | class CodeownersHighlighterFactory : SyntaxHighlighterFactory() { 11 | 12 | override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?) = CodeownersHighlighter() 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/CodeownersElementType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi 2 | 3 | import com.github.fantom.codeowners.lang.kind.bitbucket.BitbucketLanguage 4 | import com.intellij.psi.tree.IElementType 5 | import org.jetbrains.annotations.NonNls 6 | 7 | class CodeownersElementType(@NonNls debugName: String) : IElementType(debugName, BitbucketLanguage.INSTANCE) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/CodeownersEntryManipulator.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi 2 | 3 | /** 4 | * Entry manipulator. 5 | */ 6 | //class CodeownersPatternManipulator : AbstractElementManipulator() { 7 | // 8 | // @Throws(IncorrectOperationException::class) 9 | // override fun handleContentChange( 10 | // entry: com.github.fantom.codeowners.lang.kind.github.psi.CodeownersEntry, 11 | // range: TextRange, 12 | // newContent: String 13 | // ): com.github.fantom.codeowners.lang.kind.github.psi.CodeownersEntry { 14 | // val language = entry.language as? CodeownersLanguage ?: return entry 15 | // val fileType = (language.associatedFileType as CodeownersFileType) 16 | // val file = PsiFileFactory.getInstance(entry.project) 17 | // .createFileFromText(language.filename, fileType, range.replace(entry.text, newContent)) 18 | // 19 | // return when ( 20 | // val newEntry = PsiTreeUtil.findChildOfType( 21 | // file, 22 | // com.github.fantom.codeowners.lang.kind.github.psi.CodeownersEntry::class.java 23 | // ) 24 | // ) { 25 | // null -> entry 26 | // else -> entry.replace(newEntry) as com.github.fantom.codeowners.lang.kind.github.psi.CodeownersEntry 27 | // } 28 | // } 29 | // 30 | //// override fun getRangeInElement(element: CodeownersEntry) = element.negation?.run { 31 | //// TextRange.create(startOffsetInParent + textLength, element.textLength) 32 | //// } ?: super.getRangeInElement(element) 33 | //} 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/CodeownersRuleBase.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersRuleBase as CommonCodeownersRuleBase 4 | 5 | interface CodeownersRuleBase : CommonCodeownersRuleBase 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/CodeownersTeamNameNamedElement.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi 2 | 3 | import com.intellij.extapi.psi.ASTWrapperPsiElement 4 | import com.intellij.lang.ASTNode 5 | import com.intellij.openapi.diagnostic.Logger 6 | import com.intellij.psi.PsiElement 7 | import com.intellij.psi.PsiNameIdentifierOwner 8 | import com.intellij.psi.PsiReference 9 | import com.intellij.psi.PsiReferenceBase 10 | import com.intellij.psi.util.PsiTreeUtil 11 | 12 | interface CodeownersTeamNameNamedElement : PsiNameIdentifierOwner { 13 | val teamName: CodeownersTeamName 14 | } 15 | 16 | // class CodeownersTeamNameNamedElementFactory 17 | 18 | abstract class CodeownersTeamNameNamedElementImpl(node: ASTNode) : 19 | ASTWrapperPsiElement(node), 20 | CodeownersTeamNameNamedElement { 21 | companion object { 22 | private val LOGGER = Logger.getInstance(CodeownersTeamReference::class.java) 23 | } 24 | override fun getName(): String? { 25 | return teamName.text.also { 26 | LOGGER.trace("> getName: $it") 27 | } 28 | } 29 | 30 | override fun setName(newName: String): PsiElement? { 31 | // val teamNameNode = node.findChildByType(CodeownersTypes.TEAM_NAME) 32 | // if (teamNameNode != null) { 33 | // val property = SimpleElementFactory.createProperty(project, newName) 34 | // val newTeamNameNode = property.getFirstChild().getNode() 35 | // node.replaceChild(teamNameNode, newTeamNameNode) 36 | // } 37 | return this 38 | } 39 | 40 | override fun getNameIdentifier() = node.findChildByType(CodeownersTypes.TEAM_NAME)?.psi.also { 41 | LOGGER.trace("< getNameIdentifier: ${it?.text}") 42 | } 43 | } 44 | 45 | class CodeownersTeamReference(element: CodeownersTeam) : 46 | PsiReferenceBase( 47 | element, 48 | element.teamName.textRangeInParent.also { 49 | LOGGER.trace("> textRange: $it") 50 | } 51 | ), 52 | PsiReference { 53 | companion object { 54 | private val LOGGER = Logger.getInstance(CodeownersTeamReference::class.java) 55 | } 56 | override fun resolve(): PsiElement? { 57 | LOGGER.trace("> resolve: $this") 58 | val teamDefinitions = 59 | PsiTreeUtil.getChildrenOfType(element.containingFile, CodeownersTeamDefinition::class.java) ?: run { 60 | LOGGER.trace("> resolved to null") 61 | return null 62 | } 63 | return teamDefinitions.firstOrNull { it.teamName.name_.text == (element as CodeownersTeam).teamName.text }.also { 64 | LOGGER.trace("> resolve: $it") 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/CodeownersTokenType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi 2 | 3 | import com.github.fantom.codeowners.lang.kind.bitbucket.BitbucketLanguage 4 | import com.intellij.psi.tree.IElementType 5 | import org.jetbrains.annotations.NonNls 6 | 7 | class CodeownersTokenType(@NonNls debugName: String) : IElementType(debugName, BitbucketLanguage.INSTANCE) { 8 | override fun toString(): String { 9 | return "CodeownersTokenType." + super.toString() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/impl/CodeownersNamedOwnerExtImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersElementImpl 4 | import com.intellij.lang.ASTNode 5 | 6 | /** 7 | * Custom [CodeownersElementImpl] implementation. 8 | */ 9 | @Suppress("UnnecessaryAbstractClass") 10 | abstract class CodeownersNamedOwnerExtImpl(node: ASTNode) : CodeownersElementImpl(node) 11 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/impl/CodeownersPatternExtImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersElementImpl 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersNegation 5 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersPattern 6 | import com.github.fantom.codeowners.util.Glob 7 | import com.intellij.lang.ASTNode 8 | import com.intellij.openapi.util.text.StringUtil 9 | 10 | /** 11 | * Custom [CodeownersElementImpl] implementation. 12 | */ 13 | abstract class CodeownersPatternExtImpl(node: ASTNode) : CodeownersElementImpl(node), CodeownersPattern { 14 | 15 | /** 16 | * Checks if the first child is negated - i.e. `!file.txt` entry. 17 | * 18 | * @return first child is negated 19 | */ 20 | private val isNegated 21 | get() = firstChild is CodeownersNegation 22 | 23 | /** 24 | * Checks if current entry is a directory - i.e. `dir/`. 25 | * 26 | * @return is directory 27 | */ 28 | override val isDirectory 29 | get() = this.patternDirectory != null 30 | 31 | /** 32 | * Returns entry value without leading `!` if entry is negated. 33 | * 34 | * @return entry value without `!` negation sign 35 | */ 36 | override val value 37 | get() = text.takeIf { !isNegated } ?: StringUtil.trimStart(text, "!") 38 | 39 | /** 40 | * Returns entries pattern. 41 | * 42 | * @return pattern 43 | */ 44 | override fun regex(acceptChildren: Boolean) = Glob.createRegex(value, acceptChildren, true) 45 | 46 | /** 47 | * Returns entries pattern. 48 | * 49 | * @return pattern 50 | */ 51 | override fun pattern(acceptChildren: Boolean) = Glob.createPattern(value, acceptChildren, true) 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/impl/CodeownersRuleExtImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersElementImpl 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersOwner 5 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersPattern 6 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersRule 7 | import com.intellij.lang.ASTNode 8 | 9 | /** 10 | * Custom [CodeownersElementImpl] implementation. 11 | */ 12 | abstract class CodeownersRuleExtImpl(node: ASTNode) : CodeownersElementImpl(node), CodeownersRule { 13 | override val pattern: CodeownersPattern 14 | // either assign or entry is available 15 | get() = assign?.pattern ?: reset?.pattern!! 16 | override val owners: List 17 | // reset matches path and assigns empty owners list 18 | get() = assign?.owners?.ownerList ?: emptyList() 19 | override val isExplicitlyUnowned: Boolean 20 | get() = reset != null 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/bitbucket/psi/impl/CodeownersTeamDefinitionExtImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersElementImpl 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTeamDefinition 5 | import com.intellij.lang.ASTNode 6 | import com.intellij.navigation.ItemPresentation 7 | import javax.swing.Icon 8 | 9 | abstract class CodeownersTeamDefinitionExtImpl(node: ASTNode) : CodeownersElementImpl(node), CodeownersTeamDefinition { 10 | override fun getName(): String = teamName.text 11 | 12 | override fun getPresentation(): ItemPresentation? { 13 | return object : ItemPresentation { 14 | override fun getPresentableText(): String = name 15 | override fun getIcon(unused: Boolean): Icon? = null 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/CodeownersLexerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github 2 | 3 | import com.github.fantom.codeowners.lang.kind.github.lexer.CodeownersLexer 4 | import com.intellij.lexer.FlexAdapter 5 | import java.io.Reader 6 | 7 | class CodeownersLexerAdapter : FlexAdapter(CodeownersLexer(null as Reader?)) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/CodeownersParserDefinition.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github 2 | 3 | import com.github.fantom.codeowners.file.type.CodeownersFileType 4 | import com.github.fantom.codeowners.lang.CodeownersFile 5 | import com.github.fantom.codeowners.lang.CodeownersLanguage 6 | import com.github.fantom.codeowners.lang.kind.github.parser.CodeownersParser 7 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersTypes 8 | import com.intellij.lang.ASTNode 9 | import com.intellij.lang.ParserDefinition 10 | import com.intellij.lang.ParserDefinition.SpaceRequirements 11 | import com.intellij.openapi.project.Project 12 | import com.intellij.psi.FileViewProvider 13 | import com.intellij.psi.PsiElement 14 | import com.intellij.psi.TokenType 15 | import com.intellij.psi.tree.IFileElementType 16 | import com.intellij.psi.tree.TokenSet 17 | 18 | class CodeownersParserDefinition : ParserDefinition { 19 | override fun createLexer(project: Project) = CodeownersLexerAdapter() 20 | 21 | override fun getWhitespaceTokens() = WHITE_SPACES 22 | 23 | override fun getCommentTokens() = COMMENTS 24 | 25 | override fun getStringLiteralElements(): TokenSet = TokenSet.EMPTY 26 | 27 | override fun createParser(project: Project) = CodeownersParser() 28 | 29 | override fun getFileNodeType() = FILE 30 | 31 | override fun createFile(viewProvider: FileViewProvider) = when (viewProvider.baseLanguage) { 32 | is CodeownersLanguage -> (viewProvider.baseLanguage as CodeownersLanguage).createFile(viewProvider) 33 | else -> CodeownersFile(viewProvider, CodeownersFileType.INSTANCE) 34 | } 35 | 36 | override fun spaceExistenceTypeBetweenTokens(left: ASTNode, right: ASTNode) = SpaceRequirements.MAY 37 | 38 | override fun createElement(node: ASTNode): PsiElement = CodeownersTypes.Factory.createElement(node) 39 | 40 | companion object { 41 | /** Whitespaces. */ 42 | val WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE) 43 | 44 | /** Section comment started with ## */ 45 | val SECTIONS = TokenSet.create(CodeownersTypes.SECTION) 46 | 47 | /** Header comment started with ### */ 48 | val HEADERS = TokenSet.create(CodeownersTypes.HEADER) 49 | 50 | /** Slashes / */ 51 | val SLASHES = TokenSet.create(CodeownersTypes.SLASH) 52 | 53 | /** All values - parts of paths */ 54 | val VALUES = TokenSet.create(CodeownersTypes.VALUE) 55 | 56 | /** Regular comment started with # */ 57 | val COMMENTS = TokenSet.create(CodeownersTypes.COMMENT) 58 | 59 | /** Element type of the node describing a file in the specified language. */ 60 | val FILE = IFileElementType(GithubLanguage.INSTANCE) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/GithubLanguage.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github 2 | 3 | import com.github.fantom.codeowners.file.type.kind.GithubFileType 4 | import com.github.fantom.codeowners.lang.CodeownersLanguage 5 | import com.github.fantom.codeowners.lang.CodeownersVisitor 6 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersNamedOwner 7 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersRule 8 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersTypes 9 | import com.github.fantom.codeowners.reference.CodeownersGithubOwnerReference 10 | import com.intellij.psi.PsiElement 11 | import com.intellij.psi.PsiReference 12 | import com.intellij.psi.tree.IElementType 13 | import com.intellij.util.ProcessingContext 14 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersVisitor as GithubCodeownersVisitor 15 | 16 | class GithubLanguage private constructor() : CodeownersLanguage("Github") { 17 | companion object { 18 | val INSTANCE = GithubLanguage() 19 | } 20 | 21 | override val fileType 22 | get() = GithubFileType 23 | 24 | override fun getCrlfToken(): IElementType { 25 | return CodeownersTypes.CRLF 26 | } 27 | 28 | override fun getVisitor(visitor: CodeownersVisitor) = 29 | object : GithubCodeownersVisitor() { 30 | override fun visitRule(rule: CodeownersRule) { 31 | visitor.visitRule(rule) 32 | } 33 | } 34 | 35 | // override fun getPatternsVisitor(items: MutableList>) = 36 | // object : GithubCodeownersVisitor() { 37 | // override fun visitRule(rule: CodeownersRule) { 38 | // val regex = rule.pattern.regex(false) 39 | // items.add( 40 | // Pair( 41 | // RegexString(regex), 42 | // OwnersReference(rule.owners.map { OwnerString(it.text) }, rule.textOffset) 43 | // ) 44 | // ) 45 | // } 46 | // } 47 | 48 | override fun getReferencesByElement( 49 | psiElement: PsiElement, 50 | processingContext: ProcessingContext 51 | ): Array? = 52 | if (psiElement is CodeownersNamedOwner) { 53 | arrayOf(CodeownersGithubOwnerReference(psiElement)) 54 | } else { 55 | super.getReferencesByElement(psiElement, processingContext) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/highlighter/CodeownersHighlighter.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.highlighter 2 | 3 | import com.github.fantom.codeowners.highlighter.CodeownersHighlighterColors 4 | import com.github.fantom.codeowners.lang.kind.github.CodeownersLexerAdapter 5 | import com.github.fantom.codeowners.lang.kind.github.CodeownersParserDefinition 6 | import com.intellij.openapi.editor.colors.TextAttributesKey 7 | import com.intellij.openapi.fileTypes.SyntaxHighlighterBase 8 | import com.intellij.psi.tree.IElementType 9 | 10 | /** 11 | * Syntax highlighter definition. 12 | */ 13 | @Suppress("UnusedPrivateMember") 14 | class CodeownersHighlighter : SyntaxHighlighterBase() { 15 | 16 | companion object { 17 | private val ATTRIBUTES = mutableMapOf() 18 | 19 | init { 20 | fillMap(ATTRIBUTES, CodeownersParserDefinition.COMMENTS, CodeownersHighlighterColors.COMMENT) 21 | fillMap(ATTRIBUTES, CodeownersParserDefinition.SECTIONS, CodeownersHighlighterColors.SECTION) 22 | fillMap(ATTRIBUTES, CodeownersParserDefinition.HEADERS, CodeownersHighlighterColors.HEADER) 23 | fillMap(ATTRIBUTES, CodeownersParserDefinition.SLASHES, CodeownersHighlighterColors.SLASH) 24 | fillMap(ATTRIBUTES, CodeownersParserDefinition.VALUES, CodeownersHighlighterColors.VALUE) 25 | } 26 | } 27 | 28 | override fun getHighlightingLexer() = CodeownersLexerAdapter() 29 | 30 | override fun getTokenHighlights(tokenType: IElementType): Array = pack(ATTRIBUTES[tokenType]) 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/highlighter/CodeownersHighlighterFactory.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.highlighter 2 | 3 | import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vfs.VirtualFile 6 | 7 | /** 8 | * [SyntaxHighlighterFactory] class definition. 9 | */ 10 | class CodeownersHighlighterFactory : SyntaxHighlighterFactory() { 11 | 12 | override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?) = CodeownersHighlighter() 13 | } 14 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/psi/CodeownersElementType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.psi 2 | 3 | import com.github.fantom.codeowners.lang.kind.github.GithubLanguage 4 | import com.intellij.psi.tree.IElementType 5 | import org.jetbrains.annotations.NonNls 6 | 7 | class CodeownersElementType(@NonNls debugName: String) : IElementType(debugName, GithubLanguage.INSTANCE) 8 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/psi/CodeownersNamedOwnerManipulator.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.psi 2 | 3 | import com.github.fantom.codeowners.file.type.CodeownersFileType 4 | import com.github.fantom.codeowners.lang.CodeownersLanguage 5 | import com.intellij.openapi.util.TextRange 6 | import com.intellij.psi.AbstractElementManipulator 7 | import com.intellij.psi.PsiFileFactory 8 | import com.intellij.psi.util.PsiTreeUtil 9 | import com.intellij.util.IncorrectOperationException 10 | 11 | /** 12 | * Entry manipulator. 13 | */ 14 | class CodeownersNamedOwnerManipulator : AbstractElementManipulator() { 15 | 16 | @Throws(IncorrectOperationException::class) 17 | override fun handleContentChange( 18 | owner: CodeownersNamedOwner, 19 | range: TextRange, 20 | newContent: String 21 | ): CodeownersNamedOwner { 22 | val language = owner.language as? CodeownersLanguage ?: return owner 23 | val fileType = (language.associatedFileType as CodeownersFileType) 24 | val file = PsiFileFactory.getInstance(owner.project) 25 | .createFileFromText(language.filename, fileType, range.replace(owner.text, newContent)) 26 | 27 | return when (val newOwner = PsiTreeUtil.findChildOfType(file, CodeownersNamedOwner::class.java)) { 28 | null -> owner 29 | else -> owner.replace(newOwner) as CodeownersNamedOwner 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/psi/CodeownersPatternManipulator.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.psi 2 | 3 | import com.github.fantom.codeowners.file.type.CodeownersFileType 4 | import com.github.fantom.codeowners.lang.CodeownersLanguage 5 | import com.intellij.openapi.util.TextRange 6 | import com.intellij.psi.AbstractElementManipulator 7 | import com.intellij.psi.PsiFileFactory 8 | import com.intellij.psi.util.PsiTreeUtil 9 | import com.intellij.util.IncorrectOperationException 10 | 11 | /** 12 | * Entry manipulator. 13 | */ 14 | class CodeownersPatternManipulator : AbstractElementManipulator() { 15 | 16 | @Throws(IncorrectOperationException::class) 17 | override fun handleContentChange(pattern: CodeownersPattern, range: TextRange, newContent: String): CodeownersPattern { 18 | val language = pattern.language as? CodeownersLanguage ?: return pattern 19 | val fileType = (language.associatedFileType as CodeownersFileType) 20 | val file = PsiFileFactory.getInstance(pattern.project) 21 | .createFileFromText(language.filename, fileType, range.replace(pattern.text, newContent)) 22 | 23 | return when (val newPattern = PsiTreeUtil.findChildOfType(file, CodeownersPattern::class.java)) { 24 | null -> pattern 25 | else -> pattern.replace(newPattern) as CodeownersPattern 26 | } 27 | } 28 | 29 | // override fun getRangeInElement(element: CodeownersPattern) = element.negation?.run { 30 | // TextRange.create(startOffsetInParent + textLength, element.textLength) 31 | // } ?: super.getRangeInElement(element) 32 | } 33 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/psi/CodeownersRuleBase.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.psi 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersRuleBase as CommonCodeownersRuleBase 4 | 5 | interface CodeownersRuleBase : CommonCodeownersRuleBase 6 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/psi/CodeownersTokenType.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.psi 2 | 3 | import com.github.fantom.codeowners.lang.kind.github.GithubLanguage 4 | import com.intellij.psi.tree.IElementType 5 | import org.jetbrains.annotations.NonNls 6 | 7 | class CodeownersTokenType(@NonNls debugName: String) : IElementType(debugName, GithubLanguage.INSTANCE) { 8 | override fun toString(): String { 9 | return "CodeownersTokenType." + super.toString() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/psi/impl/CodeownersNamedOwnerExtImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.psi.impl 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersElementImpl 4 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersNamedOwner 5 | import com.intellij.lang.ASTNode 6 | 7 | /** 8 | * Custom [CodeownersElementImpl] implementation. 9 | */ 10 | @Suppress("UnnecessaryAbstractClass") 11 | abstract class CodeownersNamedOwnerExtImpl(node: ASTNode) : CodeownersElementImpl(node), CodeownersNamedOwner 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/psi/impl/CodeownersPatternExtImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.psi.impl 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersElementImpl 4 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersPattern 5 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersPatternDirectory 6 | import com.github.fantom.codeowners.util.Glob 7 | import com.intellij.lang.ASTNode 8 | 9 | /** 10 | * Custom [CodeownersElementImpl] implementation. 11 | */ 12 | abstract class CodeownersPatternExtImpl(node: ASTNode) : CodeownersElementImpl(node), CodeownersPattern { 13 | 14 | /** 15 | * Checks if current entry is a directory - i.e. `dir/`. 16 | * 17 | * @return is directory 18 | */ 19 | override val isDirectory 20 | get() = this is CodeownersPatternDirectory 21 | 22 | /** 23 | * Returns entry value 24 | * 25 | * @return entry value 26 | */ 27 | override val value: String 28 | get() = text 29 | 30 | /** 31 | * Returns entries pattern. 32 | * 33 | * @return pattern 34 | */ 35 | override fun regex(acceptChildren: Boolean) = Glob.createRegex(value, acceptChildren, supportSquareBrackets = false) 36 | 37 | /** 38 | * Returns entries pattern. 39 | * 40 | * @return pattern 41 | */ 42 | override fun pattern(acceptChildren: Boolean) = Glob.createPattern(value, acceptChildren, supportSquareBrackets = false) 43 | } 44 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/lang/kind/github/psi/impl/CodeownersRuleExtImpl.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.lang.kind.github.psi.impl 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersElementImpl 4 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersOwner 5 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersPattern 6 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersRule 7 | import com.intellij.lang.ASTNode 8 | 9 | /** 10 | * Custom [CodeownersElementImpl] implementation. 11 | */ 12 | abstract class CodeownersRuleExtImpl(node: ASTNode) : CodeownersElementImpl(node), CodeownersRule { 13 | override val pattern: CodeownersPattern 14 | // either assign or entry is available 15 | get() = assign?.patternFile ?: reset?.patternFile!! 16 | override val owners: List 17 | // reset matches path and assigns empty owners list 18 | get() = assign?.owners?.ownerList ?: emptyList() 19 | override val isExplicitlyUnowned: Boolean 20 | get() = reset != null 21 | } 22 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/reference/CodeownersGithubOwnerDocumentationProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.reference 2 | 3 | import com.intellij.lang.documentation.AbstractDocumentationProvider 4 | import com.intellij.psi.PsiElement 5 | 6 | class CodeownersGithubOwnerDocumentationProvider : AbstractDocumentationProvider() { 7 | override fun getQuickNavigateInfo(element: PsiElement?, originalElement: PsiElement?): String? { 8 | return if (element is CodeownersGithubOwnerReference.MyFakePsiElement) { 9 | element.url 10 | } else null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/reference/CodeownersGithubOwnerReference.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.reference 2 | 3 | import com.github.fantom.codeowners.lang.kind.github.psi.CodeownersNamedOwner 4 | import com.intellij.navigation.ItemPresentation 5 | import com.intellij.openapi.paths.WebReference 6 | import com.intellij.openapi.util.TextRange 7 | import com.intellij.pom.Navigatable 8 | import com.intellij.psi.PsiElement 9 | import com.intellij.psi.PsiNamedElement 10 | import com.intellij.psi.SyntheticElement 11 | import com.intellij.psi.impl.FakePsiElement 12 | 13 | class CodeownersGithubOwnerReference(val owner: CodeownersNamedOwner) : WebReference(owner, createUrl(owner)) { 14 | companion object { 15 | // TODO extract url into language (and settings for BitBucket/GitLab) 16 | fun createUrl(owner: CodeownersNamedOwner) = if (owner.ownerName.userName != null) { 17 | "https://github.com/${owner.ownerName.userName!!.text}" 18 | } else { 19 | "https://github.com/orgs/${owner.ownerName.team!!.orgName.text}/teams/${owner.ownerName.team!!.teamName.text}" 20 | } 21 | } 22 | 23 | override fun resolve(): MyFakePsiElement? { 24 | return MyFakePsiElement(super.resolve() ?: return null, createUrl(owner)) 25 | } 26 | 27 | /** 28 | * This class basically reimplements [WebReference.MyFakePsiElement] to be able to show 29 | * custom tooltip on Ctrl-hover 30 | * 31 | * WebReference.MyFakePsiElement is handled by [com.intellij.openapi.paths.WebReferenceDocumentationProvider] 32 | */ 33 | inner class MyFakePsiElement(val element: PsiElement, val url: String) : FakePsiElement(), SyntheticElement { 34 | 35 | override fun getParent(): PsiElement = element.parent 36 | 37 | override fun navigate(requestFocus: Boolean) { 38 | (element as Navigatable).navigate(requestFocus) 39 | } 40 | 41 | override fun getPresentableText() = (element as ItemPresentation).presentableText 42 | 43 | override fun getName() = (element as PsiNamedElement).name 44 | 45 | override fun getTextRange(): TextRange? = element.textRange 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/reference/CodeownersPatternsMatchedFilesCache.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.reference 2 | 3 | import com.github.benmanes.caffeine.cache.Cache 4 | import com.github.benmanes.caffeine.cache.Caffeine 5 | import com.github.fantom.codeowners.services.PatternCache 6 | import com.intellij.openapi.Disposable 7 | import com.intellij.openapi.application.ApplicationManager 8 | import com.intellij.openapi.components.Service 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.openapi.vfs.VirtualFileListener 12 | import com.intellij.openapi.vfs.VirtualFileManager 13 | import com.intellij.openapi.vfs.newvfs.BulkFileListener 14 | import com.intellij.openapi.vfs.newvfs.events.* 15 | import java.util.concurrent.ConcurrentHashMap 16 | import java.util.concurrent.TimeUnit 17 | 18 | /** 19 | * Cache that retrieves matching files using given glob prefix, taking at an level/dir only into account. 20 | * Cache population happened on demand in the background. 21 | * The cache eviction happen in the following cases: 22 | * * by using [VirtualFileListener] to handle filesystem changes 23 | * and clean cache if needed for the specific pattern parts. 24 | * * after entries have been expired: entries becomes expired if no read/write operations happened with the 25 | * corresponding key during some amount of time (10 minutes). 26 | * * after project dispose 27 | */ 28 | typealias AtAnyLevel = Boolean 29 | typealias DirOnly = Boolean 30 | typealias Root = String 31 | 32 | @Service(Service.Level.PROJECT) 33 | internal class CodeownersPatternsMatchedFilesCache : Disposable { 34 | private val cacheByPrefix = ConcurrentHashMap, Collection>>() 35 | 36 | init { 37 | ApplicationManager.getApplication().messageBus.connect(this) 38 | .subscribe( 39 | VirtualFileManager.VFS_CHANGES, 40 | object : BulkFileListener { 41 | override fun after(events: List) { 42 | if (cacheByPrefix.isEmpty()) { 43 | return 44 | } 45 | 46 | for (event in events) { 47 | if (event is VFileCreateEvent || 48 | event is VFileDeleteEvent || 49 | event is VFileCopyEvent 50 | ) { 51 | cleanupCache(event.path) 52 | } else if (event is VFilePropertyChangeEvent && event.isRename) { 53 | cleanupCache(event.oldPath) 54 | cleanupCache(event.path) 55 | } else if (event is VFileMoveEvent) { 56 | cleanupCache(event.oldPath) 57 | cleanupCache(event.path) 58 | } 59 | } 60 | } 61 | 62 | private fun cleanupCache(path: String) { 63 | val caches = cacheByPrefix.filterKeys { 64 | path.startsWith(it) 65 | } 66 | // in practice there should be only one cache for given path 67 | caches.forEach { (root, cache) -> 68 | val relativePath = path.removePrefix(root).let { 69 | // TODO check if this condition can be false 70 | if (!it.endsWith('/')) { 71 | StringBuilder(it).append('/') // glob compiled to regex always contains trailing slash 72 | } else { 73 | it 74 | } 75 | } 76 | val cacheMap = cache.asMap() 77 | val globCache = PatternCache.getInstance() 78 | for (key in cacheMap.keys) { 79 | // TODO think about how to take the fact that path may point to a file into account. 80 | // In this case we shouldn't assume that atAnyLevel glob may point 81 | // to some subtree of this path and so shouldn't invalidate such a glob 82 | val regex = globCache.getOrCreatePrefixRegex(key.first, key.second, key.third) 83 | val match = regex.find(relativePath) ?: continue 84 | // if relative path matched only partially, it means 85 | // it points to a tree that is not covered by this glob, 86 | // even if they have common prefix 87 | if (match.range.last >= relativePath.indices.last) { 88 | cacheMap.remove(key) 89 | } 90 | } 91 | } 92 | } 93 | } 94 | ) 95 | } 96 | 97 | override fun dispose() { 98 | cacheByPrefix.values.forEach { it.invalidateAll() } 99 | cacheByPrefix.clear() 100 | } 101 | 102 | private fun getCache(context: String) = cacheByPrefix.computeIfAbsent(context) { 103 | Caffeine.newBuilder() 104 | .expireAfterAccess(10, TimeUnit.MINUTES) 105 | .build() // CharSequence as a key to be able to lookup by substring without instantiation 106 | } 107 | 108 | fun getFilesByPrefix(context: String, prefix: CharSequence, atAnyLevel: Boolean, dirOnly: Boolean): Collection { 109 | return getCache(context) 110 | .getIfPresent(Triple(prefix, atAnyLevel, dirOnly)) ?: emptyList() 111 | } 112 | 113 | fun addFilesByPrefix(context: String, prefix: CharSequence, atAnyLevel: AtAnyLevel, dirOnly: DirOnly, files: Collection) { 114 | getCache(context).put(Triple(prefix, atAnyLevel, dirOnly), files) 115 | } 116 | 117 | companion object { 118 | @JvmStatic 119 | fun getInstance(project: Project): CodeownersPatternsMatchedFilesCache { 120 | return project.getService(CodeownersPatternsMatchedFilesCache::class.java) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/reference/CodeownersReferenceContributor.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.reference 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersFile 4 | import com.github.fantom.codeowners.lang.CodeownersLanguage 5 | import com.github.fantom.codeowners.util.TimeTracer 6 | import com.github.fantom.codeowners.util.TimeTracerKey 7 | import com.intellij.patterns.PlatformPatterns 8 | import com.intellij.psi.* 9 | import com.intellij.util.ProcessingContext 10 | 11 | /** 12 | * PSI elements references contributor. 13 | */ 14 | class CodeownersReferenceContributor : PsiReferenceContributor() { 15 | 16 | override fun registerReferenceProviders(psiReferenceRegistrar: PsiReferenceRegistrar) { 17 | psiReferenceRegistrar.registerReferenceProvider( 18 | PlatformPatterns.psiElement().inFile(PlatformPatterns.psiFile(CodeownersFile::class.java)), 19 | CodeownersReferenceProvider() 20 | ) 21 | } 22 | 23 | private class CodeownersReferenceProvider : PsiReferenceProvider() { 24 | override fun getReferencesByElement( 25 | psiElement: PsiElement, 26 | processingContext: ProcessingContext 27 | ): Array { 28 | // println("> getReferencesByElement for $psiElement") 29 | return TimeTracer.wrap("CodeownersReferenceProvider.getReferencesByElement ${psiElement.text}") { tracer -> 30 | (psiElement.language as? CodeownersLanguage)?.run { 31 | processingContext.put(TimeTracerKey, tracer.nested("CodeownersLanguage.getReferencesByElement")) 32 | getReferencesByElement(psiElement, processingContext) 33 | } ?: PsiReference.EMPTY_ARRAY 34 | } 35 | // when (psiElement) { 36 | // is com.github.fantom.codeowners.lang.kind.github.psi.CodeownersEntry -> 37 | // CodeownersEntryReferenceSet(psiElement).allReferences 38 | // is com.github.fantom.codeowners.lang.kind.github.psi.CodeownersNamedOwner -> 39 | // arrayOf(CodeownersGithubOwnerReference(psiElement)) 40 | // else -> PsiReference.EMPTY_ARRAY 41 | // } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/search/CodeownersSearchFilter.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.search 2 | 3 | import com.github.fantom.codeowners.CodeownersManager 4 | import com.github.fantom.codeowners.OwnersList 5 | import com.github.fantom.codeowners.indexing.CodeownersEntryOccurrence 6 | import com.github.fantom.codeowners.indexing.OwnerString 7 | import com.intellij.openapi.vfs.VirtualFile 8 | 9 | interface Predicate { 10 | // TODO change to OwnersSet, duplicates make no sense 11 | fun satisfies(fileOwners: OwnersList?): Boolean 12 | } 13 | 14 | sealed interface Filter: Predicate { 15 | fun displayName(): CharSequence 16 | 17 | // TODO rewrite using service + context 18 | /** 19 | * @return false, if this filter definitely cannot be satisfied by given codeowners file, true otherwise 20 | */ 21 | fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean 22 | 23 | sealed interface Condition: Filter { 24 | // files without assigned owners 25 | data object Unowned: Condition { 26 | override fun displayName() = "unowned" 27 | 28 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean { 29 | return true // cannot easily calculate it precise: need to proof there is at least one unowned file 30 | } 31 | 32 | override fun satisfies(fileOwners: OwnersList?) = fileOwners == null 33 | } 34 | 35 | // explicitly unowned files 36 | data object OwnershipReset: Condition { 37 | override fun displayName() = "explicitly unowned" 38 | 39 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean { 40 | return codeownersFile.items.any { it.second.owners.isEmpty() } 41 | } 42 | 43 | override fun satisfies(fileOwners: OwnersList?) = fileOwners?.isEmpty() ?: false 44 | } 45 | 46 | // files, owned by any (at least one) of the given owners 47 | data class OwnedByAnyOf(val owners: Set): Condition { 48 | override fun displayName() = "owned by any of ${owners.joinToString(", ")}" 49 | 50 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean { 51 | return true // because "owners" can be only from this file, so they own some files 52 | } 53 | 54 | override fun satisfies(fileOwners: OwnersList?) = fileOwners?.any { it in owners } ?: false 55 | } 56 | 57 | // files, owned by all the given owners, and maybe by some extra owners 58 | data class OwnedByAllOf(val owners: Set): Condition { 59 | override fun displayName() = "owned by all of ${owners.joinToString(", ")}" 60 | 61 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean { 62 | return codeownersFile.items.map { it.second }.any { satisfies(it.owners) } 63 | } 64 | 65 | override fun satisfies(fileOwners: OwnersList?) = fileOwners?.containsAll(owners) ?: false 66 | } 67 | 68 | // files, owned by only given owners, no extra owners allowed 69 | data class OwnedByExactly(val owners: Set): Condition { 70 | override fun displayName() = "owned by exactly ${owners.joinToString(", ")}" 71 | 72 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean { 73 | return codeownersFile.items.map { it.second }.any { satisfies(it.owners) } 74 | } 75 | 76 | override fun satisfies(fileOwners: OwnersList?) = fileOwners?.let { owners == it.toSet() } ?: false 77 | } 78 | } 79 | 80 | // negation of some Condition 81 | data class Not(val condition: Condition): Filter { 82 | override fun displayName() = "not ${condition.displayName()}" 83 | 84 | override fun satisfiable(codeownersFile: CodeownersEntryOccurrence): Boolean { 85 | // cannot calculate it in abstract: it depends on the nature of the "condition" 86 | return true 87 | } 88 | 89 | override fun satisfies(fileOwners: OwnersList?): Boolean { 90 | return !condition.satisfies(fileOwners) 91 | } 92 | } 93 | } 94 | 95 | // disjunction of conjunctions 96 | data class DNF(val filters: List>): Predicate { 97 | override fun satisfies(fileOwners: OwnersList?) = filters.any { conj -> conj.all { n -> n.satisfies(fileOwners) } } 98 | 99 | fun displayName() = 100 | filters.joinToString(" or ") { conj -> "(${conj.joinToString(" and ") { n -> n.displayName() }})" } 101 | } 102 | 103 | data class CodeownersSearchFilter( 104 | val codeownersFile: CodeownersEntryOccurrence, 105 | // DNF: disjunction of conjunctions 106 | private val dnf: DNF 107 | ): Predicate by dnf { 108 | fun displayName() = "${codeownersFile.url}: ${dnf.displayName()}" 109 | 110 | context(CodeownersManager) 111 | fun satisfies(file: VirtualFile): Boolean { 112 | val ownersRef = getFileOwners(file, codeownersFile).getOrNull() ?: return false 113 | val ownersList = ownersRef.ref?.owners 114 | 115 | return satisfies(ownersList) 116 | } 117 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/search/CodeownersSearchScope.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.search 2 | 3 | import com.github.fantom.codeowners.CodeownersIcons 4 | import com.github.fantom.codeowners.CodeownersManager 5 | import com.intellij.openapi.components.service 6 | import com.intellij.openapi.module.Module 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.vfs.VirtualFile 9 | import com.intellij.psi.search.GlobalSearchScope 10 | 11 | 12 | class CodeownersSearchScope( 13 | project: Project, 14 | private val filterCtx: CodeownersSearchFilter 15 | ): GlobalSearchScope(project) { 16 | private val manager = project.service() 17 | 18 | override fun contains(file: VirtualFile): Boolean { 19 | with(manager) { 20 | return filterCtx.satisfies(file) 21 | } 22 | } 23 | 24 | override fun isSearchInModuleContent(aModule: Module) = true 25 | 26 | override fun isSearchInLibraries() = true 27 | 28 | override fun getDisplayName() = filterCtx.displayName() 29 | 30 | override fun getIcon() = CodeownersIcons.FILE 31 | } 32 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/search/CodeownersSearchScopeDescriptor.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.search 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.CodeownersIcons 5 | import com.github.fantom.codeowners.CodeownersManager 6 | import com.github.fantom.codeowners.search.ui.CodeownersSearchFilterDialog 7 | import com.intellij.ide.util.scopeChooser.ScopeDescriptor 8 | import com.intellij.openapi.components.service 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.psi.search.GlobalSearchScope 11 | import com.intellij.psi.search.SearchScope 12 | 13 | class CodeownersSearchScopeDescriptor(private val project: Project) : ScopeDescriptor(null) { 14 | private val manager = project.service() 15 | 16 | /** 17 | * It is a very dirty hack to avoid showing [CodeownersSearchFilterDialog] when user opens `Search Structurally` dialog. 18 | * The issue is that when it happens, IDEA uses [com.intellij.structuralsearch.plugin.ui.ScopePanel.SCOPE_FILTER] 19 | * to allow `com.intellij.ide.util.scopeChooser.ClassHierarchyScopeDescriptor` in selection list, but 20 | * filter out [com.intellij.openapi.module.impl.scopes.ModuleWithDependenciesScope]. 21 | * It does so by comparing [com.intellij.ide.util.scopeChooser.ScopeDescriptor.getDisplayName] value with 22 | * the message for key `scope.class.hierarchy` 23 | * and checking type of the scope returned by [com.intellij.ide.util.scopeChooser.ScopeDescriptor.getScope] 24 | * for all other descriptors: 25 | * ```java 26 | * private static final Condition SCOPE_FILTER = 27 | * (ScopeDescriptor descriptor) -> IdeBundle.message("scope.class.hierarchy").equals(descriptor.getDisplayName()) || 28 | * !(descriptor.getScope() instanceof ModuleWithDependenciesScope); // don't show module scope 29 | * ``` 30 | * So, since upon first call to [getScope] [cachedScope] is `null`, we will show the dialog. 31 | * The same would happen for `ClassHierarchyScopeDescriptor`, but it is handled by this `getDisplayName` comparison 32 | * 33 | * The solution is to have a [unsafeToShowDialog] flag, that will protect us from showing the dialog 34 | * on first invocation in this case, since we set it to `true` only after [getDisplayName] s invoked, 35 | * which is the first method invoked in this `SCOPE_FILTER`. All other methods' invocations will reset it to `false`. 36 | * 37 | * In the regular case, e.g., when selecting this scope in `Find in Files` dialog, 38 | * we will show it on the first invocation, because in this case it turns out [getDisplayName] 39 | * is not the last method invoked before [getScope], so when [getScope] is invoked in these cases, 40 | * [unsafeToShowDialog] is reset to `false` by some other method (including the first invocation of [getScope]). 41 | */ 42 | private var unsafeToShowDialog = false 43 | 44 | private var cachedScope: SearchScope? = null 45 | 46 | override fun getDisplayName(): String { 47 | unsafeToShowDialog = true 48 | return CodeownersBundle.message("search.scope.name") 49 | } 50 | 51 | override fun getIcon() = CodeownersIcons.FILE.also { unsafeToShowDialog = false } 52 | 53 | override fun scopeEquals(scope: SearchScope?): Boolean { 54 | unsafeToShowDialog = false 55 | return cachedScope == scope 56 | } 57 | 58 | override fun getScope(): SearchScope? { 59 | if (unsafeToShowDialog) { 60 | unsafeToShowDialog = false 61 | return null 62 | } 63 | if (cachedScope == null) { 64 | val codeownersFiles = manager.getCodeownersFiles().ifEmpty { return null } 65 | 66 | val dialog = CodeownersSearchFilterDialog(project, codeownersFiles) 67 | 68 | if (!dialog.showAndGet()) { 69 | cachedScope = GlobalSearchScope.EMPTY_SCOPE 70 | return null 71 | } 72 | 73 | val result = dialog.result!! // it cannot be null 74 | 75 | val (codeownersFile, dnf) = result 76 | 77 | cachedScope = CodeownersSearchScope(project, CodeownersSearchFilter(codeownersFile, DNF(dnf))) 78 | } 79 | 80 | return cachedScope 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/search/CodeownersSearchScopeDescriptorProvider.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.search 2 | 3 | import com.intellij.ide.util.scopeChooser.ScopeDescriptor 4 | import com.intellij.ide.util.scopeChooser.ScopeDescriptorProvider 5 | import com.intellij.openapi.actionSystem.DataContext 6 | import com.intellij.openapi.project.Project 7 | 8 | class CodeownersSearchScopeDescriptorProvider : ScopeDescriptorProvider { 9 | override fun getScopeDescriptors(project: Project, dataContext: DataContext): Array { 10 | return arrayOf(CodeownersSearchScopeDescriptor(project)) 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/search/ui/CodeownersSearchFilterDialog.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.search.ui 2 | 3 | import com.github.fantom.codeowners.CodeownersManager 4 | import com.github.fantom.codeowners.indexing.CodeownersEntryOccurrence 5 | import com.github.fantom.codeowners.search.Filter 6 | import com.intellij.openapi.components.service 7 | import com.intellij.openapi.project.Project 8 | import com.intellij.openapi.ui.ComboBox 9 | import com.intellij.openapi.ui.DialogWrapper 10 | import com.intellij.ui.CollectionComboBoxModel 11 | import com.intellij.ui.ScrollPaneFactory 12 | import java.awt.BorderLayout 13 | import java.awt.event.ItemEvent 14 | import javax.swing.BorderFactory 15 | import javax.swing.JComponent 16 | import javax.swing.JPanel 17 | 18 | class CodeownersSearchFilterDialog( 19 | project: Project, 20 | codeownersFiles: List, // must be not empty 21 | ) : DialogWrapper(project) { 22 | private var contentPane: JPanel 23 | private var codeownersFileChooser = CodeownersFileChooser(codeownersFiles) 24 | 25 | private var orsPanel: OrsPanel 26 | 27 | var result: Pair>>? = null 28 | private set 29 | 30 | private val manager = project.service() 31 | 32 | private fun getOwners() = codeownersFileChooser 33 | .getChosenFile() 34 | .let(manager::getMentionedOwners) 35 | 36 | private fun createOrsPanel(): JPanel { 37 | val container = JPanel().apply { layout = BorderLayout() } 38 | container.add(orsPanel, BorderLayout.NORTH) 39 | return container 40 | } 41 | 42 | init { 43 | orsPanel = OrsPanel(getOwners()) 44 | 45 | val scrollPanel = ScrollPaneFactory.createScrollPane(createOrsPanel()) 46 | 47 | codeownersFileChooser.addItemListener { 48 | if (it.stateChange == ItemEvent.SELECTED) { 49 | orsPanel = OrsPanel(getOwners()) 50 | scrollPanel.setViewportView(createOrsPanel()) 51 | scrollPanel.revalidate() 52 | scrollPanel.repaint() 53 | } 54 | } 55 | 56 | // setup content panel 57 | contentPane = JPanel().also { cp -> 58 | cp.layout = BorderLayout() // to not let nested panels stretch vertically 59 | 60 | cp.add(JPanel() 61 | .also { 62 | it.layout = BorderLayout() // to make combobox stretch horizontally 63 | it.border = BorderFactory.createTitledBorder("Codeowners file") 64 | it.add(codeownersFileChooser) 65 | }, 66 | BorderLayout.NORTH, 67 | ) 68 | cp.add(scrollPanel, BorderLayout.CENTER) 69 | } 70 | 71 | init() 72 | } 73 | 74 | override fun doOKAction() { 75 | result = Pair((codeownersFileChooser.getChosenFile()), orsPanel.getOrs()) 76 | super.doOKAction() 77 | } 78 | 79 | override fun createCenterPanel(): JComponent { 80 | return contentPane 81 | } 82 | } 83 | 84 | private class CodeownersFileChooser( 85 | codeownersFiles: List, 86 | ): ComboBox() { 87 | init { 88 | toolTipText = "Select a CODEOWNERS file used to calculate ownership" 89 | model = CollectionComboBoxModel(codeownersFiles.map(::CodeownersEntryOccurrenceWrapper)) 90 | } 91 | 92 | fun getChosenFile() = (selectedItem as CodeownersEntryOccurrenceWrapper).codeownersEntryOccurrence 93 | 94 | class CodeownersEntryOccurrenceWrapper(val codeownersEntryOccurrence: CodeownersEntryOccurrence) { 95 | override fun toString(): String { 96 | return codeownersEntryOccurrence.url 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/services/CodeownersMatcher.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.services 2 | 3 | import com.github.fantom.codeowners.util.MatcherUtil 4 | import com.intellij.openapi.Disposable 5 | import com.intellij.openapi.components.Service 6 | import com.intellij.util.containers.IntObjectCache 7 | import java.util.* 8 | import java.util.regex.Pattern 9 | 10 | @Service(Service.Level.APP) 11 | class CodeownersMatcher : Disposable { 12 | 13 | private val cache = IntObjectCache() 14 | 15 | /** 16 | * Extracts alphanumeric parts from the regex pattern and checks if any of them is contained in the tested path. 17 | * Looking for the parts speed ups the matching and prevents from running whole regex on the string. 18 | * 19 | * @param pattern to explode 20 | * @param path to check 21 | * @return path matches the pattern 22 | */ 23 | fun match(pattern: Pattern?, path: String?): Boolean { 24 | if (pattern == null || path == null) { 25 | return false 26 | } 27 | synchronized(cache) { 28 | val hashCode = Objects.hash(pattern, path) 29 | if (!cache.containsKey(hashCode)) { 30 | val parts = MatcherUtil.getParts(pattern) 31 | var result = false 32 | if (parts.isEmpty() || MatcherUtil.matchAllParts(parts, path)) { 33 | try { 34 | result = pattern.matcher(path).find() 35 | } catch (ignored: StringIndexOutOfBoundsException) { 36 | } 37 | } 38 | cache.put(hashCode, result) 39 | } 40 | return cache[hashCode] 41 | } 42 | } 43 | 44 | override fun dispose() { 45 | cache.removeAll() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/services/PatternCache.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.services 2 | 3 | import com.github.fantom.codeowners.util.Glob 4 | import com.intellij.openapi.Disposable 5 | import com.intellij.openapi.components.Service 6 | import com.intellij.openapi.components.service 7 | import dk.brics.automaton.Automaton 8 | import dk.brics.automaton.RegExp 9 | import java.util.concurrent.ConcurrentHashMap 10 | import java.util.concurrent.ConcurrentMap 11 | 12 | /** 13 | * Component that prepares patterns for glob/regex statements and cache them. 14 | */ 15 | @Service(Service.Level.APP) 16 | class PatternCache : Disposable { 17 | private val PREFIX_TO_REGEX_CACHE: ConcurrentMap, Regex> = ConcurrentHashMap() 18 | 19 | private val GLOB_TO_AUTOMATON_CACHE: ConcurrentMap = ConcurrentHashMap() 20 | 21 | override fun dispose() { 22 | clearCache() 23 | } 24 | 25 | private fun clearCache() { 26 | PREFIX_TO_REGEX_CACHE.clear() 27 | GLOB_TO_AUTOMATON_CACHE.clear() 28 | } 29 | 30 | fun createRelativePattern(text: CharSequence, caseSensitive: Boolean): Regex { 31 | // TODO cache? 32 | return Regex(Glob.createFragmentRegex(text), if (caseSensitive) setOf() else setOf(RegexOption.IGNORE_CASE)) 33 | } 34 | 35 | fun getOrCreatePrefixRegex(prefixGlob: CharSequence, atAnyLevel: Boolean, dirOnly: Boolean): Regex { 36 | val key = Triple(prefixGlob, atAnyLevel, dirOnly) 37 | return PREFIX_TO_REGEX_CACHE.computeIfAbsent(key) { 38 | Glob.createPrefixRegex(it.first, it.second, it.third) 39 | } 40 | } 41 | 42 | fun getOrCreateGlobRegexes2(glob: String): Automaton { 43 | return GLOB_TO_AUTOMATON_CACHE.computeIfAbsent(glob) { 44 | RegExp( 45 | Glob.createDregex(it, acceptChildren = false, supportSquareBrackets = false) 46 | ).toAutomaton() 47 | } 48 | } 49 | 50 | companion object { 51 | fun getInstance() = service() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/settings/CodeownersSettingsConfigurable.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.settings 2 | 3 | import com.github.fantom.codeowners.CodeownersBundle 4 | import com.github.fantom.codeowners.ui.CodeownersSettingsPanel 5 | import com.intellij.openapi.options.SearchableConfigurable 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.util.Comparing 8 | import com.intellij.openapi.util.Disposer 9 | import com.intellij.openapi.vcs.VcsConfigurableProvider 10 | 11 | /** 12 | * Configuration interface for [CodeownersSettings]. 13 | */ 14 | @Suppress("UnsafeCallOnNullableType") 15 | class CodeownersSettingsConfigurable : SearchableConfigurable, VcsConfigurableProvider { 16 | 17 | private val settings = CodeownersSettings.getInstance() 18 | private var settingsPanel = CodeownersSettingsPanel() 19 | 20 | override fun getDisplayName(): String = CodeownersBundle.message("settings.displayName") 21 | 22 | override fun getHelpTopic(): String = displayName 23 | 24 | override fun createComponent() = settingsPanel.panel 25 | 26 | override fun isModified() = settingsPanel.run { 27 | !Comparing.equal(settings.missingCodeowners, missingCodeowners) || 28 | !Comparing.equal(settings.insertAtCursor, insertAtCursor) // || 29 | // !languagesSettings.equalSettings(settings.languagesSettings) 30 | } 31 | 32 | override fun apply() { 33 | settingsPanel.apply { 34 | settings.missingCodeowners = missingCodeowners 35 | settings.insertAtCursor = insertAtCursor 36 | // settings.languagesSettings = languagesSettings.settings 37 | } 38 | } 39 | 40 | override fun reset() { 41 | settingsPanel.apply { 42 | missingCodeowners = settings.missingCodeowners 43 | insertAtCursor = settings.insertAtCursor 44 | // languagesSettings.update(settings.languagesSettings.clone()) 45 | } 46 | } 47 | 48 | override fun disposeUIResources() { 49 | Disposer.dispose(settingsPanel) 50 | } 51 | 52 | override fun getConfigurable(project: Project) = this 53 | 54 | override fun getId() = helpTopic 55 | 56 | override fun enableSearch(option: String): Runnable? = null 57 | } 58 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/structureview/StructureViewElement.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.structureview 2 | 3 | import com.github.fantom.codeowners.lang.CodeownersFile 4 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTeamDefinition 5 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.impl.CodeownersTeamDefinitionImpl 6 | import com.intellij.ide.projectView.PresentationData 7 | import com.intellij.ide.structureView.StructureViewTreeElement 8 | import com.intellij.ide.util.treeView.smartTree.SortableTreeElement 9 | import com.intellij.ide.util.treeView.smartTree.TreeElement 10 | import com.intellij.navigation.ItemPresentation 11 | import com.intellij.psi.NavigatablePsiElement 12 | import com.intellij.psi.util.PsiTreeUtil 13 | 14 | class StructureViewElement(private val myElement: NavigatablePsiElement) : StructureViewTreeElement, SortableTreeElement { 15 | override fun getValue(): Any { 16 | return myElement 17 | } 18 | 19 | override fun navigate(requestFocus: Boolean) { 20 | myElement.navigate(requestFocus) 21 | } 22 | 23 | override fun canNavigate(): Boolean { 24 | return myElement.canNavigate() 25 | } 26 | 27 | override fun canNavigateToSource(): Boolean { 28 | return myElement.canNavigateToSource() 29 | } 30 | 31 | override fun getAlphaSortKey(): String { 32 | val name = myElement.name 33 | return name ?: "" 34 | } 35 | 36 | override fun getPresentation(): ItemPresentation { 37 | val presentation = myElement.presentation 38 | return presentation ?: PresentationData() 39 | } 40 | 41 | override fun getChildren(): Array { 42 | if (myElement is CodeownersFile) { 43 | val teamDefinitions = PsiTreeUtil.getChildrenOfTypeAsList(myElement, CodeownersTeamDefinition::class.java) 44 | return teamDefinitions.map { StructureViewElement(it as CodeownersTeamDefinitionImpl) } 45 | .toTypedArray() 46 | } 47 | return StructureViewTreeElement.EMPTY_ARRAY 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/structureview/StructureViewFactory.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.structureview 2 | 3 | import com.intellij.ide.structureView.StructureViewBuilder 4 | import com.intellij.ide.structureView.StructureViewModel 5 | import com.intellij.ide.structureView.TreeBasedStructureViewBuilder 6 | import com.intellij.lang.PsiStructureViewFactory 7 | import com.intellij.openapi.editor.Editor 8 | import com.intellij.psi.PsiFile 9 | 10 | class StructureViewFactory : PsiStructureViewFactory { 11 | override fun getStructureViewBuilder(psiFile: PsiFile): StructureViewBuilder { 12 | return object : TreeBasedStructureViewBuilder() { 13 | override fun createStructureViewModel(editor: Editor?): StructureViewModel { 14 | return StructureViewModel(psiFile) 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/structureview/StructureViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.structureview 2 | 3 | import com.github.fantom.codeowners.lang.kind.bitbucket.psi.CodeownersTeamDefinition 4 | import com.intellij.ide.structureView.StructureViewModel.ElementInfoProvider 5 | import com.intellij.ide.structureView.StructureViewModelBase 6 | import com.intellij.ide.structureView.StructureViewTreeElement 7 | import com.intellij.ide.util.treeView.smartTree.Sorter 8 | import com.intellij.psi.PsiFile 9 | 10 | class StructureViewModel(psiFile: PsiFile) : StructureViewModelBase(psiFile, StructureViewElement(psiFile)), ElementInfoProvider { 11 | override fun getSorters(): Array { 12 | return arrayOf(Sorter.ALPHA_SORTER) 13 | } 14 | 15 | override fun isAlwaysShowsPlus(element: StructureViewTreeElement): Boolean { 16 | return false 17 | } 18 | 19 | override fun isAlwaysLeaf(element: StructureViewTreeElement): Boolean { 20 | return element.value is CodeownersTeamDefinition 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/ui/CodeownersSettingsPanel.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 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/CachedConcurrentMap.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | import com.intellij.util.containers.ContainerUtil 4 | import java.util.concurrent.ConcurrentMap 5 | 6 | /** 7 | * [ConcurrentMap] wrapper with additional ability to cache values. 8 | */ 9 | class CachedConcurrentMap private constructor(private val fetcher: DataFetcher) { 10 | 11 | private val map: ConcurrentMap = ContainerUtil.createConcurrentWeakMap() 12 | 13 | companion object { 14 | fun create(fetcher: DataFetcher) = CachedConcurrentMap(fetcher) 15 | } 16 | 17 | operator fun get(key: K): V { 18 | // fetcher doesn't return nulls 19 | return map.computeIfAbsent(key) { fetcher.fetch(key) } 20 | } 21 | 22 | fun remove(key: K) { 23 | map.remove(key) 24 | } 25 | 26 | fun clear() { 27 | map.clear() 28 | } 29 | 30 | /** Fetcher interface. */ 31 | fun interface DataFetcher { 32 | /** 33 | * Fetches data for the given key. 34 | * 35 | * @param key key 36 | * @return value 37 | */ 38 | fun fetch(key: K): V 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | import org.jetbrains.annotations.NonNls 4 | 5 | /** 6 | * Class containing common constant variables. 7 | */ 8 | object Constants { 9 | /** Star sign. */ 10 | @NonNls 11 | val STAR = "*" 12 | 13 | /** Stars sign. */ 14 | @NonNls 15 | val DOUBLESTAR = "**" 16 | } 17 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/Debounced.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | import com.intellij.concurrency.JobScheduler 4 | import com.intellij.openapi.Disposable 5 | import com.intellij.openapi.project.DumbAwareRunnable 6 | import java.util.concurrent.ScheduledFuture 7 | import java.util.concurrent.TimeUnit 8 | 9 | /** 10 | * Debounced runnable class that allows to run command just once in case it was triggered too often. 11 | */ 12 | abstract class Debounced(private val delay: Int) : DumbAwareRunnable, Disposable { 13 | 14 | /** Timer that depends on the given [.delay] value. */ 15 | private var timer: ScheduledFuture<*>? = null 16 | 17 | /** Wrapper run() method to invoke [.timer] properly. */ 18 | override fun run() { 19 | run(null) 20 | } 21 | 22 | /** Wrapper run() method to invoke [.timer] properly. */ 23 | fun run(argument: T?) { 24 | timer?.cancel(false) 25 | timer = JobScheduler.getScheduler().schedule( 26 | DumbAwareRunnable { task(argument) }, 27 | delay.toLong(), 28 | TimeUnit.MILLISECONDS 29 | ) 30 | } 31 | 32 | /** Task to run in debounce way. */ 33 | protected abstract fun task(argument: T?) 34 | 35 | override fun dispose() { 36 | timer?.cancel(true) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/ExpiringMap.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | import com.intellij.openapi.util.Pair 4 | import com.jetbrains.rd.util.concurrentMapOf 5 | 6 | /** 7 | * Wrapper for ConcurrentHashMap that allows to expire values after given time. 8 | */ 9 | class ExpiringMap(private val time: Int) { 10 | 11 | private val map = concurrentMapOf>() 12 | 13 | operator fun get(key: K): V? { 14 | val current = System.currentTimeMillis() 15 | map[key]?.let { 16 | if (it.getSecond() + time > current) { 17 | return it.getFirst() 18 | } 19 | map.remove(key) 20 | } 21 | return null 22 | } 23 | 24 | operator fun set(key: K, value: V): V { 25 | val current = System.currentTimeMillis() 26 | map[key] = Pair.create(value, current) 27 | return value 28 | } 29 | 30 | fun clear() { 31 | map.clear() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/Listenable.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | /** 4 | * Abstracts a listenable object. 5 | */ 6 | interface Listenable { 7 | 8 | fun addListener(listener: T) 9 | 10 | fun removeListener(listener: T) 11 | } 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/MatcherUtil.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | import java.util.regex.Pattern 4 | 5 | /** 6 | * Util class to speed up and limit regex operation on the files paths. 7 | */ 8 | class MatcherUtil private constructor() { 9 | 10 | companion object { 11 | /** 12 | * Checks if given path contains all of the path parts. 13 | * 14 | * @param parts that should be contained in path 15 | * @param path to check 16 | * @return path contains all parts 17 | */ 18 | @Suppress("ReturnCount") 19 | fun matchAllParts(parts: Array, path: String): Boolean { 20 | var index = -1 21 | parts.forEach { 22 | index = path.indexOf(it, index) 23 | if (index == -1) { 24 | return false 25 | } 26 | } 27 | return true 28 | } 29 | 30 | /** 31 | * Extracts alphanumeric parts from [Pattern]. 32 | * 33 | * @param pattern to handle 34 | * @return extracted parts 35 | */ 36 | fun getParts(pattern: Pattern?): Array { 37 | if (pattern == null) { 38 | return emptyArray() 39 | } 40 | val parts: MutableList = ArrayList() 41 | val sPattern = pattern.toString() 42 | var part = StringBuilder() 43 | for (i in sPattern.indices) { 44 | val ch = sPattern[i] 45 | if (Character.isLetterOrDigit(ch)) { 46 | part.append(sPattern[i]) 47 | } else if (part.isNotEmpty()) { 48 | parts.add(part.toString()) 49 | part = StringBuilder() 50 | } 51 | } 52 | return parts.toTypedArray() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/Resources.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | import java.io.InputStream 4 | import java.util.Scanner 5 | 6 | /** 7 | * [Resources] util class that contains methods that work on plugin resources. 8 | */ 9 | object Resources { 10 | 11 | /** 12 | * Reads resource file and returns its content as a String. 13 | * 14 | * @param path Resource path 15 | * @return Content 16 | */ 17 | fun getResourceContent(path: String) = convertStreamToString(Resources::class.java.getResourceAsStream(path)) 18 | 19 | /** 20 | * Converts InputStream resource to String. 21 | * 22 | * @param inputStream Input stream 23 | * @return Content 24 | */ 25 | private fun convertStreamToString(inputStream: InputStream?) = 26 | inputStream?.let { stream -> Scanner(stream).useDelimiter("\\A").takeIf { it.hasNext() }?.next() ?: "" } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/TimeTracer.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | import com.github.fantom.codeowners.util.TimeTracer.LogEntry.Nested 4 | import com.intellij.openapi.diagnostic.Logger 5 | import com.intellij.openapi.util.Key 6 | import org.apache.commons.lang3.time.StopWatch 7 | import java.util.concurrent.ConcurrentLinkedQueue 8 | 9 | class TimeTracerStub(private val upper: TimeTracer) { 10 | fun start(name: String): TimeTracer { 11 | val tracer = upper.nested(name) 12 | tracer.start() 13 | return tracer 14 | } 15 | } 16 | 17 | class TimeTracer(val name: String, private val isRoot: Boolean = false) : AutoCloseable { 18 | sealed class LogEntry { 19 | abstract val elapsedNs: Long 20 | class Log(val name: String, private val ns: Long) : LogEntry() { 21 | override val elapsedNs: Long 22 | get() = ns 23 | 24 | override fun toString(): String { 25 | return "$name took $ns ns\n" 26 | } 27 | } 28 | class Nested(private val tracer: TimeTracer) : LogEntry() { 29 | override val elapsedNs: Long 30 | get() = tracer.nanoTime 31 | 32 | override fun toString() = "${tracer}\n" 33 | } 34 | } 35 | private val logs: ConcurrentLinkedQueue = ConcurrentLinkedQueue() 36 | 37 | private val sw = StopWatch() 38 | 39 | fun start() { 40 | sw.start() 41 | } 42 | 43 | private fun stop() { 44 | sw.stop() 45 | } 46 | 47 | val nanoTime 48 | get() = sw.nanoTime 49 | 50 | fun nested(name: String): TimeTracer { 51 | val nestedTracer = TimeTracer(name) 52 | logs.add(Nested(nestedTracer)) 53 | return nestedTracer 54 | } 55 | 56 | fun nested() = TimeTracerStub(this) 57 | 58 | override fun toString(): String { 59 | val sb = StringBuilder("$name took ${sw.nanoTime} ns\n") 60 | logs.forEach { 61 | sb.append(it.toString().prependIndent(" ")) 62 | } 63 | return sb.toString() 64 | } 65 | 66 | override fun close() { 67 | stop() 68 | if (isRoot) { 69 | logger.trace(toString()) 70 | } 71 | } 72 | 73 | companion object { 74 | val logger = Logger.getInstance(TimeTracer::class.java) 75 | 76 | fun wrap(name: String, f: (TimeTracer) -> T): T { 77 | val tracer = TimeTracer(name, true) 78 | tracer.start() 79 | return tracer.use { 80 | f(it) 81 | } 82 | } 83 | } 84 | } 85 | 86 | object TimeTracerKey : Key("TimeTracer") 87 | 88 | inline fun withNullableCloseable(closeable: AutoCloseable?, f: () -> T): T { 89 | return if (closeable == null) { 90 | f() 91 | } else { 92 | closeable.use { f() } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/kotlin/com/github/fantom/codeowners/util/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.github.fantom.codeowners.util 2 | 3 | import com.intellij.openapi.module.Module 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.project.modules 6 | import com.intellij.openapi.util.text.StringUtil 7 | import com.intellij.openapi.vfs.VfsUtilCore 8 | import com.intellij.openapi.vfs.VirtualFile 9 | 10 | /** 11 | * [Utils] class that contains various methods. 12 | */ 13 | object Utils { 14 | 15 | /** 16 | * Gets relative path of given [VirtualFile] and root directory. 17 | * 18 | * @param directory root directory 19 | * @param file file to get it's path 20 | * @return relative path 21 | */ 22 | fun getRelativePath(directory: VirtualFile, file: VirtualFile) = 23 | VfsUtilCore.getRelativePath(file, directory)?.let { 24 | it + ('/'.takeIf { file.isDirectory } ?: "") 25 | } 26 | 27 | /** 28 | * Searches for the module in the project that contains given file. 29 | * 30 | * @param file file 31 | * @param project project 32 | * @return module containing passed file or null 33 | */ 34 | fun getModuleForFile(file: VirtualFile, project: Project): Module? = 35 | project.modules.find { it.moduleContentScope.contains(file) } 36 | 37 | /** 38 | * Checks if file is in project directory. 39 | * 40 | * @param file file 41 | * @param project project 42 | * @return file is under directory 43 | */ 44 | fun isInProject(file: VirtualFile, project: Project) = 45 | StringUtil.startsWith(file.url, "temp://") || getModuleForFile(file, project) != null 46 | } 47 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/colorSchemes/CodeownersDarcula.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 14 | 21 | 27 | 33 | 38 | 45 | 50 | 55 | 60 | 65 | 71 | -------------------------------------------------------------------------------- /src/main/resources/colorSchemes/CodeownersDefault.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 14 | 21 | 27 | 33 | 38 | 45 | 50 | 55 | 60 | 65 | 71 | -------------------------------------------------------------------------------- /src/main/resources/icons/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/icons/icon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/inspectionDescriptions/CodeownersCoverPattern.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Detects rules that are covered by other rules later in file 4 |
E.g if we have such rules in our CODEOWNERS file 5 |
 6 |     /foo/bar/baz/ @owner1
 7 |     # some other rules
 8 |     /foo/ @owner2
 9 | 
10 | the first rule will never have effect, as it is overridden by the second rule 11 | 12 | -------------------------------------------------------------------------------- /src/main/resources/inspectionDescriptions/CodeownersDuplicateEntry.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Checks if pattern is duplicated. 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/inspectionDescriptions/CodeownersIncorrectEntry.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Checks if entry has correct form in specific according to the specific syntax. 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/inspectionDescriptions/CodeownersMetasymbolsUsage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Detects incorrect/redundant usage of metasymbols 4 |
5 | Detected issues: 6 | 7 |
  • Leading double stars (**/foo), if there are no other filepath delimiters after them
  • 8 |
  • Consecutive double stars (/foo/**/**/bar), as they may be replaced by single pair of stars
  • 9 |
    10 | 11 | -------------------------------------------------------------------------------- /src/main/resources/inspectionDescriptions/CodeownersRelativeEntry.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Checks if pattern is a relative path. 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/inspectionDescriptions/CodeownersUnusedPattern.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Detects patters that don't cover any files or directories 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/messages/CodeownersBundle.properties: -------------------------------------------------------------------------------- 1 | name=Codeowners Plugin 2 | 3 | action.appendFile.entryExists=Entry "{0}" already exists 4 | action.appendFile.entryExists.in=in {0} 5 | 6 | codeInspection.coverPattern=Cover pattern 7 | codeInspection.coverPattern.message=#ref is covered by {0} #loc 8 | codeInspection.duplicateEntry=Duplicate entry 9 | codeInspection.duplicateEntry.message=#ref entry is defined more than once #loc 10 | codeInspection.group=Codeowners 11 | codeInspection.unusedPattern=Unused pattern 12 | codeInspection.unusedPattern.message=#ref This pattern doesn't cover any file or directory #loc 13 | codeInspection.metasymbolsUsage=Metasymbols usage 14 | codeInspection.metasymbolsUsage.leadingDoubleStar=#ref Leading **/ can be removed since there are no other filepath delimiters #loc 15 | codeInspection.metasymbolsUsage.consecutiveDoubleStars=#ref One of these double star parts can be removed, because they cover each other #loc 16 | codeInspection.relativeEntry=Relative entry 17 | codeInspection.relativeEntry.message=#ref entry contains relative path 18 | codeInspection.incorrectEntry=Incorrect entry 19 | codeInspection.incorrectEntry.message=#ref has incorrect syntax:\n{0} #loc 20 | 21 | quick.fix.remove.rule=Remove rule 22 | quick.fix.relative.entry=Remove relative part 23 | 24 | daemon.lineMarker.directory=Directory pattern 25 | daemon.missingCodeowners=Missing CODEOWNERS file in project 26 | daemon.missingGitignore.create=Create .gitignore 27 | daemon.cancel=Cancel 28 | 29 | highlighter.brackets=Brackets 30 | highlighter.comment=Comment 31 | highlighter.header=Header 32 | highlighter.negation=Negation 33 | highlighter.section=Section 34 | highlighter.slash=Slash 35 | highlighter.value=Value 36 | highlighter.name=Name 37 | highlighter.configValue=Config value 38 | highlighter.configName=Config name 39 | highlighter.unused=Unused entry 40 | 41 | codeowners.colorSettings.displayName=CODEOWNERS files 42 | 43 | outer.label=Outer ignore rules: 44 | 45 | projectView.ignored=Ignored (.ignore plugin) 46 | projectView.containsHidden={0} hidden 47 | 48 | template.container.global=Global templates (OS, IDE, ...) 49 | template.container.root=Languages, frameworks 50 | template.container.user=User templates 51 | template.container.starred=Starred templates 52 | 53 | tokenType./= 54 | tokenType.BRACKET_LEFT= 55 | tokenType.BRACKET_RIGHT= 56 | tokenType.COMMENT= 57 | tokenType.CRLF= 58 | tokenType.HEADER=
    59 | tokenType.SECTION=
    60 | tokenType.VALUE= 61 | 62 | settings.displayName=Codeowners File Support 63 | settings.general=General settings 64 | settings.general.missingCodeownersFile=Notify about &missing CODEOWNERS file in the project 65 | settings.general.ignoredColor=Edit ignored files text color && font 66 | settings.general.ignoredFileStatus=Enable ignored file status &coloring 67 | settings.general.insertAtCursor=Insert new owners entries at cursor &position 68 | settings.general.unignoreFiles=Enable unignore files group (add entries prefixed with !) 69 | settings.general.notifyIgnoredEditing=Inform about editing ignored file 70 | settings.userTemplates=User templates 71 | settings.userTemplates.noTemplateSelected=No template is selected. 72 | settings.userTemplates.dialogTitle=User Template 73 | settings.userTemplates.dialogDescription=Set new template name: 74 | settings.userTemplates.dialogError=Template name cannot be empty 75 | settings.userTemplates.default.name=Example user template 76 | settings.userTemplates.default.content=### Example user template\n\n# IntelliJ project files\n.idea\n*.iml\nout\ngen 77 | settings.languagesSettings=Languages settings 78 | settings.languagesSettings.table.name=Language 79 | settings.languagesSettings.table.newFile=Show in "New > .ignore file" 80 | settings.languagesSettings.table.enable=Enable ignoring 81 | action.exportTemplates=Export Templates 82 | action.exportTemplates.description=Export ignore templates to file 83 | action.exportTemplates.wrapper=Export Selected User Templates to File... 84 | action.exportTemplates.error=User templates export failed. 85 | action.exportTemplates.success=Exported templates: {0} 86 | action.exportTemplates.success.title=Ignore User Templates 87 | action.importTemplates=Import Templates 88 | action.importTemplates.description=Import ignore templates from file 89 | action.importTemplates.wrapper=Import Ignore Templates 90 | action.importTemplates.wrapper.description=Please select the user ignore templates file to import. 91 | action.importTemplates.success=Imported templates: {0} 92 | action.importTemplates.error=User templates import failed. 93 | action.group.by.codeowner = Codeowner 94 | 95 | notification.group=.ignore plugin 96 | notification.group.update=.ignore plugin update information 97 | 98 | status.bar.codeowners.widget.name=Codeowners 99 | search.scope.name=File ownership -------------------------------------------------------------------------------- /src/main/resources/sample.codeowners: -------------------------------------------------------------------------------- 1 | ### Header element 2 | 3 | ## First section 4 | * @default-owner 5 | *.java @java-owner 6 | # Team definition 7 | @@@MyDevs @PeterTheHacker @PeterTheJavaExpert ann@scala.lang @@JSDevs 8 | # Owner with spaces in name 9 | *.ts @"Paul the JSGuru" @@"Dev Ops Team" 10 | # Email owner 11 | docs/* docs@example.com 12 | 13 | # Path with spaces doesn't work yet 14 | # "a/path with spaces/*" docs@example.com 15 | 16 | # ci/* will match all files in the directory ci, but not deeper in 17 | # the directory hierarchy (so ci/jobs/prod.yml will not match). 18 | ci/* @devops 19 | 20 | # Negation 21 | !ci/playgrounds.yml 22 | 23 | # It's also possible to use double-asterisk globs. Here's an example that will match 24 | # all JS files under /src/components. 25 | src/components/**/*.js @@MyDevs 26 | 27 | # GroovyMaster owns any files in the groovy directory anywhere in the 28 | # file tree (e.g., src/main/groovy/com/x/y/z.groovy). 29 | groovy/ @GroovyMaster 30 | 31 | # Files starting with a `#` or a `!` can still be used by escaping them. 32 | \#myfile.rb @PeterTheHacker 33 | \!yourfile.rb @PaulTheJSGuru --------------------------------------------------------------------------------